diff --git a/docs/source/plugins/example/plugin.py b/docs/source/plugins/example/plugin.py index e99f9e85..0ea51b23 100644 --- a/docs/source/plugins/example/plugin.py +++ b/docs/source/plugins/example/plugin.py @@ -2,7 +2,7 @@ from openflexure_microscope.plugins import MicroscopePlugin from openflexure_microscope.api.views import MicroscopeViewPlugin from openflexure_microscope.api.utilities import JsonResponse -from openflexure_microscope.common.tasks import taskify +from openflexure_microscope.common.labthings_core.tasks import taskify import os import time diff --git a/openflexure_microscope/__init__.py b/openflexure_microscope/__init__.py index ad56e205..85af80b2 100644 --- a/openflexure_microscope/__init__.py +++ b/openflexure_microscope/__init__.py @@ -1,8 +1,7 @@ -__all__ = ["Microscope", "config", "task", "utilities"] +__all__ = ["Microscope", "config", "utilities"] __version__ = "0.1.0" from .microscope import Microscope from . import config from . import utilities -from . import task from . import common diff --git a/openflexure_microscope/api/__init__.py b/openflexure_microscope/api/__init__.py index 9a39cfda..e69de29b 100644 --- a/openflexure_microscope/api/__init__.py +++ b/openflexure_microscope/api/__init__.py @@ -1,3 +0,0 @@ -__all__ = ["utilities", "views"] - -from . import utilities, views diff --git a/openflexure_microscope/api/app.py b/openflexure_microscope/api/app.py index c3db5f89..6e769489 100644 --- a/openflexure_microscope/api/app.py +++ b/openflexure_microscope/api/app.py @@ -3,189 +3,117 @@ import time import atexit import logging -import sys import os +import pkg_resources from flask import Flask, jsonify, send_file -from serial import SerialException from datetime import datetime -from flask_cors import CORS +from flask_cors import CORS, cross_origin -from openflexure_microscope.api.exceptions import JSONExceptionHandler -from openflexure_microscope.api.utilities import list_routes +from openflexure_microscope.api.utilities import list_routes, init_default_extensions -from openflexure_microscope import Microscope +from openflexure_microscope.config import ( + settings_file_path, + JSONEncoder, + USER_EXTENSIONS_PATH, +) -from openflexure_microscope.camera.capture import build_captures_from_exif -from openflexure_microscope.config import settings_file_path, JSONEncoder -from openflexure_microscope.api.v1 import blueprints -from openflexure_microscope.api import v2 +from openflexure_microscope.common.flask_labthings.quick import create_app +from openflexure_microscope.common.flask_labthings.extensions import find_extensions -# Import device modules -# NB this will eventually be handled by the RC file, so you can choose what device -# class should be attached. -try: - from openflexure_microscope.camera.pi import PiCameraStreamer -except ImportError: - logging.warning("Unable to import PiCameraStreamer") -from openflexure_microscope.camera.mock import MockStreamer - -from openflexure_microscope.stage.sanga import SangaStage -from openflexure_microscope.stage.mock import MockStage +from openflexure_microscope.api.microscope import default_microscope as api_microscope +from openflexure_microscope.api.v2 import views # Handle logging is_gunicorn = "gunicorn" in os.environ.get("SERVER_SOFTWARE", "") DEFAULT_LOGFILE = settings_file_path("openflexure_microscope.log") +logger = logging.getLogger() if (__name__ == "__main__") or (not is_gunicorn): # If imported, but not by gunicorn print("Letting sys handle logs") - logger = logging.getLogger() logger.setLevel(logging.DEBUG) else: # Direct standard Python logging to file and console - root = logging.getLogger() error_formatter = logging.Formatter( "[%(asctime)s] [%(threadName)s] [%(levelname)s] %(message)s" ) rotating_logfile = logging.handlers.RotatingFileHandler( - DEFAULT_LOGFILE, maxBytes=1000000, backupCount=7 + DEFAULT_LOGFILE, maxBytes=1_000_000, backupCount=7 ) error_handlers = [rotating_logfile, logging.StreamHandler()] for handler in error_handlers: handler.setFormatter(error_formatter) - root.addHandler(handler) + logger.addHandler(handler) - root.setLevel(logging.getLogger("gunicorn.error").level) - -# Create a dummy microscope object, with no hardware attachments -api_microscope = Microscope() - -# Initialise camera -logging.debug("Creating camera object...") -try: - api_camera = PiCameraStreamer() -except Exception as e: - logging.error(e) - logging.warning("No valid camera hardware found. Falling back to mock camera!") - api_camera = MockStreamer() - -# Initialise stage -logging.debug("Creating stage object...") -try: - api_stage = SangaStage() -except Exception as e: - logging.error(e) - logging.warning("No valid stage hardware found. Falling back to mock stage!") - api_stage = MockStage() - -# Attach devices to microscope -logging.debug("Attaching devices to microscope...") -api_microscope.attach(api_camera, api_stage) - -# Restore loaded capture array to camera object -logging.debug("Restoring captures...") -api_microscope.camera.images = build_captures_from_exif( - api_microscope.camera.paths["default"] -) - -logging.debug("Microscope successfully attached!") - - -# Generate API URI based on version from filename -def uri(suffix, api_version, base=None): - if not base: - base = "/api/{}".format(api_version) - return_uri = base + suffix - logging.debug("Created app route: {}".format(return_uri)) - return return_uri + logger.setLevel(logging.getLogger("gunicorn.error").level) # Create flask app -app = Flask(__name__) -app.url_map.strict_slashes = False +app, labthing = create_app( + __name__, + prefix="/api/v2", + title=f"OpenFlexure Microscope {api_microscope.name}", + description="Test LabThing-based API for OpenFlexure Microscope", + version=pkg_resources.get_distribution("openflexure_microscope").version, +) + +# Enable CORS for some routes outside of LabThings +cors = CORS(app) # Use custom JSON encoder app.json_encoder = JSONEncoder -# Enable CORS everywhere -CORS(app, resources=r"*") +# Attach lab devices +labthing.add_component(api_microscope, "org.openflexure.microscope") -# Make errors more API friendly -handler = JSONExceptionHandler(app) +# Attach extensions +if not os.path.isfile(USER_EXTENSIONS_PATH): + init_default_extensions(USER_EXTENSIONS_PATH) +for extension in find_extensions(USER_EXTENSIONS_PATH): + labthing.register_extension(extension) -# WEBAPP ROUTES +# Attach captures resources +labthing.add_view(views.CaptureList, f"/captures") +labthing.add_root_link(views.CaptureList, "captures") -# Base routes -base_blueprint = blueprints.base.construct_blueprint(api_microscope) -app.register_blueprint(base_blueprint, url_prefix=uri("", "v1")) +labthing.add_view(views.CaptureView, f"/captures/") +labthing.add_view(views.CaptureDownload, f"/captures//download/") +labthing.add_view(views.CaptureTags, f"/captures//tags") +labthing.add_view(views.CaptureMetadata, f"/captures//metadata") -# Stage routes -stage_blueprint = blueprints.stage.construct_blueprint(api_microscope) -app.register_blueprint(stage_blueprint, url_prefix=uri("/stage", "v1")) +# Attach settings and status resources +labthing.add_view(views.SettingsProperty, f"/settings") +labthing.add_root_link(views.SettingsProperty, "settings") +labthing.add_view(views.NestedSettingsProperty, "/settings/") +labthing.add_view(views.StatusProperty, "/status") +labthing.add_view(views.NestedStatusProperty, "/status/") +labthing.add_root_link(views.StatusProperty, "status") -# Camera routes -camera_blueprint = blueprints.camera.construct_blueprint(api_microscope) -app.register_blueprint(camera_blueprint, url_prefix=uri("/camera", "v1")) +# Attach streams resources +labthing.add_view(views.MjpegStream, f"/streams/mjpeg") +labthing.add_view(views.SnapshotStream, f"/streams/snapshot") -# Plugin routes -plugin_blueprint = blueprints.plugins.construct_blueprint(api_microscope) -app.register_blueprint(plugin_blueprint, url_prefix=uri("/plugin", "v1")) - -# Task routes -task_blueprint = blueprints.task.construct_blueprint(api_microscope) -app.register_blueprint(task_blueprint, url_prefix=uri("/task", "v1")) - -### V2 -# Root routes -v2_root_blueprint = v2.blueprints.root.construct_blueprint(api_microscope) -app.register_blueprint(v2_root_blueprint, url_prefix=uri("/", "v2")) - -v2_streams_blueprint = v2.blueprints.streams.construct_blueprint(api_microscope) -app.register_blueprint(v2_streams_blueprint, url_prefix=uri("/streams", "v2")) - -# Captures routes -v2_captures_blueprint = v2.blueprints.captures.construct_blueprint(api_microscope) -app.register_blueprint(v2_captures_blueprint, url_prefix=uri("/captures", "v2")) - -# Settings routes -v2_settings_blueprint = v2.blueprints.settings.construct_blueprint(api_microscope) -app.register_blueprint(v2_settings_blueprint, url_prefix=uri("/settings", "v2")) - -# Status routes -v2_status_blueprint = v2.blueprints.status.construct_blueprint(api_microscope) -app.register_blueprint(v2_status_blueprint, url_prefix=uri("/status", "v2")) - -# Plugins routes -v2_plugin_blueprint = v2.blueprints.plugins.construct_blueprint(api_microscope) -app.register_blueprint(v2_plugin_blueprint, url_prefix=uri("/plugins", "v2")) - -# Tasks routes -v2_tasks_blueprint = v2.blueprints.tasks.construct_blueprint(api_microscope) -app.register_blueprint(v2_tasks_blueprint, url_prefix=uri("/tasks", "v2")) - -# Actions routes -v2_actions_blueprint = v2.blueprints.actions.construct_blueprint(api_microscope) -app.register_blueprint(v2_actions_blueprint, url_prefix=uri("/actions", "v2")) +# Attach microscope action resources +labthing.add_view(views.actions.ActionsView, "/actions") +for name, action in views.enabled_root_actions().items(): + view_class = action["view_class"] + rule = action["rule"] + labthing.add_view(view_class, f"/actions{rule}") @app.route("/routes") +@cross_origin() def routes(): """ List of all connected API routes - - .. :quickref: Global; Routes - - :>header Accept: application/json - :>header Content-Type: application/json - :status 200: stream active """ return jsonify(list_routes(app)) @@ -194,12 +122,6 @@ def routes(): def err_log(): """ Most recent 1mb of log output - - .. :quickref: Global; Log - - :>header Accept: application/json - :>header Content-Type: application/json - :status 200: stream active """ timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") return send_file( @@ -211,7 +133,6 @@ def err_log(): # Automatically clean up microscope at exit def cleanup(): - global api_microscope logging.debug("App teardown started...") logging.debug("Settling...") time.sleep(0.5) diff --git a/openflexure_microscope/api/default_extensions/__init__.py b/openflexure_microscope/api/default_extensions/__init__.py new file mode 100644 index 00000000..9ad73b3c --- /dev/null +++ b/openflexure_microscope/api/default_extensions/__init__.py @@ -0,0 +1,3 @@ +from .autofocus import autofocus_extension_v2 +from .scan import scan_extension_v2 +from .zip_builder import zip_extension_v2 diff --git a/openflexure_microscope/api/default_extensions/autofocus.py b/openflexure_microscope/api/default_extensions/autofocus.py new file mode 100644 index 00000000..2832835a --- /dev/null +++ b/openflexure_microscope/api/default_extensions/autofocus.py @@ -0,0 +1,369 @@ +from openflexure_microscope.common.flask_labthings.find import find_component +from openflexure_microscope.common.flask_labthings.extensions import BaseExtension +from openflexure_microscope.common.flask_labthings.view import View +from openflexure_microscope.common.flask_labthings.decorators import ( + ThingAction, + ThingProperty, +) + +from openflexure_microscope.devel import JsonResponse, request, jsonify, taskify, abort +from openflexure_microscope.utilities import set_properties + +import time +import numpy as np +import threading +import logging +from scipy import ndimage +from contextlib import contextmanager + +### Autofocus utilities + + +class JPEGSharpnessMonitor: + def __init__(self, microscope, timeout=60): + self.microscope = microscope + self.camera = microscope.camera + self.stage = microscope.stage + self.jpeg_sizes = [] + self.jpeg_times = [] + self.stage_positions = [] + self.stage_times = [] + self.stop_event = threading.Event() + self.timeout = timeout + self.keep_alive() + self.background_thread = None + + def is_alive(self): + if self.background_thread is None: + return False + else: + return self.background_thread.is_alive() + + def should_stop(self): + import time + + return time.time() - self.kept_alive > self.timeout + + def keep_alive(self): + import time + + self.kept_alive = time.time() + + def start(self): + "Start monitoring sharpness by looking at JPEG size" + self.background_thread = threading.Thread(target=self._measure_jpegs) + self.background_thread.start() + return self + + def stop(self): + "Stop the background thread" + self.stop_event.set() + self.background_thread.join() + + def _measure_jpegs(self): + "Function that runs in a background thread to record sharpness" + logging.info("Starting sharpness measurement in background thread") + self.keep_alive() + while not self.stop_event.is_set() and not self.should_stop(): + self.jpeg_sizes.append(self.jpeg_size()) + self.jpeg_times.append(time.time()) + if self.stop_event.is_set(): + logging.info("Cleanly stopped sharpness measurement in background thread") + if self.should_stop(): + logging.info("Sharpness measurement timed out and has stopped") + + def jpeg_size(self): + """Return the size of a frame from the MJPEG stream""" + return len(self.camera.get_frame()) + + def focus_rel(self, dz, backlash=False, **kwargs): + self.keep_alive() + self.stage_times.append(time.time()) + self.stage_positions.append(self.stage.position) + self.stage.move_rel([0, 0, dz], backlash=backlash, **kwargs) + self.stage_times.append(time.time()) + self.stage_positions.append(self.stage.position) + i = len(self.stage_positions) - 2 + return i, self.stage_positions[-1][2] + + def move_data(self, istart, istop=None): + "Extract sharpness as a function of (interpolated) z" + global np, logging + if istop is None: + istop = istart + 2 + jpeg_times = np.array(self.jpeg_times) + jpeg_sizes = np.array(self.jpeg_sizes) + stage_times = np.array(self.stage_times)[istart:istop] + stage_zs = np.array(self.stage_positions)[istart:istop, 2] + start = np.argmax(jpeg_times > stage_times[0]) + stop = np.argmax(jpeg_times > stage_times[1]) + if stop < 1: + stop = len(jpeg_times) + logging.debug("changing stop to {}".format(stop)) + jpeg_times = jpeg_times[start:stop] + jpeg_zs = np.interp(jpeg_times, stage_times, stage_zs) + return jpeg_times, jpeg_zs, jpeg_sizes[start:stop] + + def sharpest_z_on_move(self, index): + """Return the z position of the sharpest image on a given move""" + jt, jz, js = self.move_data(index) + return jz[np.argmax(js)] + + def data_dict(self): + """Return the gathered data as a single convenient dictionary""" + data = {} + for k in ["jpeg_times", "jpeg_sizes", "stage_times", "stage_positions"]: + data[k] = getattr(self, k) + return data + + +def decimate_to(shape, image): + """Decimate an image to reduce its size if it's too big.""" + decimation = np.max( + np.ceil(np.array(image.shape, dtype=np.float)[: len(shape)] / np.array(shape)) + ) + return image[:: int(decimation), :: int(decimation), ...] + + +def sharpness_sum_lap2(rgb_image): + """Return an image sharpness metric: sum(laplacian(image)**")""" + # image_bw=np.mean(decimate_to((1000,1000), rgb_image),2) + image_bw = np.mean(rgb_image, 2) + image_lap = ndimage.filters.laplace(image_bw) + return np.mean(image_lap.astype(np.float) ** 4) + + +def sharpness_edge(image): + """Return a sharpness metric optimised for vertical lines""" + gray = np.mean(image.astype(float), 2) + n = 20 + edge = np.array([[-1] * n + [1] * n]) + return np.sum( + [np.sum(ndimage.filters.convolve(gray, W) ** 2) for W in [edge, edge.T]] + ) + + +### Autofocus extension + + +def measure_sharpness(microscope, metric_fn=sharpness_sum_lap2): + """Measure the sharpness of the camera's current view.""" + if hasattr(microscope.camera, "array"): + return metric_fn(microscope.camera.array(use_video_port=True)) + + +def autofocus(microscope, dz, settle=0.5, metric_fn=sharpness_sum_lap2): + """Perform a simple autofocus routine. + The stage is moved to z positions (relative to current position) in dz, + and at each position an image is captured and the sharpness function + evaulated. We then move back to the position where the sharpness was + highest. No interpolation is performed. + dz is assumed to be in ascending order (starting at -ve values) + """ + camera = microscope.camera + stage = microscope.stage + + with set_properties(stage, backlash=256), stage.lock, camera.lock: + sharpnesses = [] + positions = [] + camera.annotate_text = "" + + for _ in stage.scan_z(dz, return_to_start=False): + positions.append(stage.position[2]) + time.sleep(settle) + sharpnesses.append(measure_sharpness(microscope, metric_fn)) + + newposition = positions[np.argmax(sharpnesses)] + stage.move_rel([0, 0, newposition - stage.position[2]]) + + return positions, sharpnesses + + +@contextmanager +def monitor_sharpness(microscope): + m = JPEGSharpnessMonitor(microscope) + m.start() + try: + yield m + finally: + m.stop() + + +def move_and_find_focus(microscope, dz): + """Make a relative Z move and return the peak sharpness position""" + with monitor_sharpness(microscope) as m: + m.focus_rel(dz) + return m.sharpest_z_on_move(0) + + +def fast_autofocus(microscope, dz=2000, backlash=None): + """Perform a down-up-down-up autofocus""" + with monitor_sharpness(microscope) as m: + i, z = m.focus_rel(-dz / 2) + i, z = m.focus_rel(dz) + fz = m.sharpest_z_on_move(i) + if backlash is None: + i, z = m.focus_rel(-dz) # move all the way to the start so it's consistent + else: + i, z = m.focus_rel(fz - z - backlash) + m.focus_rel(fz - z) + return m.data_dict() + + +def fast_up_down_up_autofocus( + microscope, dz=2000, target_z=0, initial_move_up=True, mini_backlash=150 +): + """Autofocus by measuring on the way down, and moving back up with feedback. + + This autofocus method is very efficient, as it only passes the peak once. + The sequence of moves it performs is: + + 1. Move to the top of the range `dz/2` (can be disabled) + + 2. Move down by `dz` while monitoring JPEG size to find the focus. + + 3. Move back up to the `target_z` position, relative to the sharpest image. + + 4. Measure the sharpness, and compare against the curve recorded in (2) to \\ + estimate how much further we need to go. Make this move, to reach our \\ + target position. + + Moving back to the target position in two steps allows us to correct for + backlash, by using the sharpness-vs-z curve as a rough encoder for Z. + + Parameters: + dz: number of steps over which to scan (optional, default 2000) + + target_z: we aim to finish at this position, relative to focus. This may + be useful if, for example, you want to acquire a stack of images in Z. + It is optional, and the default value of 0 will finish at the focus. + + initial_move_up: (optional, default True) set this to `False` to move down + from the starting position. Mostly useful if you're able to combine + the initial move with something else, e.g. moving to the next scan point. + + mini_backlash: (optional, default 50) is a small extra move made in step + 3 to help counteract backlash. It should be small enough that you + would always expect there to be greater backlash than this. Too small + might slightly hurt accuracy, but is unlikely to be a big issue. Too big + may cause you to overshoot, which is a problem. + """ + with monitor_sharpness(microscope) as m, microscope.camera.lock: + # Ensure the MJPEG stream has started + microscope.camera.start_stream_recording() + + df = dz # TODO: refactor so I actually use dz in the code below! + if initial_move_up: + m.focus_rel(df / 2) + # move down + i, z = m.focus_rel(-df) + # now inspect where the sharpest point is, and estimate the sharpness + # (JPEG size) that we should find at the start of the Z stack + jt, jz, js = m.move_data(i) + best_z = jz[np.argmax(js)] + target_s = np.interp( + [best_z + target_z], jz[::-1], js[::-1] + ) # NB jz is decreasing + + # now move to the start of the z stack + i, z = m.focus_rel( + best_z + target_z - z + mini_backlash + ) # takes us to the start of the stack + + # We've deliberately undershot - figure out how much further we should move based on the curve + current_js = m.jpeg_size() + imax = np.argmax(js) # we want to crop out just the bit below the peak + js = js[imax:] # NB z is in DECREASING order + jz = jz[imax:] + inow = np.argmax( + js < current_js + ) # use the curve we recorded to estimate our position + # TODO: fancy interpolation stuff + + # So, the Z position corresponding to our current sharpness value is zs[inow] + # That means we should move forwards, by best_z - zs[inow] + correction_move = best_z + target_z - jz[inow] + logging.debug( + "Fast autofocus scan: correcting backlash by moving {} steps".format( + correction_move + ) + ) + m.focus_rel(correction_move) + return m.data_dict() + + +class MeasureSharpnessAPI(View): + def post(self): + microscope = find_component("org.openflexure.microscope") + + if not microscope: + abort(503, "No microscope connected. Unable to measure sharpness.") + + return jsonify({"sharpness": measure_sharpness(microscope)}) + + +@ThingAction +class AutofocusAPI(View): + """ + Run a standard autofocus + """ + + def post(self): + payload = JsonResponse(request) + microscope = find_component("org.openflexure.microscope") + + if not microscope: + abort(503, "No microscope connected. Unable to autofocus.") + + # Figure out the range of z values to use + dz = payload.param("dz", default=np.linspace(-300, 300, 7), convert=np.array) + + if microscope.has_real_stage(): + logging.info("Running autofocus...") + task = taskify(autofocus)(microscope, dz) + + # return a handle on the autofocus task + return jsonify(task.state), 201 + + else: + abort(503, "No stage connected. Unable to autofocus.") + + +@ThingAction +class FastAutofocusAPI(View): + """ + Run a fast autofocus + """ + + def post(self): + payload = JsonResponse(request) + microscope = find_component("org.openflexure.microscope") + + if not microscope: + abort(503, "No microscope connected. Unable to autofocus.") + + # Figure out the parameters to use + dz = payload.param("dz", default=2000, convert=int) + backlash = payload.param("backlash", default=0, convert=int) + if backlash < 0: + backlash = 0 + + if microscope.has_real_stage(): + logging.info("Running autofocus...") + task = taskify(fast_autofocus)(microscope, dz, backlash=backlash) + + # return a handle on the autofocus task + return jsonify(task.state), 201 + + else: + abort(503, "No stage connected. Unable to autofocus.") + + +autofocus_extension_v2 = BaseExtension("org.openflexure.autofocus", version="2.0.0-beta.1") + +autofocus_extension_v2.add_method(fast_autofocus, "fast_autofocus") +autofocus_extension_v2.add_method(autofocus, "autofocus") + +autofocus_extension_v2.add_view(MeasureSharpnessAPI, "/measure_sharpness") +autofocus_extension_v2.add_view(AutofocusAPI, "/autofocus") +autofocus_extension_v2.add_view(FastAutofocusAPI, "/fast_autofocus") diff --git a/openflexure_microscope/api/default_extensions/scan.py b/openflexure_microscope/api/default_extensions/scan.py new file mode 100644 index 00000000..3b25e2e0 --- /dev/null +++ b/openflexure_microscope/api/default_extensions/scan.py @@ -0,0 +1,401 @@ +import itertools +import logging +import uuid +from typing import Tuple +from functools import reduce + +from openflexure_microscope.camera.base import generate_basename +from openflexure_microscope.common.flask_labthings.find import ( + find_component, + find_extension, +) +from openflexure_microscope.common.flask_labthings.extensions import BaseExtension +from openflexure_microscope.common.flask_labthings.decorators import ( + marshal_task, + use_args, + ThingAction, +) +from openflexure_microscope.common.flask_labthings import fields + +from openflexure_microscope.devel import taskify, abort, update_task_progress + +from openflexure_microscope.common.flask_labthings.view import View +import time + + +### Grid construction + + +def construct_grid(initial, step_sizes, n_steps, style="raster"): + """ + Given an initial position, step sizes, and number of steps, + construct a 2-dimensional list of scan x-y positions. + """ + arr = [] + + for i in range(n_steps[0]): # x axis + arr.append([]) + for j in range(n_steps[1]): # y axis + # Create a coordinate array + coord = [initial[ax] + [i, j][ax] * step_sizes[ax] for ax in range(2)] + # Append coordinate array to position grid + arr[i].append(tuple(coord)) + + # Style modifiers + if style == "snake": + for i, line in enumerate(arr): + if i % 2 != 0: + line.reverse() + + return arr + + +def flatten_grid(grid): + """ + Convert a 3D list of scan positions into a flat list + of sequential positions. + """ + + grid = list(itertools.chain(*grid)) + return grid + + +### Progress + +_images_to_be_captured: int = 1 +_images_captured_so_far: int = 0 + + +def progress(): + progress = (_images_captured_so_far / _images_to_be_captured) * 100 + logging.info(progress) + return progress + + +### Capturing + + +def capture( + microscope, + basename, + scan_id, + temporary: bool = False, + use_video_port: bool = False, + resize: Tuple[int, int] = None, + bayer: bool = False, + metadata: dict = {}, + tags: list = [], +): + + # Construct a tile filename + filename = "{}_{}_{}_{}".format(basename, *microscope.stage.position) + folder = "SCAN_{}".format(basename) + + # Create output object + output = microscope.camera.new_image( + temporary=temporary, filename=filename, folder=folder + ) + + # Capture + microscope.camera.capture( + output.file, use_video_port=use_video_port, resize=resize, bayer=bayer + ) + + # Affix metadata + if "scan" not in tags: + tags.append("scan") + + # Inject system metadata + output.put_metadata(microscope.metadata, system=True) + + # Insert custom metadata + output.put_metadata(metadata) + + # Insert custom tags + output.put_tags(tags) + + +### Scanning + + +def tile( + microscope, + basename: str = None, + temporary: bool = False, + step_size: int = [2000, 1500, 100], + grid: list = [3, 3, 5], + style="raster", + autofocus_dz: int = 50, + use_video_port: bool = False, + resize: Tuple[int, int] = None, + bayer: bool = False, + fast_autofocus=False, + metadata: dict = {}, + tags: list = [], +): + global _images_to_be_captured + global _images_captured_so_far + + # Keep task progress + # TODO: Make this line not nasty + _images_to_be_captured = reduce((lambda x, y: x * y), grid) + _images_captured_so_far = 0 + + # Generate a basename if none given + if not basename: + basename = generate_basename() + + # Generate a stack ID + scan_id = uuid.uuid4() + + # Store initial position + initial_position = microscope.stage.position + + # Add scan metadata + if "time" not in metadata: + metadata["time"] = generate_basename() + + metadata.update( + { + "scan_id": scan_id, + "basename": basename, + "scan_parameters": { + "step_size": step_size, + "grid": grid, + "style": style, + "autofocus_dz": autofocus_dz, + }, + } + ) + + # Check if autofocus is enabled + autofocus_extension = find_extension("org.openflexure.autofocus") + if ( + autofocus_dz + and autofocus_extension + and microscope.has_real_stage() + and microscope.has_real_camera() + ): + autofocus_enabled = True + else: + autofocus_enabled = False + + z_stack_dz = ( + grid[2] * step_size[2] if grid[2] > 1 else 0 + ) # shorthand for Z stack range + + # Construct an x-y grid (worry about z later) + x_y_grid = construct_grid(initial_position, step_size[:2], grid[:2], style=style) + + # Keep the initial Z position the same as our current position + next_z = initial_position[2] + if fast_autofocus: # If fast autofocus is enabled, make + next_z += autofocus_dz / 2 # sure we start from the top of the range + initial_z = next_z # Save this value for use in raster scans + + # Now step through each point in the x-y coordinate array + for line in x_y_grid: + # If rastering, rather than snake (or eventually spiral) + # Return focus to initial position + if style == "raster": + next_z = initial_z + logging.debug("Returning to initial z position") + microscope.stage.move_abs( + [line[0][0], line[0][1], next_z] + ) # RWB: I think this line is redundant + + for x_y in line: + # Move to new grid position without changing z + logging.debug("Moving to step {}".format([x_y[0], x_y[1], next_z])) + microscope.stage.move_abs([x_y[0], x_y[1], next_z]) + # Refocus + if autofocus_enabled: + if fast_autofocus: + autofocus_extension.fast_autofocus( + dz=autofocus_dz, + target_z=-z_stack_dz / 2.0, # Finish below the focus + initial_move_up=False, # We're already at the top of the scan + ) + # TODO: save the focus data for future reference? Use it for diagnostics? + else: + logging.debug("Running autofocus") + autofocus_extension.autofocus( + range(-3 * autofocus_dz, 4 * autofocus_dz, autofocus_dz) + ) + logging.debug("Finished autofocus") + time.sleep(1) # TODO: Remove + + # If we're not doing a z-stack, just capture + if grid[2] <= 1: + capture( + microscope, + basename, + scan_id, + temporary=temporary, + use_video_port=use_video_port, + resize=resize, + bayer=bayer, + metadata=metadata, + tags=tags, + ) + # Update task progress + _images_captured_so_far += 1 + update_task_progress(progress()) + else: + logging.debug("Entering z-stack") + stack( + microscope=microscope, + basename=basename, + temporary=temporary, + scan_id=scan_id, + step_size=step_size[2], + steps=grid[2], + center=not fast_autofocus, # fast_autofocus does this for us! + return_to_start=not fast_autofocus, + use_video_port=use_video_port, + resize=resize, + bayer=bayer, + metadata=metadata, + tags=tags, + ) + # Make sure we use our current best estimate of focus (i.e. the current position) next point + next_z = microscope.stage.position[2] + if fast_autofocus: + next_z += ( + autofocus_dz / 2 + ) # Fast autofocus requires us to start at the top of the range + if grid[2] > 1: + next_z -= int( + grid[2] / 2.0 * step_size[2] + ) # Z stacking means we're higher up to start with + + logging.debug("Returning to {}".format(initial_position)) + microscope.stage.move_abs(initial_position) + + +def stack( + microscope, + basename: str = None, + temporary: bool = False, + scan_id: str = None, + step_size: int = 100, + steps: int = 5, + center: bool = True, + return_to_start: bool = True, + use_video_port: bool = False, + resize: Tuple[int, int] = None, + bayer: bool = False, + metadata: dict = {}, + tags: list = [], +): + global _images_captured_so_far + + # Generate a basename if none given + if not basename: + basename = generate_basename() + + # Generate a stack ID + if not scan_id: + scan_id = uuid.uuid4() + + # Add scan metadata + if not "time" in metadata: + metadata["time"] = generate_basename() + + # Store initial position + initial_position = microscope.stage.position + + with microscope.lock: + # Move to center scan + if center: + logging.debug("Moving to starting position") + microscope.stage.move_rel([0, 0, int((-step_size * steps) / 2)]) + + for i in range(steps): + time.sleep(0.1) + logging.debug("Capturing...") + capture( + microscope, + basename, + scan_id, + temporary=temporary, + use_video_port=use_video_port, + resize=resize, + bayer=bayer, + metadata=metadata, + tags=tags, + ) + # Update task progress + _images_captured_so_far += 1 + update_task_progress(progress()) + + if i != steps - 1: + logging.debug("Moving z by {}".format(step_size)) + microscope.stage.move_rel([0, 0, step_size]) + if return_to_start: + logging.debug("Returning to {}".format(initial_position)) + microscope.stage.move_abs(initial_position) + + +### Web views + + +@ThingAction +class TileScanAPI(View): + @use_args( + { + "filename": fields.String(), + "temporary": fields.Boolean(missing=False), + "step_size": fields.List(fields.Integer, missing=[2000, 1500, 100]), + "grid": fields.List(fields.Integer, missing=[3, 3, 5]), + "style": fields.String(missing="raster"), + "autofocus_dz": fields.Integer(missing=50), + "fast_autofocus": fields.Boolean(missing=False), + "use_video_port": fields.Boolean(missing=False), + "bayer": fields.Boolean(missing=False), + "metadata": fields.Dict(missing={}), + "tags": fields.List(fields.String, missing=[]), + "resize": fields.Dict(missing=None), # TODO: Validate keys + } + ) + @marshal_task + def post(self, args): + microscope = find_component("org.openflexure.microscope") + + if not microscope: + abort(503, "No microscope connected. Unable to autofocus.") + + resize = args.get("resize", None) + if resize: + if ("width" in resize) and ("height" in resize): + resize = ( + int(resize["width"]), + int(resize["height"]), + ) # Convert dict to tuple + else: + abort(404) + + logging.info("Running tile scan...") + task = taskify(tile)( + microscope, + basename=args.get("filename"), + temporary=args.get("temporary"), + step_size=args.get("step_size"), + grid=args.get("grid"), + style=args.get("style"), + autofocus_dz=args.get("autofocus_dz"), + use_video_port=args.get("use_video_port"), + resize=resize, + bayer=args.get("bayer"), + fast_autofocus=args.get("fast_autofocus"), + metadata=args.get("metadata"), + tags=args.get("tags"), + ) + + # return a handle on the scan task + return task + + +scan_extension_v2 = BaseExtension("org.openflexure.scan", version="2.0.0-beta.1") + +scan_extension_v2.add_view(TileScanAPI, "/tile") diff --git a/openflexure_microscope/api/default_extensions/zip_builder.py b/openflexure_microscope/api/default_extensions/zip_builder.py new file mode 100644 index 00000000..67bac694 --- /dev/null +++ b/openflexure_microscope/api/default_extensions/zip_builder.py @@ -0,0 +1,155 @@ +from openflexure_microscope.devel import ( + JsonResponse, + request, + jsonify, + taskify, + update_task_progress, +) + +from flask import send_file, abort + +import uuid +import os +import zipfile +import tempfile +import logging + +from openflexure_microscope.common.flask_labthings.find import find_component +from openflexure_microscope.common.flask_labthings.view import View +from openflexure_microscope.common.flask_labthings.extensions import BaseExtension +from openflexure_microscope.common.flask_labthings.decorators import ( + ThingAction, + ThingProperty, +) + + +class ZipManager: + """ + ZIP-builder manager + """ + + def __init__(self): + super().__init__() + + self.session_zips = {} + + def build_zip_from_capture_ids(self, microscope, capture_id_list): + logging.debug(capture_id_list) + + # Get array of captures from IDs + capture_list = [ + microscope.camera.image_from_id(capture_id) + for capture_id in capture_id_list + ] + # Remove Nones from list (missing/invalid captures) + capture_list = [capture for capture in capture_list if capture] + + # Get size (in bytes) of each capture + capture_sizes = [ + os.path.getsize(capture_obj.file) for capture_obj in capture_list + ] + # Calculate size of input data in megabytes + data_size_megabytes = sum(capture_sizes) * 1e-6 + + # If more than 1GB + if data_size_megabytes > 1000: + # Throw exception + raise Exception( + "Zip data cannot exceed 1GB. Please transfer data manually." + ) + + # Number of files to add (used for task progress) + n_files = len(capture_id_list) + + # Create temporary file + fp = tempfile.NamedTemporaryFile(delete=False) + + # Open temp file as a ZIP file + with zipfile.ZipFile(fp, "w") as zipObj: + for index, capture_obj in enumerate(capture_list): + # Add to ZIP file if it exists + file_path = capture_obj.file + rel_path = os.path.relpath( + file_path, microscope.camera.paths["default"] + ) + zipObj.write(file_path, arcname=rel_path) + # Update task progress + update_task_progress(int((index / n_files) * 100)) + + session_id = uuid.uuid4() + session_key = str(session_id) + # self.session_zips[session_id] = fp + self.session_zips[session_key] = { + "id": session_id, + "fp": fp, + "data_size": data_size_megabytes, + "zip_size": os.path.getsize(fp.name) * 1e-6, + } + + return self.session_zips[session_key] + + def zip_from_id(self, session_id): + return self.session_zips[session_id]["fp"] + + +# Create a global ZIP manager +default_zip_manager = ZipManager() + + +@ThingAction +class ZipBuilderAPIView(View): + def post(self): + + ids = list(JsonResponse(request).json) + microscope = find_component("org.openflexure.microscope") + + task = taskify(default_zip_manager.build_zip_from_capture_ids)(microscope, ids) + + # Return a handle on the autofocus task + return jsonify(task.state), 201 + + +@ThingProperty +class ZipListAPIView(View): + def get(self): + return jsonify(default_zip_manager.session_zips) + + +class ZipGetterAPIView(View): + def get(self, session_id): + if not session_id in default_zip_manager.session_zips: + return abort(404) # 404 Not Found + + logging.info(f"Session ID: {session_id}") + + return send_file( + default_zip_manager.zip_from_id(session_id).name, + mimetype="application/zip", + as_attachment=True, + attachment_filename=f"{session_id}.zip", + ) + + def delete(self, session_id): + if not session_id in default_zip_manager.session_zips: + return abort(404) # 404 Not Found + + logging.info(f"Session ID: {session_id}") + + fp = default_zip_manager.zip_from_id(session_id) + logging.debug(fp.name) + fp.close() + os.unlink(fp.name) + + assert not os.path.exists(fp.name) + + del default_zip_manager.session_zips[session_id] + + return jsonify({"return": session_id}) + + +zip_extension_v2 = BaseExtension("org.openflexure.zipbuilder", version="2.0.0-beta.1") + +zip_extension_v2.add_view(ZipGetterAPIView, "/get/") +zip_extension_v2.add_view(ZipListAPIView, "/get") + +zip_extension_v2.add_view(ZipBuilderAPIView, "/build") diff --git a/openflexure_microscope/api/example_extensions/ev_gui.py b/openflexure_microscope/api/example_extensions/ev_gui.py new file mode 100644 index 00000000..9a7c1552 --- /dev/null +++ b/openflexure_microscope/api/example_extensions/ev_gui.py @@ -0,0 +1,122 @@ +from openflexure_microscope.common.flask_labthings.view import View +from openflexure_microscope.common.flask_labthings.extensions import BaseExtension +from openflexure_microscope.common.flask_labthings.decorators import ( + ThingAction, + ThingProperty, +) + +from openflexure_microscope.api.utilities.gui import build_gui + +import logging + +from flask import jsonify + +# Some value that will change over time +# Here, we add 1 to it every time a GET request is made +val_int = 0 + + +def dynamic_form(): + global val_int + return { + "id": "test-plugin", + "icon": "pets", + "forms": [ + { + "name": "Simple request", + "isCollapsible": False, + "isTask": False, + "selfUpdate": True, + "route": "/do", + "submitLabel": "Do things", + "schema": [ + { + "fieldType": "numberInput", + "name": "val_int", + "label": "Number value", + "minValue": 0, + "value": val_int + }, + { + "fieldType": "htmlBlock", + "name": "html_block", + "content": f"Value is:
{val_int}.", # We dynamically use the val_int variable + }, + ], + } + ], + } + + +# Alternate form without any dynamic parts +static_form = { + "id": "test-plugin", + "icon": "pets", + "forms": [ + { + "name": "Simple request", + "isCollapsible": False, + "isTask": False, + "selfUpdate": True, + "route": "/do", + "submitLabel": "Do things", + "schema": [ + { + "fieldType": "numberInput", + "name": "val_int", + "label": "Number value", + "minValue": 0, + "default": 1 + }, + { + "fieldType": "htmlBlock", + "name": "html_block", + "content": f"Value is fixed.", + }, + ], + } + ], +} + + +@ThingProperty +class TestAPIView(View): + def get(self): + global val_int + val_int += 1 + return jsonify({"val": True}) + + +@ThingAction +class TestDoAPIView(View): + def post(self): + global val_int + val_int += 1 + return jsonify({"val": True}) + + +# Using the dynamic form +dynamic_test_extension_v2 = BaseExtension("org.openflexure.examples.dynamic-gui") +dynamic_test_extension_v2.add_view( + TestAPIView, "/get", endpoint="dynamic_test_extension_get" +) +dynamic_test_extension_v2.add_view( + TestDoAPIView, "/do", endpoint="dynamic_test_extension_do" +) + +dynamic_test_extension_v2.add_meta( + "gui", build_gui(dynamic_form, dynamic_test_extension_v2) +) + + +# Using the static form +static_test_extension_v2 = BaseExtension("org.openflexure.examples.static-gui") +static_test_extension_v2.add_view( + TestAPIView, "/get", endpoint="static_test_extension_get" +) +static_test_extension_v2.add_view( + TestDoAPIView, "/do", endpoint="static_test_extension_do" +) +static_test_extension_v2.add_meta( + "gui", build_gui(static_form, static_test_extension_v2) +) diff --git a/openflexure_microscope/api/microscope.py b/openflexure_microscope/api/microscope.py new file mode 100644 index 00000000..04053892 --- /dev/null +++ b/openflexure_microscope/api/microscope.py @@ -0,0 +1,44 @@ +from openflexure_microscope import Microscope +from openflexure_microscope.camera.capture import build_captures_from_exif + +import logging + +# Import device modules +# NB this will eventually be handled by the RC file, so you can choose what device +# class should be attached. +try: + from openflexure_microscope.camera.pi import PiCameraStreamer +except ImportError: + logging.warning("Unable to import PiCameraStreamer") +from openflexure_microscope.camera.mock import MockStreamer + +from openflexure_microscope.stage.sanga import SangaStage +from openflexure_microscope.stage.mock import MockStage + +default_microscope = Microscope() + +# Initialise camera +logging.debug("Creating camera object...") +try: + api_camera = PiCameraStreamer() +except Exception as e: + logging.error(e) + logging.warning("No valid camera hardware found. Falling back to mock camera!") + api_camera = MockStreamer() + +# Initialise stage +logging.debug("Creating stage object...") + +api_stage = MockStage() + +# Attach devices to microscope +logging.debug("Attaching devices to microscope...") +default_microscope.attach(api_camera, api_stage) + +# Restore loaded capture array to camera object +logging.debug("Restoring captures...") +default_microscope.camera.images = build_captures_from_exif( + default_microscope.camera.paths["default"] +) + +logging.debug("Microscope successfully attached!") diff --git a/openflexure_microscope/api/utilities.py b/openflexure_microscope/api/utilities/__init__.py similarity index 69% rename from openflexure_microscope/api/utilities.py rename to openflexure_microscope/api/utilities/__init__.py index 32a854e5..4b5a7cee 100644 --- a/openflexure_microscope/api/utilities.py +++ b/openflexure_microscope/api/utilities/__init__.py @@ -1,6 +1,14 @@ import logging +import os +import errno from werkzeug.exceptions import BadRequest -from flask import url_for, Blueprint +from flask import url_for, Blueprint, current_app + +from . import gui + + +def view_class_from_endpoint(endpoint: str): + return current_app.view_functions[endpoint].view_class def blueprint_for_module(module_name, api_ver=2, suffix=""): @@ -95,3 +103,32 @@ def list_routes(app): output[url] = line return output + + +def create_file(config_path): + if not os.path.exists(os.path.dirname(config_path)): + try: + os.makedirs(os.path.dirname(config_path)) + except OSError as exc: # Guard against race condition + if exc.errno != errno.EEXIST: + raise + + +def init_default_extensions(extension_dir): + global _DEFAULT_EXTENSION_INIT + os.makedirs(extension_dir, exist_ok=True) + + default_ext_path = os.path.join(extension_dir, "defaults.py") + + if not os.path.isfile(default_ext_path): # If user extensions file doesn't exist + logging.warning( + "No extension file found at {}. Creating...".format(extension_dir) + ) + create_file(default_ext_path) + + logging.info("Populating {}...".format(default_ext_path)) + with open(default_ext_path, "w") as outfile: + outfile.write(_DEFAULT_EXTENSION_INIT) + + +_DEFAULT_EXTENSION_INIT = "from openflexure_microscope.api.default_extensions import *" diff --git a/openflexure_microscope/api/utilities/gui.py b/openflexure_microscope/api/utilities/gui.py new file mode 100644 index 00000000..19d83bd8 --- /dev/null +++ b/openflexure_microscope/api/utilities/gui.py @@ -0,0 +1,48 @@ +import copy +import logging +from functools import wraps + + +def build_gui_from_dict(gui_description, extension_object): + # Make a working copy of GUI description + api_gui = copy.deepcopy(gui_description) + + # Expand shorthand routes into full relative URLs + if "forms" in gui_description and isinstance(api_gui["forms"], list): + for form in api_gui["forms"]: + if "route" in form and form["route"] in extension_object._rules.keys(): + form["route"] = extension_object._rules[form["route"]]["rule"] + else: + logging.warn( + "No valid expandable route found for {}".format(form["route"]) + ) + + # Inject extension information + api_gui["id"] = extension_object.name + api_gui["version"] = extension_object.version + return api_gui + + +def build_gui_from_func(func, extension_object): + @wraps(func) + def wrapped(*args, **kwargs): + return build_gui_from_dict(func(*args, **kwargs), extension_object) + + return wrapped + + +def build_gui(gui_description, extension_object): + # If given a function that generates a GUI dictionary + if callable(gui_description): + # Wrap in the route expander + return build_gui_from_func(gui_description, extension_object) + # If given a dictionary directly + elif isinstance(gui_description, dict): + # Build a GUI generator function + def gui_description_func(): + return gui_description + + # Wrap in the route expander + return build_gui_from_func(gui_description_func, extension_object) + else: + raise RuntimeError("GUI description must be a function or a dictionary") diff --git a/openflexure_microscope/api/v1/blueprints/__init__.py b/openflexure_microscope/api/v1/blueprints/__init__.py deleted file mode 100644 index eea7c36c..00000000 --- a/openflexure_microscope/api/v1/blueprints/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import camera, stage, base, plugins, task diff --git a/openflexure_microscope/api/v1/blueprints/base.py b/openflexure_microscope/api/v1/blueprints/base.py deleted file mode 100644 index cf3b014b..00000000 --- a/openflexure_microscope/api/v1/blueprints/base.py +++ /dev/null @@ -1,253 +0,0 @@ -from openflexure_microscope.api.utilities import gen, JsonResponse -from openflexure_microscope.api.views import MicroscopeView - -from flask import Response, Blueprint, jsonify, request -import logging - - -class StreamAPI(MicroscopeView): - def get(self): - """ - Real-time MJPEG stream from the microscope camera - - .. :quickref: State; Camera stream - - :>header Accept: image/jpeg - :>header Content-Type: image/jpeg - :status 200: stream active - """ - # Restart stream worker thread - self.microscope.camera.start_worker() - - return Response( - gen(self.microscope.camera), - mimetype="multipart/x-mixed-replace; boundary=frame", - ) - - -class SnapshotAPI(MicroscopeView): - def get(self): - """ - Single snapshot from the camera stream - - .. :quickref: State; Camera snapshot - - :>header Accept: image/jpeg - :>header Content-Type: image/jpeg - :status 200: stream active - """ - # Restart stream worker thread - self.microscope.camera.start_worker() - - return Response(self.microscope.camera.get_frame(), mimetype="image/jpeg") - - -class StateAPI(MicroscopeView): - def get(self): - """ - JSON representation of the microscope object. - - .. :quickref: State; Microscope state - - **Example request**: - - .. sourcecode:: http - - GET /state/ HTTP/1.1 - Accept: application/json - - **Example response**: - - .. sourcecode:: http - - HTTP/1.1 200 OK - Vary: Accept - Content-Type: application/json - - { - "camera": { - "preview_active": false, - "record_active": false, - "stream_active": true - }, - "plugin": {}, - "stage": { - "backlash": { - "x": 128, - "y": 128, - "z": 128 - }, - "position": { - "x": -8080, - "y": 5665, - "z": -12600 - } - } - } - - :>header Accept: application/json - :>header Content-Type: application/json - :status 200: state available - """ - return jsonify(self.microscope.state) - - -class ConfigAPI(MicroscopeView): - def get(self): - """ - JSON representation of the microscope config. - - .. :quickref: Config; Get microscope config - - **Example request**: - - .. sourcecode:: http - - GET /config/ HTTP/1.1 - Accept: application/json - - **Example response**: - - .. sourcecode:: http - - HTTP/1.1 200 OK - Vary: Accept - Content-Type: application/json - - { - "id": "0e2c6fac5421429aac67c7903107bdd8", - "name": "docuscope-2000", - "fov": [4100, 3146], - "camera_settings": { - "image_resolution": [2592, 1944], - "numpy_resolution": [1312, 976], - "video_resolution": [832, 624], - "jpeg_quality": 75, - "picamera_settings": { - "analog_gain": 1.0, - "digital_gain": 1.0, - "awb_gains": [0.92578125, 2.94921875], - "awb_mode": "off", - "exposure_mode": "off", - "framerate": 24.0, - "saturation": 0, - "shutter_speed": 5378 - }, - }, - "stage_settings": { - "backlash": { - "x": 256, - "y": 256, - "z": 0 - } - } - "plugins": [ - "openflexure_microscope.plugins.default.autofocus:AutofocusPlugin", - "openflexure_microscope.plugins.default.scan:ScanPlugin", - "openflexure_microscope.plugins.default.camera_calibration:Plugin" - ] - } - - :>header Accept: application/json - :>header Content-Type: application/json - :status 200: state available - """ - return jsonify(self.microscope.read_settings(json_safe=True)) - - def post(self): - """ - Modify microscope configuration - - .. :quickref: Config; Set microscope config - - **Example request**: - - .. sourcecode:: http - - POST /config HTTP/1.1 - Accept: application/json - - { - "id": "0e2c6fac5421429aac67c7903107bdd8", - "name": "docuscope-2000", - "fov": [4100, 3146], - "camera_settings": { - "image_resolution": [2592, 1944], - "numpy_resolution": [1312, 976], - "video_resolution": [832, 624], - "jpeg_quality": 75, - "picamera_settings": { - "analog_gain": 1.0, - "digital_gain": 1.0, - "awb_gains": [0.92578125, 2.94921875], - "awb_mode": "off", - "exposure_mode": "off", - "framerate": 24.0, - "saturation": 0, - "shutter_speed": 5378 - }, - }, - "stage_settings": { - "backlash": { - "x": 256, - "y": 256, - "z": 0 - } - } - "plugins": [ - "openflexure_microscope.plugins.default.autofocus:AutofocusPlugin", - "openflexure_microscope.plugins.default.scan:ScanPlugin", - "openflexure_microscope.plugins.default.camera_calibration:Plugin" - ] - } - - :>header Accept: application/json - - :/tags", - view_func=capture.TagsAPI.as_view("capture_tags", microscope=microscope_obj), - ) - - # Capture routes - blueprint.add_url_rule( - "/capture//download/", - view_func=capture.DownloadAPI.as_view( - "capture_download", microscope=microscope_obj - ), - ) - - blueprint.add_url_rule( - "/capture//download", - view_func=capture.DownloadRedirectAPI.as_view( - "capture_download_redirect", microscope=microscope_obj - ), - ) - - blueprint.add_url_rule( - "/capture//", - view_func=capture.CaptureAPI.as_view("capture", microscope=microscope_obj), - ) - - blueprint.add_url_rule( - "/capture/", - view_func=capture.ListAPI.as_view("capture_list", microscope=microscope_obj), - ) - - # Preview routes - blueprint.add_url_rule( - "/preview/", - view_func=preview.GPUPreviewAPI.as_view( - "gpu_preview", microscope=microscope_obj - ), - ) - - # Function routes - blueprint.add_url_rule( - "/overlay", - view_func=function.OverlayAPI.as_view("overlay", microscope=microscope_obj), - ) - - blueprint.add_url_rule( - "/zoom", view_func=function.ZoomAPI.as_view("zoom", microscope=microscope_obj) - ) - - return blueprint diff --git a/openflexure_microscope/api/v1/blueprints/camera/capture.py b/openflexure_microscope/api/v1/blueprints/camera/capture.py deleted file mode 100644 index d5e66dc5..00000000 --- a/openflexure_microscope/api/v1/blueprints/camera/capture.py +++ /dev/null @@ -1,459 +0,0 @@ -from openflexure_microscope.api.utilities import get_bool, JsonResponse -from openflexure_microscope.api.views import MicroscopeView -from openflexure_microscope.utilities import filter_dict - -from flask import jsonify, request, abort, url_for, redirect, send_file - - -class ListAPI(MicroscopeView): - def get(self): - """ - Get list of image captures. - - .. :quickref: Captures; Get collection of captures - - :>header Accept: application/json - :query include_unavailable: return json representations of captures that have been completely deleted - - :>jsonarr boolean available: availability of capture data - :>jsonarr string filename: filename of capture - :>jsonarr string id: unique id of the capture object - :>jsonarr boolean keep_on_disk: keep the capture file on microscope after closing - :>jsonarr boolean locked: file locked for modifications (mostly used for video recording) - :>jsonarr string path: path on pi storage to the capture file, if available - :>jsonarr boolean stream: capture stored in-memory as a BytesIO stream - :>jsonarr json uri: - **download** *(string)*: api uri to the capture file download - - **state** *(string)*: api uri to the capture json representation - - :>header Content-Type: application/json - :status 200: capture found - :status 404: no capture found with that id - """ - include_unavailable = get_bool(request.args.get("include_unavailable")) - - if include_unavailable: - captures = [image.state for image in self.microscope.camera.images] - else: - captures = [ - image.state - for image in self.microscope.camera.images - if image.state["available"] - ] - - return jsonify(captures) - - def delete(self): - """ - Delete all captures (not yet implemented) - - .. :quickref: Captures; Delete all captures - """ - for image in self.microscope.camera.images: - image.delete() - - captures = [image.state for image in self.microscope.camera.images] - - return jsonify(captures) - - def post(self): - """ - Create a new image capture. - - .. :quickref: Captures; New capture - - **Example request**: - - .. sourcecode:: http - - POST /camera/capture HTTP/1.1 - Accept: application/json - - { - "filename": "myfirstcapture", - "temporary": false, - "use_video_port": true, - "bayer": true, - "size": { - "width": 640, - "height": 480 - } - } - - :>header Accept: application/json - - :json boolean available: availability of capture data - :>json string filename: filename of capture - :>json string id: unique id of the capture object - :>json boolean temporary: delete the capture file on microscope after closing - :>json boolean locked: file locked for modifications (mostly used for video recording) - :>json string path: path on pi storage to the capture file, if available - :>json boolean stream: capture stored in-memory as a BytesIO stream - :>json json uri: - **download** *(string)*: api uri to the capture file download - - **state** *(string)*: api uri to the capture json representation - - :
json boolean available: availability of capture data - :>json string filename: filename of capture - :>json string id: unique id of the capture object - :>json boolean keep_on_disk: keep the capture file on microscope after closing - :>json boolean locked: file locked for modifications (mostly used for video recording) - :>json string path: path on pi storage to the capture file, if available - :>json boolean stream: capture stored in-memory as a BytesIO stream - :>json json uri: - **download** *(string)*: api uri to the capture file download - - **state** *(string)*: api uri to the capture json representation - - """ - capture_obj = self.microscope.camera.image_from_id(capture_id) - - if not capture_obj: - return abort(404) # 404 Not Found - - # Get capture state - capture_metadata = capture_obj.state - - # Add API routes to returned state - uri_dict = { - "uri": { - "state": "{}".format(url_for(".capture", capture_id=capture_obj.id)) - } - } - - # If available, also add download link - if capture_metadata["available"]: - uri_dict["uri"]["download"] = "{}download/{}".format( - url_for(".capture", capture_id=capture_obj.id), capture_obj.filename - ) - - capture_metadata.update(uri_dict) - - return jsonify(capture_metadata) - - def delete(self, capture_id): - """ - Delete all capture data from the Pi, even if `keep_on_disk=true;`. - - .. :quickref: Capture; Delete capture. - """ - capture_obj = self.microscope.camera.image_from_id(capture_id) - - if not capture_obj: - return abort(404) # 404 Not Found - - capture_obj.delete() - - return jsonify({"return": capture_id}) - - def put(self, capture_id): - """ - Add arbitrary metadata to the capture - - .. :quickref: Capture; Update capture metadata - - **Example request**: - - .. sourcecode:: http - - PUT /camera/capture/d0b2067abbb946f19351e075c5e7cd5b HTTP/1.1 - Accept: application/json - - { - "user_id": "ofm_1234", - "patient_number": 1452, - } - - :>header Accept: application/json - - :
header Accept: image/jpeg - :query thumbnail: return an image thumbnail e.g. ?thumbnail=true - - :>header Content-Type: image/jpeg - :status 200: capture data found - :status 404: no capture found with that id - - """ - capture_obj = self.microscope.camera.image_from_id(capture_id) - - if not capture_obj or not capture_obj.state["available"]: - return abort(404) # 404 Not Found - - thumbnail = get_bool(request.args.get("thumbnail")) - - return redirect( - url_for( - ".capture_download", - capture_id=capture_id, - filename=capture_obj.filename, - thumbnail=thumbnail, - ), - code=307, - ) - - -class DownloadAPI(MicroscopeView): - def get(self, capture_id, filename): - - capture_obj = self.microscope.camera.image_from_id(capture_id) - - if not capture_obj or not capture_obj.state["available"]: - return abort(404) # 404 Not Found - - thumbnail = get_bool(request.args.get("thumbnail")) - - # If no filename is specified, redirect to the capture's currently set filename - if not filename: - return redirect( - url_for( - "capture_download", - capture_id=capture_id, - filename=capture_obj.filename, - thumbnail=thumbnail, - ), - code=307, - ) - - # Download the image data using the requested filename - if thumbnail: - img = capture_obj.thumbnail - else: - img = capture_obj.data - - return send_file(img, mimetype="image/jpeg") - - -class TagsAPI(MicroscopeView): - def get(self, capture_id): - """ - Return tag list for a capture. - - .. :quickref: Capture; Show capture tags - - **Example request**: - - .. sourcecode:: http - - GET /camera/capture/d0b2067abbb946f19351e075c5e7cd5b/tags HTTP/1.1 - Accept: text/json - - :>header Accept: text/json - - :>header Content-Type: text/json - :status 200: capture data found - :status 404: no capture found with that id - """ - capture_obj = self.microscope.camera.image_from_id(capture_id) - - if not capture_obj or not capture_obj.state["available"]: - return abort(404) # 404 Not Found - - metadata_tags = filter_dict(capture_obj.state, ["metadata", "tags"]) - - return jsonify(metadata_tags) - - def put(self, capture_id): - """ - Add tags to the capture - - .. :quickref: Capture; Update capture tags - - **Example request**: - - .. sourcecode:: http - - PUT /camera/capture/d0b2067abbb946f19351e075c5e7cd5b/tags HTTP/1.1 - Accept: application/json - - ["tests", "mytag", "someothertag"] - - :>header Accept: application/json - - :
header Accept: application/json - - :
header Accept: application/json - - :
header Accept: application/json - - :
header Accept: application/json - - :
header Accept: application/json - - :
header Accept: application/json - - :
header Accept: application/json - :query include_unavailable: return json representations of captures that have been completely deleted - - :>header Content-Type: application/json - """ - - data = tasks.states() - - return jsonify(data) - - def delete(self): - """ - Clean list of long-running tasks (running tasks persist). - - .. :quickref: Tasks; Clean collection of tasks - - :>header Accept: application/json - - :>header Content-Type: application/json - """ - - tasks.cleanup_tasks() - data = tasks.states() - - return jsonify(data) - - -class TaskAPI(MethodView): - def get(self, task_id): - """ - Get JSON representation of a task - - .. :quickref: Tasks; Get task - - **Example request**: - - .. sourcecode:: http - - GET /task/db13a66787e1419bb06b1504e4d80b0c/ HTTP/1.1 - Accept: application/json - - **Example response**: - - .. sourcecode:: http - - HTTP/1.1 200 OK - Vary: Accept - Content-Type: application/json - - { - "end_time": "2019-01-23 16-33-33", - "id": "db13a66787e1419bb06b1504e4d80b0c", - "return": [ - 0.848622546386467, - 0.6106785018091292, - ], - "start_time": "2019-01-23 16-33-13", - "status": "success" - } - - """ - - try: - task = tasks.tasks()[task_id] - except KeyError: - return abort(404) # 404 Not Found - - # Return task state - return jsonify(task.state) - - def delete(self, task_id): - """ - Terminate a particular task. - - .. :quickref: Tasks; Terminate a task - - :>header Accept: application/json - - :>header Content-Type: application/json - """ - - try: - task = tasks.tasks()[task_id] - except KeyError: - return abort(404) # 404 Not Found - - task.terminate() - - return jsonify(task.state) - - -def construct_blueprint(microscope_obj): - - blueprint = Blueprint("task_blueprint", __name__) - - blueprint.add_url_rule("/", view_func=TaskListAPI.as_view("task_list")) - - blueprint.add_url_rule("//", view_func=TaskAPI.as_view("task")) - - return blueprint diff --git a/openflexure_microscope/api/v2/__init__.py b/openflexure_microscope/api/v2/__init__.py index 3f07e575..e69de29b 100644 --- a/openflexure_microscope/api/v2/__init__.py +++ b/openflexure_microscope/api/v2/__init__.py @@ -1 +0,0 @@ -from . import blueprints diff --git a/openflexure_microscope/api/v2/blueprints/__init__.py b/openflexure_microscope/api/v2/blueprints/__init__.py deleted file mode 100644 index 0ce1ebae..00000000 --- a/openflexure_microscope/api/v2/blueprints/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import root, captures, settings, status, tasks, streams, plugins, actions diff --git a/openflexure_microscope/api/v2/blueprints/actions/__init__.py b/openflexure_microscope/api/v2/blueprints/actions/__init__.py deleted file mode 100644 index e57c2c04..00000000 --- a/openflexure_microscope/api/v2/blueprints/actions/__init__.py +++ /dev/null @@ -1,98 +0,0 @@ -""" -Top-level representation of enabled actions -""" - -from flask import Blueprint, url_for, jsonify - -from openflexure_microscope.api.utilities import blueprint_for_module -from openflexure_microscope.utilities import get_docstring, description_from_view -from openflexure_microscope.api.views import MicroscopeView - -from . import camera, stage, system - - -class ActionsAPI(MicroscopeView): - def get(self): - return jsonify(actions_representation()) - - -_actions = { - "capture": { - "rule": "/camera/capture/", - "view_class": camera.CaptureAPI, - "conditions": True, - }, - "previewStart": { - "rule": "/camera/preview/start", - "view_class": camera.GPUPreviewStartAPI, - "conditions": True, - }, - "previewStop": { - "rule": "/camera/preview/stop", - "view_class": camera.GPUPreviewStopAPI, - "conditions": True, - }, - "move": { - "rule": "/stage/move/", - "view_class": stage.MoveStageAPI, - "conditions": True, - }, - "zeroStage": { - "rule": "/stage/zero/", - "view_class": stage.ZeroStageAPI, - "conditions": True, - }, - "shutdown": { - "rule": "/system/shutdown/", - "view_class": system.ShutdownAPI, - "conditions": system.is_raspberrypi(), - }, - "reboot": { - "rule": "/system/reboot/", - "view_class": system.RebootAPI, - "conditions": system.is_raspberrypi(), - }, -} - - -def enabled_actions(): - global _actions - return {k: v for k, v in _actions.items() if v["conditions"]} - - -def actions_representation(): - global _actions - - actions = {} - for name, action in enabled_actions().items(): - d = { - "links": {"self": url_for(f".{name}")}, - "rule": action["rule"], - "view_class": str(action["view_class"]), - } - - d.update(description_from_view(action["view_class"])) - - actions[name] = d - - return actions - - -def construct_blueprint(microscope_obj): - global _actions - - blueprint = blueprint_for_module(__name__) - - # For each enabled action route defined in our dictionary above - for name, action in enabled_actions().items(): - # Add the action to our blueprint - blueprint.add_url_rule( - action["rule"], - view_func=action["view_class"].as_view(name, microscope=microscope_obj), - ) - - blueprint.add_url_rule( - "/", view_func=ActionsAPI.as_view("actions", microscope=microscope_obj) - ) - - return blueprint diff --git a/openflexure_microscope/api/v2/blueprints/actions/camera.py b/openflexure_microscope/api/v2/blueprints/actions/camera.py deleted file mode 100644 index 3b899102..00000000 --- a/openflexure_microscope/api/v2/blueprints/actions/camera.py +++ /dev/null @@ -1,172 +0,0 @@ -from openflexure_microscope.api.utilities import get_bool, JsonResponse -from openflexure_microscope.api.views import MicroscopeView -from openflexure_microscope.utilities import filter_dict - -import logging -from flask import jsonify, request, abort, url_for, redirect, send_file - - -class CaptureAPI(MicroscopeView): - """ - Create a new image capture. - """ - - def post(self): - """ - Create a new image capture. - - .. :quickref: Actions; New capture - - **Example request**: - - .. sourcecode:: http - - POST /actions/camera/capture HTTP/1.1 - Accept: application/json - - { - "filename": "myfirstcapture", - "temporary": false, - "use_video_port": true, - "bayer": true, - "size": { - "width": 640, - "height": 480 - } - } - - :>header Accept: application/json - - :json boolean available: availability of capture data - :>json string filename: filename of capture - :>json string id: unique id of the capture object - :>json boolean temporary: delete the capture file on microscope after closing - :>json boolean locked: file locked for modifications (mostly used for video recording) - :>json string path: path on pi storage to the capture file, if available - :>json boolean stream: capture stored in-memory as a BytesIO stream - :>json json uri: - **download** *(string)*: api uri to the capture file download - - **state** *(string)*: api uri to the capture json representation - - :
header Accept: application/json - - :
header Accept: application/json - - :
header Accept: application/json - :query include_unavailable: return json representations of captures that have been completely deleted - - :>jsonarr boolean available: availability of capture data - :>jsonarr string filename: filename of capture - :>jsonarr string id: unique id of the capture object - :>jsonarr boolean keep_on_disk: keep the capture file on microscope after closing - :>jsonarr boolean locked: file locked for modifications (mostly used for video recording) - :>jsonarr string path: path on pi storage to the capture file, if available - :>jsonarr boolean stream: capture stored in-memory as a BytesIO stream - :>jsonarr json uri: - **download** *(string)*: api uri to the capture file download - - **state** *(string)*: api uri to the capture json representation - - :>header Content-Type: application/json - :status 200: capture found - :status 404: no capture found with that id - """ - representation = captures_representation(self.microscope.camera.images) - - return jsonify(representation) - - def delete(self): - """ - Delete all captures - - .. :quickref: Captures; Delete all captures - """ - for image in self.microscope.camera.images: - image.delete() - - captures = [image.state for image in self.microscope.camera.images] - - return jsonify(captures) - - -class CaptureAPI(MicroscopeView): - def get(self, capture_id): - """ - Get JSON representation of a capture - - .. :quickref: Capture; Get capture - - **Example request**: - - .. sourcecode:: http - - GET /camera/capture/d0b2067abbb946f19351e075c5e7cd5b/ HTTP/1.1 - Accept: application/json - - **Example response**: - - .. sourcecode:: http - - HTTP/1.1 200 OK - Vary: Accept - Content-Type: application/json - - { - "available": true, - "bytestream": false, - "keep_on_disk": false, - "locked": false, - "metadata": { - "filename": "2018-11-16_10-21-53.jpeg", - "id": "d0b2067abbb946f19351e075c5e7cd5b", - "path": "capture/2018-11-16_10-21-53.jpeg", - "time": "2018-11-16_10-21-53" - } - "uri": { - "download": "/api/v1/capture/d0b2067abbb946f19351e075c5e7cd5b/download", - "state": "/api/v1/capture/d0b2067abbb946f19351e075c5e7cd5b/" - } - } - - :>json boolean available: availability of capture data - :>json string filename: filename of capture - :>json string id: unique id of the capture object - :>json boolean keep_on_disk: keep the capture file on microscope after closing - :>json boolean locked: file locked for modifications (mostly used for video recording) - :>json string path: path on pi storage to the capture file, if available - :>json boolean stream: capture stored in-memory as a BytesIO stream - :>json json uri: - **download** *(string)*: api uri to the capture file download - - **state** *(string)*: api uri to the capture json representation - - """ - - try: - representation = captures_representation(self.microscope.camera.images)[ - capture_id - ] - except KeyError: - return abort(404) # 404 Not Found - - return jsonify(representation) - - def delete(self, capture_id): - """ - Delete all capture data from the Pi, even if `keep_on_disk=true;`. - - .. :quickref: Capture; Delete capture. - """ - capture_obj = self.microscope.camera.image_from_id(capture_id) - - if not capture_obj: - return abort(404) # 404 Not Found - - capture_obj.delete() - - return jsonify({"return": capture_id}) - - def put(self, capture_id): - """ - Add arbitrary metadata to the capture - - .. :quickref: Capture; Update capture metadata - - **Example request**: - - .. sourcecode:: http - - PUT /camera/capture/d0b2067abbb946f19351e075c5e7cd5b HTTP/1.1 - Accept: application/json - - { - "user_id": "ofm_1234", - "patient_number": 1452, - } - - :>header Accept: application/json - - :
header Accept: image/jpeg - :query thumbnail: return an image thumbnail e.g. ?thumbnail=true - - :>header Content-Type: image/jpeg - :status 200: capture data found - :status 404: no capture found with that id - - """ - capture_obj = self.microscope.camera.image_from_id(capture_id) - - if not capture_obj or not capture_obj.state["available"]: - return abort(404) # 404 Not Found - - thumbnail = get_bool(request.args.get("thumbnail")) - - return redirect( - url_for( - ".capture_download", - capture_id=capture_id, - filename=capture_obj.filename, - thumbnail=thumbnail, - ), - code=307, - ) - - -class DownloadAPI(MicroscopeView): - def get(self, capture_id, filename): - - capture_obj = self.microscope.camera.image_from_id(capture_id) - - if not capture_obj or not capture_obj.state["available"]: - return abort(404) # 404 Not Found - - thumbnail = get_bool(request.args.get("thumbnail")) - - # If no filename is specified, redirect to the capture's currently set filename - if not filename: - return redirect( - url_for( - "capture_download", - capture_id=capture_id, - filename=capture_obj.filename, - thumbnail=thumbnail, - ), - code=307, - ) - - # Download the image data using the requested filename - if thumbnail: - img = capture_obj.thumbnail - else: - img = capture_obj.data - - return send_file(img, mimetype="image/jpeg") - - -class TagsAPI(MicroscopeView): - def get(self, capture_id): - """ - Return tag list for a capture. - - .. :quickref: Capture; Show capture tags - - **Example request**: - - .. sourcecode:: http - - GET /camera/capture/d0b2067abbb946f19351e075c5e7cd5b/tags HTTP/1.1 - Accept: text/json - - :>header Accept: text/json - - :>header Content-Type: text/json - :status 200: capture data found - :status 404: no capture found with that id - """ - capture_obj = self.microscope.camera.image_from_id(capture_id) - - if not capture_obj or not capture_obj.state["available"]: - return abort(404) # 404 Not Found - - return jsonify(capture_obj.tags) - - def put(self, capture_id): - """ - Add tags to the capture - - .. :quickref: Capture; Update capture tags - - **Example request**: - - .. sourcecode:: http - - PUT /camera/capture/d0b2067abbb946f19351e075c5e7cd5b/tags HTTP/1.1 - Accept: application/json - - ["tests", "mytag", "someothertag"] - - :>header Accept: application/json - - :
header Accept: application/json - - :
/tags", - view_func=TagsAPI.as_view("capture_tags", microscope=microscope_obj), - ) - - # Capture routes - blueprint.add_url_rule( - "//download/", - view_func=DownloadAPI.as_view("capture_download", microscope=microscope_obj), - ) - - blueprint.add_url_rule( - "//download", - view_func=DownloadRedirectAPI.as_view( - "capture_download_redirect", microscope=microscope_obj - ), - ) - - blueprint.add_url_rule( - "//", - view_func=CaptureAPI.as_view("capture", microscope=microscope_obj), - ) - - blueprint.add_url_rule( - "/", view_func=ListAPI.as_view("captures", microscope=microscope_obj) - ) - return blueprint diff --git a/openflexure_microscope/api/v2/blueprints/plugins.py b/openflexure_microscope/api/v2/blueprints/plugins.py deleted file mode 100644 index f12ef19a..00000000 --- a/openflexure_microscope/api/v2/blueprints/plugins.py +++ /dev/null @@ -1,97 +0,0 @@ -""" -Top-level representation of attached and enabled plugins -""" - -from openflexure_microscope.plugins import PluginLoader, MicroscopePlugin -from openflexure_microscope.api.views import MicroscopeViewPlugin -from openflexure_microscope.utilities import get_docstring, description_from_view -from openflexure_microscope.api.utilities import blueprint_for_module - -from flask import Blueprint, jsonify, url_for -from openflexure_microscope.api.views import MicroscopeView - -import copy -import logging -import warnings - - -def plugins_representation(plugin_loader_object: PluginLoader): - """ - Generate a dictionary representation of all plugins, including Flask route URLs - - Args: - plugin_loader_object (:py:class:`openflexure_microscope.plugins.PluginLoader`): Microscope plugin loader - - Returns: - dict: Dictionary representation of all plugins - """ - plugins = {} - - for plugin in plugin_loader_object.active: - logging.debug(f"Representing plugin {plugin._name}") - d = { - "python_name": plugin._name_python_safe, - "plugin": str(plugin), - "views": {}, - "gui": plugin.gui, - "description": get_docstring(plugin), - } - - for view_id, view_data in plugin.views.items(): - logging.debug(f"Representing view {view_id}") - uri = url_for(f"v2_plugins_blueprint.plugins") + view_data["rule"][1:] - # uri = view_data["rule"] - # Make links dictionary if it doesn't yet exist - view_d = {"links": {"self": uri}} - - view_d.update(description_from_view(view_data["view"])) - - d["views"][view_id] = view_d - - plugins[plugin._name] = d - - return plugins - - -class PluginFormAPI(MicroscopeView): - def get(self): - """ - Return the current plugin forms - - .. :quickref: Plugin; Get forms - - Returns an array of present plugin forms (describing plugin user interfaces.) - Please note, this is *not* a list of all enabled plugins, only those with associated - user interface forms. - - A complete list of enabled plugins can be found in the microscope state. - - """ - return jsonify(plugins_representation(self.microscope.plugins)) - - -def construct_blueprint(microscope_obj): - - blueprint = blueprint_for_module(__name__) - - # Create a base route to return plugin API forms, if any exist - blueprint.add_url_rule( - "/", view_func=PluginFormAPI.as_view("plugins", microscope=microscope_obj) - ) - - # For each plugin attached to the microscope object - for plugin in microscope_obj.plugins.active: - - for plugin_view_id, plugin_view in plugin.views.items(): - # Add route to the plugins blueprint - blueprint.add_url_rule( - plugin_view["rule"], - view_func=plugin_view["view"].as_view( - f"{plugin._name_python_safe}_{plugin_view_id}", - microscope=microscope_obj, - plugin=plugin, - ), - **plugin_view["kwargs"], - ) - - return blueprint diff --git a/openflexure_microscope/api/v2/blueprints/root.py b/openflexure_microscope/api/v2/blueprints/root.py deleted file mode 100644 index bdaf348b..00000000 --- a/openflexure_microscope/api/v2/blueprints/root.py +++ /dev/null @@ -1,52 +0,0 @@ -from flask import Blueprint, jsonify, url_for -from openflexure_microscope import Microscope -from openflexure_microscope.api.views import MicroscopeView - -from openflexure_microscope.utilities import get_docstring, bottom_level_name -from openflexure_microscope.api.utilities import blueprint_name_for_module -from openflexure_microscope.api.v2.blueprints import ( - settings, - status, - plugins, - captures, - actions, - streams, -) - -# List of submodules containing create_blueprint methods using standard blueprint_for_module naming -_root_blueprint_modules = [settings, status, plugins, captures, actions, streams] - - -def root_representation(): - """ - Generate a dictionar representation of all top-level blueprint rules - """ - global _root_blueprint_modules - d = {} - - for blueprint_module in _root_blueprint_modules: - module_short_name = bottom_level_name(blueprint_module) - blueprint_name = blueprint_name_for_module(blueprint_module.__name__) - - d[module_short_name] = { - "name": blueprint_module.__name__, - "description": get_docstring(blueprint_module), - "links": {"self": url_for(f"{blueprint_name}.{module_short_name}")}, - } - - return d - - -class RootAPI(MicroscopeView): - def get(self): - - return jsonify(root_representation()) - - -def construct_blueprint(microscope_obj): - blueprint = Blueprint("root_blueprint", __name__) - - blueprint.add_url_rule( - "/", view_func=RootAPI.as_view("root_repr", microscope=microscope_obj) - ) - return blueprint diff --git a/openflexure_microscope/api/v2/blueprints/settings.py b/openflexure_microscope/api/v2/blueprints/settings.py deleted file mode 100644 index e56e5b36..00000000 --- a/openflexure_microscope/api/v2/blueprints/settings.py +++ /dev/null @@ -1,198 +0,0 @@ -""" -Writeable settings for the microscope, and attached hardware -""" - -from openflexure_microscope.api.utilities import gen, JsonResponse -from openflexure_microscope.api.views import MicroscopeView -from openflexure_microscope.api.utilities import blueprint_for_module - -from openflexure_microscope.utilities import get_by_path, set_by_path, create_from_path - -from flask import Blueprint, jsonify, request, abort -import logging - - -class SettingsAPI(MicroscopeView): - def get(self): - """ - JSON representation of the microscope settings. - - .. :quickref: Settings; Get microscope settings - - **Example request**: - - .. sourcecode:: http - - GET /settings/ HTTP/1.1 - Accept: application/json - - **Example response**: - - .. sourcecode:: http - - HTTP/1.1 200 OK - Vary: Accept - Content-Type: application/json - - { - "id": "0e2c6fac5421429aac67c7903107bdd8", - "name": "docuscope-2000", - "fov": [4100, 3146], - "camera_settings": { - "image_resolution": [2592, 1944], - "numpy_resolution": [1312, 976], - "video_resolution": [832, 624], - "jpeg_quality": 75, - "picamera_settings": { - "analog_gain": 1.0, - "digital_gain": 1.0, - "awb_gains": [0.92578125, 2.94921875], - "awb_mode": "off", - "exposure_mode": "off", - "framerate": 24.0, - "saturation": 0, - "shutter_speed": 5378 - }, - }, - "stage_settings": { - "backlash": { - "x": 256, - "y": 256, - "z": 0 - } - } - "plugins": [ - "openflexure_microscope.plugins.default.autofocus:AutofocusPlugin", - "openflexure_microscope.plugins.default.scan:ScanPlugin", - "openflexure_microscope.plugins.default.camera_calibration:Plugin" - ] - } - - :>header Accept: application/json - :>header Content-Type: application/json - :status 200: state available - """ - return jsonify(self.microscope.read_settings()) - - def put(self): - """ - Modify microscope configuration - - .. :quickref: Settings; Update microscope settings - - **Example request**: - - .. sourcecode:: http - - PUT /config HTTP/1.1 - Accept: application/json - - { - "id": "0e2c6fac5421429aac67c7903107bdd8", - "name": "docuscope-2000", - "fov": [4100, 3146], - "camera_settings": { - "image_resolution": [2592, 1944], - "numpy_resolution": [1312, 976], - "video_resolution": [832, 624], - "jpeg_quality": 75, - "picamera_settings": { - "analog_gain": 1.0, - "digital_gain": 1.0, - "awb_gains": [0.92578125, 2.94921875], - "awb_mode": "off", - "exposure_mode": "off", - "framerate": 24.0, - "saturation": 0, - "shutter_speed": 5378 - }, - }, - "stage_settings": { - "backlash": { - "x": 256, - "y": 256, - "z": 0 - } - } - "plugins": [ - "openflexure_microscope.plugins.default.autofocus:AutofocusPlugin", - "openflexure_microscope.plugins.default.scan:ScanPlugin", - "openflexure_microscope.plugins.default.camera_calibration:Plugin" - ] - } - - :>header Accept: application/json - - :", - view_func=NestedSettingsAPI.as_view( - "nested_settings", microscope=microscope_obj - ), - ) - - return blueprint diff --git a/openflexure_microscope/api/v2/blueprints/status.py b/openflexure_microscope/api/v2/blueprints/status.py deleted file mode 100644 index 846a594f..00000000 --- a/openflexure_microscope/api/v2/blueprints/status.py +++ /dev/null @@ -1,88 +0,0 @@ -""" -Read-only status of the microscope, and attached hardware info -""" - -from openflexure_microscope.api.views import MicroscopeView -from openflexure_microscope.api.utilities import blueprint_for_module -from openflexure_microscope.utilities import get_by_path - -from flask import Blueprint, jsonify, abort - - -class StatusAPI(MicroscopeView): - def get(self): - """ - JSON representation of the microscope status (read-only properties, modifiable with actions) - - .. :quickref: Status; Microscope status - - **Example request**: - - .. sourcecode:: http - - GET /status/ HTTP/1.1 - Accept: application/json - - **Example response**: - - .. sourcecode:: http - - HTTP/1.1 200 OK - Vary: Accept - Content-Type: application/json - - { - "camera": { - "preview_active": false, - "record_active": false, - "stream_active": true - }, - "plugin": {}, - "stage": { - "backlash": { - "x": 128, - "y": 128, - "z": 128 - }, - "position": { - "x": -8080, - "y": 5665, - "z": -12600 - } - } - } - - :>header Accept: application/json - :>header Content-Type: application/json - :status 200: state available - """ - return jsonify(self.microscope.status) - - -class NestedStatusAPI(MicroscopeView): - def get(self, route): - - keys = route.split("/") - - try: - value = get_by_path(self.microscope.status, keys) - except KeyError: - return abort(404) - - return jsonify(value) - - -def construct_blueprint(microscope_obj): - - blueprint = blueprint_for_module(__name__) - - blueprint.add_url_rule( - "/", view_func=StatusAPI.as_view("status", microscope=microscope_obj) - ) - - blueprint.add_url_rule( - "/", - view_func=NestedStatusAPI.as_view("nested_status", microscope=microscope_obj), - ) - - return blueprint diff --git a/openflexure_microscope/api/v2/blueprints/streams.py b/openflexure_microscope/api/v2/blueprints/streams.py deleted file mode 100644 index bc193d9a..00000000 --- a/openflexure_microscope/api/v2/blueprints/streams.py +++ /dev/null @@ -1,107 +0,0 @@ -""" -Top-level description of routes related to live camera stream data -""" - -from openflexure_microscope.api.utilities import gen, JsonResponse -from openflexure_microscope.api.views import MicroscopeView -from openflexure_microscope.api.utilities import ( - blueprint_for_module, - blueprint_name_for_module, -) -from openflexure_microscope.utilities import description_from_view - -from flask import Response, Blueprint, jsonify, request, url_for - - -class StreamAPI(MicroscopeView): - def get(self): - return jsonify(streams_representation()) - - -class MjpegAPI(MicroscopeView): - """ - Real-time MJPEG stream from the microscope camera - """ - - def get(self): - """ - Real-time MJPEG stream from the microscope camera - - .. :quickref: Streams; Camera MJPEG stream - - :>header Accept: image/jpeg - :>header Content-Type: image/jpeg - :status 200: stream active - """ - # Restart stream worker thread - self.microscope.camera.start_worker() - - return Response( - gen(self.microscope.camera), - mimetype="multipart/x-mixed-replace; boundary=frame", - ) - - -class SnapshotAPI(MicroscopeView): - """ - Single JPEG snapshot from the camera stream - """ - - def get(self): - """ - Single snapshot from the camera stream - - .. :quickref: Streams; Camera snapshot - - :>header Accept: image/jpeg - :>header Content-Type: image/jpeg - :status 200: stream active - """ - # Restart stream worker thread - self.microscope.camera.start_worker() - - return Response(self.microscope.camera.get_frame(), mimetype="image/jpeg") - - -_streams = { - "mjpeg": {"rule": "/mjpeg", "view_class": MjpegAPI, "conditions": True}, - "snapshot": {"rule": "/snapshot", "view_class": SnapshotAPI, "conditions": True}, -} - - -def enabled_streams(): - global _streams - return {k: v for k, v in _streams.items() if v["conditions"]} - - -def streams_representation(): - global _streams - - streams = {} - for name, stream in enabled_streams().items(): - d = {"links": {"self": url_for(f".{name}")}} - - d.update(description_from_view(stream["view_class"])) - - streams[name] = d - - return streams - - -def construct_blueprint(microscope_obj): - - blueprint = blueprint_for_module(__name__) - - # For each enabled stream route defined in our dictionary above - for name, stream in enabled_streams().items(): - # Add the action to our blueprint - blueprint.add_url_rule( - stream["rule"], - view_func=stream["view_class"].as_view(name, microscope=microscope_obj), - ) - - blueprint.add_url_rule( - "/", view_func=StreamAPI.as_view("streams", microscope=microscope_obj) - ) - - return blueprint diff --git a/openflexure_microscope/api/v2/blueprints/tasks.py b/openflexure_microscope/api/v2/blueprints/tasks.py deleted file mode 100644 index 877269ff..00000000 --- a/openflexure_microscope/api/v2/blueprints/tasks.py +++ /dev/null @@ -1,158 +0,0 @@ -from openflexure_microscope.api.views import MethodView -from flask import jsonify, abort, Blueprint, url_for - -from openflexure_microscope.common import tasks - - -def tasks_representation(): - """ - Generate a dictionary representation of all tasks, including Flask route URLs - - Returns: - dict: Dictionary representation of all tasks - """ - tasks_dict = tasks.states() - - for task_key, task_repr in tasks_dict.items(): - # Add API routes to returned representations - extra_state = { - "links": {"self": "{}".format(url_for(".task", task_id=task_key))} - } - - tasks_dict[task_key].update(extra_state) - - return tasks_dict - - -class TaskListAPI(MethodView): - def get(self): - """ - Get list of long-running tasks. - - .. :quickref: Tasks; Get collection of tasks - - **Example response**: - - .. sourcecode:: http - - HTTP/1.1 200 OK - Vary: Accept - Content-Type: application/json - - [ - { - "end_time": "2019-01-23 16-33-33", - "id": "db13a66787e1419bb06b1504e4d80b0c", - "return": [ - 0.848622546386467, - 0.6106785018091292, - ], - "start_time": "2019-01-23 16-33-13", - "status": "success" - }, - { - "end_time": null, - "id": "df46558cc8844924821bd0181881871e", - "return": null, - "start_time": "2019-01-23 16-34-54", - "status": "running" - } - ] - - :>header Accept: application/json - :query include_unavailable: return json representations of captures that have been completely deleted - - :>header Content-Type: application/json - """ - - return jsonify(tasks_representation()) - - def delete(self): - """ - Clean list of long-running tasks (running tasks persist). - - .. :quickref: Tasks; Clean collection of tasks - - :>header Accept: application/json - - :>header Content-Type: application/json - """ - - tasks.cleanup_tasks() - - return jsonify(tasks_representation()) - - -class TaskAPI(MethodView): - def get(self, task_id): - """ - Get JSON representation of a task - - .. :quickref: Tasks; Get task - - **Example request**: - - .. sourcecode:: http - - GET /task/db13a66787e1419bb06b1504e4d80b0c/ HTTP/1.1 - Accept: application/json - - **Example response**: - - .. sourcecode:: http - - HTTP/1.1 200 OK - Vary: Accept - Content-Type: application/json - - { - "end_time": "2019-01-23 16-33-33", - "id": "db13a66787e1419bb06b1504e4d80b0c", - "return": [ - 0.848622546386467, - 0.6106785018091292, - ], - "start_time": "2019-01-23 16-33-13", - "status": "success" - } - - """ - - try: - task = tasks_representation()[task_id] - except KeyError: - return abort(404) # 404 Not Found - - # Return task state - return jsonify(task) - - def delete(self, task_id): - """ - Terminate a particular task. - - .. :quickref: Tasks; Terminate a task - - :>header Accept: application/json - - :>header Content-Type: application/json - """ - - try: - task = tasks_representation()[task_id] - except KeyError: - return abort(404) # 404 Not Found - - task.terminate() - - return jsonify(task.state) - - -def construct_blueprint(microscope_obj): - - blueprint = Blueprint("v2_tasks_blueprint", __name__) - - blueprint.add_url_rule("/", view_func=TaskListAPI.as_view("task_list")) - - blueprint.add_url_rule("//", view_func=TaskAPI.as_view("task")) - - return blueprint diff --git a/openflexure_microscope/api/v2/views/__init__.py b/openflexure_microscope/api/v2/views/__init__.py new file mode 100644 index 00000000..a998c703 --- /dev/null +++ b/openflexure_microscope/api/v2/views/__init__.py @@ -0,0 +1,4 @@ +from .actions import enabled_root_actions +from .captures import * +from .state import * +from .streams import * diff --git a/openflexure_microscope/api/v2/views/actions/__init__.py b/openflexure_microscope/api/v2/views/actions/__init__.py new file mode 100644 index 00000000..076548a5 --- /dev/null +++ b/openflexure_microscope/api/v2/views/actions/__init__.py @@ -0,0 +1,83 @@ +""" +Top-level representation of enabled actions +""" + +from . import camera, stage, system + +from openflexure_microscope.common.flask_labthings.view import View +from openflexure_microscope.common.flask_labthings.find import current_labthing +from openflexure_microscope.common.flask_labthings.utilities import description_from_view +from openflexure_microscope.common.flask_labthings.decorators import Tag + +_actions = { + "capture": { + "rule": "/camera/capture/", + "view_class": camera.CaptureAPI, + "conditions": True, + }, + "previewStart": { + "rule": "/camera/preview/start", + "view_class": camera.GPUPreviewStartAPI, + "conditions": True, + }, + "previewStop": { + "rule": "/camera/preview/stop", + "view_class": camera.GPUPreviewStopAPI, + "conditions": True, + }, + "move": { + "rule": "/stage/move/", + "view_class": stage.MoveStageAPI, + "conditions": True, + }, + "zeroStage": { + "rule": "/stage/zero/", + "view_class": stage.ZeroStageAPI, + "conditions": True, + }, + "shutdown": { + "rule": "/system/shutdown/", + "view_class": system.ShutdownAPI, + "conditions": system.is_raspberrypi(), + }, + "reboot": { + "rule": "/system/reboot/", + "view_class": system.RebootAPI, + "conditions": system.is_raspberrypi(), + }, +} + + +def enabled_root_actions(): + global _actions + return {k: v for k, v in _actions.items() if v["conditions"]} + + +@Tag("actions") +class ActionsView(View): + def get(self): + """ + List of enabled default API actions. + + This list does not include any actions added by LabThings extensions, only + those part of the default OpenFlexure Microscope API. + """ + global _actions + + actions = {} + for name, action in enabled_root_actions().items(): + d = { + "links": { + "self": { + "href": current_labthing().url_for(action["view_class"]), + "mimetype": "application/json", + **description_from_view(action["view_class"]) + } + }, + "rule": action["rule"], + "view_class": str(action["view_class"]), + } + + actions[name] = d + + return actions diff --git a/openflexure_microscope/api/v2/views/actions/camera.py b/openflexure_microscope/api/v2/views/actions/camera.py new file mode 100644 index 00000000..917a72eb --- /dev/null +++ b/openflexure_microscope/api/v2/views/actions/camera.py @@ -0,0 +1,130 @@ +from openflexure_microscope.api.utilities import get_bool, JsonResponse +from openflexure_microscope.common.flask_labthings.view import View +from openflexure_microscope.common.flask_labthings.find import find_component +from openflexure_microscope.common.flask_labthings.decorators import ( + use_args, + marshal_with, + doc, + tag, + ThingAction, + doc_response, +) + +from openflexure_microscope.common.flask_labthings import fields +from openflexure_microscope.utilities import filter_dict + +from openflexure_microscope.api.v2.views.captures import capture_schema + +import logging +from flask import jsonify, request, abort, url_for, redirect, send_file + + +@ThingAction +class CaptureAPI(View): + """ + Create a new image capture. + """ + + @use_args( + { + "filename": fields.String(example="MyFileName"), + "temporary": fields.Boolean( + missing=False, description="Delete capture on shutdown" + ), + "use_video_port": fields.Boolean(missing=False), + "bayer": fields.Boolean( + missing=False, description="Store raw bayer data in file" + ), + "metadata": fields.Dict(missing={}, example={"Client": "SwaggerUI"}), + "tags": fields.List(fields.String, missing=[], example=["docs"]), + "resize": fields.Dict( + missing=None, example={"width": 640, "height": 480} + ), # TODO: Validate keys + } + ) + @marshal_with(capture_schema) + @doc_response(200, description="Capture successful") + def post(self, args): + """ + Create a new capture + """ + microscope = find_component("org.openflexure.microscope") + + resize = args.get("resize", None) + if resize: + if ("width" in resize) and ("height" in resize): + resize = ( + int(resize["width"]), + int(resize["height"]), + ) # Convert dict to tuple + else: + abort(404) + + # Explicitally acquire lock (prevents empty files being created if lock is unavailable) + with microscope.camera.lock: + output = microscope.camera.new_image( + temporary=args.get("temporary"), filename=args.get("filename") + ) + + microscope.camera.capture( + output.file, + use_video_port=args.get("use_video_port"), + resize=resize, + bayer=args.get("bayer"), + ) + + # Inject system metadata + output.put_metadata(microscope.metadata, system=True) + + # Insert custom metadata + output.put_metadata(args.get("metadata")) + + # Insert custom tags + output.put_tags(args.get("tags")) + + return output + + +@ThingAction +class GPUPreviewStartAPI(View): + """ + Start the onboard GPU preview. + Optional "window" parameter can be passed to control the position and size of the preview window, + in the format ``[x, y, width, height]``. + """ + + @use_args( + {"window": fields.List(fields.Integer, missing=[], example=[0, 0, 640, 480])} + ) + def post(self, args): + """ + Start the onboard GPU preview. + """ + microscope = find_component("org.openflexure.microscope") + + window = args.get("window") + logging.debug(window) + + if len(window) != 4: + fullscreen = True + window = None + else: + fullscreen = False + window = [int(w) for w in window] + + microscope.camera.start_preview(fullscreen=fullscreen, window=window) + + # TODO: Make schema for microscope status + return jsonify(microscope.status) + + +@ThingAction +class GPUPreviewStopAPI(View): + def post(self): + """ + Stop the onboard GPU preview. + """ + microscope = find_component("org.openflexure.microscope") + microscope.camera.stop_preview() + # TODO: Make schema for microscope status + return jsonify(microscope.status) diff --git a/openflexure_microscope/api/v2/views/actions/stage.py b/openflexure_microscope/api/v2/views/actions/stage.py new file mode 100644 index 00000000..35795781 --- /dev/null +++ b/openflexure_microscope/api/v2/views/actions/stage.py @@ -0,0 +1,75 @@ +from openflexure_microscope.api.utilities import JsonResponse +from openflexure_microscope.common.flask_labthings.view import View +from openflexure_microscope.common.flask_labthings.find import find_component +from openflexure_microscope.common.flask_labthings.decorators import ( + use_args, + marshal_with, + doc, + ThingAction, +) +from openflexure_microscope.common.flask_labthings import fields + +from openflexure_microscope.utilities import axes_to_array, filter_dict + +from flask import Blueprint, jsonify, request + +import logging + + +@ThingAction +class MoveStageAPI(View): + @use_args( + { + "absolute": fields.Boolean( + default=False, example=False, description="Move to an absolute position" + ), + "x": fields.Int(default=0, example=100), + "y": fields.Int(default=0, example=100), + "z": fields.Int(default=0, example=20), + } + ) + def post(self, args): + """ + Move the microscope stage in x, y, z + """ + microscope = find_component("org.openflexure.microscope") + + # Handle absolute positioning (calculate a relative move from current position and target) + if (args.get("absolute")) and (microscope.stage): # Only if stage exists + target_position = axes_to_array(args, ["x", "y", "z"]) + logging.debug("TARGET: {}".format(target_position)) + position = [ + target_position[i] - microscope.stage.position[i] for i in range(3) + ] + logging.debug("DELTA: {}".format(position)) + + else: + # Get coordinates from payload + position = axes_to_array(args, ["x", "y", "z"], [0, 0, 0]) + + logging.debug(position) + + # Move if stage exists + if microscope.stage: + # Explicitally acquire lock + with microscope.stage.lock: + microscope.stage.move_rel(position) + else: + logging.warning("Unable to move. No stage found.") + + # TODO: Make schema for microscope status + return jsonify(microscope.status["stage"]["position"]) + + +@ThingAction +class ZeroStageAPI(View): + def post(self): + """ + Zero the stage coordinates. + Does not move the stage, but rather makes the current position read as [0, 0, 0] + """ + microscope = find_component("org.openflexure.microscope") + microscope.stage.zero_position() + + # TODO: Make schema for microscope status + return jsonify(microscope.status["stage"]) diff --git a/openflexure_microscope/api/v2/blueprints/actions/system.py b/openflexure_microscope/api/v2/views/actions/system.py similarity index 65% rename from openflexure_microscope/api/v2/blueprints/actions/system.py rename to openflexure_microscope/api/v2/views/actions/system.py index fdc6f08e..a370d05e 100644 --- a/openflexure_microscope/api/v2/blueprints/actions/system.py +++ b/openflexure_microscope/api/v2/views/actions/system.py @@ -1,9 +1,13 @@ -from openflexure_microscope.api.views import MicroscopeView - +from openflexure_microscope.common.flask_labthings.view import View import subprocess import os from sys import platform +from openflexure_microscope.common.flask_labthings.decorators import ( + ThingAction, + doc_response, +) + def is_raspberrypi(raise_on_errors=False): """ @@ -13,34 +17,32 @@ def is_raspberrypi(raise_on_errors=False): return os.path.exists("/usr/bin/raspi-config") -class ShutdownAPI(MicroscopeView): +@ThingAction +class ShutdownAPI(View): """ Attempt to shutdown the device """ + @doc_response(201) def post(self): """ Attempt to shutdown the device - - .. :quickref: Actions; Shutdown - """ subprocess.Popen(["sudo", "shutdown", "-h", "now"]) return "{}", 201 -class RebootAPI(MicroscopeView): +@ThingAction +class RebootAPI(View): """ Attempt to reboot the device """ + @doc_response(201) def post(self): """ - Attempt to shutdown the device - - .. :quickref: Actions; Shutdown - + Attempt to reboot the device """ subprocess.Popen(["sudo", "shutdown", "-r", "now"]) diff --git a/openflexure_microscope/api/v2/views/captures.py b/openflexure_microscope/api/v2/views/captures.py new file mode 100644 index 00000000..4cb82325 --- /dev/null +++ b/openflexure_microscope/api/v2/views/captures.py @@ -0,0 +1,234 @@ +import logging +from flask import abort, request, redirect, url_for, send_file, jsonify + +from openflexure_microscope.api.utilities import get_bool, JsonResponse + +from openflexure_microscope.common.flask_labthings.schema import Schema +from openflexure_microscope.common.flask_labthings import fields +from openflexure_microscope.common.flask_labthings.view import View +from openflexure_microscope.common.flask_labthings.utilities import ( + description_from_view, +) +from openflexure_microscope.common.flask_labthings.decorators import marshal_with, doc_response, Tag, ThingProperty + +from openflexure_microscope.common.flask_labthings.find import find_component + +from marshmallow import pre_dump + + +class CaptureSchema(Schema): + id = fields.String() + file = fields.String( + data_key="path", description="Path of file on microscope device" + ) + exists = fields.Bool(data_key="available") + filename = fields.String() + metadata = fields.Dict() + + links = fields.Dict() + + # TODO: Automate this somewhat + @pre_dump + def generate_links(self, data, **kwargs): + data.links = { + "self": { + "href": url_for(CaptureView.endpoint, id=data.id, _external=True), + "mimetype": "application/json", + **description_from_view(CaptureView), + }, + "tags": { + "href": url_for(CaptureTags.endpoint, id=data.id, _external=True), + "mimetype": "application/json", + **description_from_view(CaptureTags), + }, + "metadata": { + "href": url_for(CaptureMetadata.endpoint, id=data.id, _external=True), + "mimetype": "application/json", + **description_from_view(CaptureMetadata), + }, + "download": { + "href": url_for( + CaptureDownload.endpoint, + id=data.id, + filename=data.filename, + _external=True, + ), + "mimetype": "image/jpeg", + **description_from_view(CaptureDownload), + }, + } + return data + + +capture_schema = CaptureSchema() +capture_list_schema = CaptureSchema(many=True) + + +@ThingProperty +@Tag("captures") +class CaptureList(View): + @marshal_with(CaptureSchema(many=True)) + def get(self): + """ + List all image captures + """ + microscope = find_component("org.openflexure.microscope") + image_list = microscope.camera.images + return image_list + + +@Tag("captures") +class CaptureView(View): + @marshal_with(CaptureSchema()) + def get(self, id): + """ + Description of a single image capture + """ + microscope = find_component("org.openflexure.microscope") + capture_obj = microscope.camera.image_from_id(id) + + if not capture_obj: + return abort(404) # 404 Not Found + + return capture_obj + + def delete(self, id): + """ + Delete a single image capture + """ + microscope = find_component("org.openflexure.microscope") + capture_obj = microscope.camera.image_from_id(id) + + if not capture_obj: + return abort(404) # 404 Not Found + + capture_obj.delete() + + return "", 204 + + +@Tag("captures") +class CaptureDownload(View): + @doc_response(200, mimetype="image/jpeg") + def get(self, id, filename): + """ + Image data for a single image capture + """ + microscope = find_component("org.openflexure.microscope") + capture_obj = microscope.camera.image_from_id(id) + + if not capture_obj: + return abort(404) # 404 Not Found + + thumbnail = get_bool(request.args.get("thumbnail")) + + # If no filename is specified, redirect to the capture's currently set filename + if not filename: + return redirect( + url_for( + "DownloadAPI", + id=id, + filename=capture_obj.filename, + thumbnail=thumbnail, + ), + code=307, + ) + + # Download the image data using the requested filename + if thumbnail: + img = capture_obj.thumbnail + else: + img = capture_obj.data + + return send_file(img, mimetype="image/jpeg") + + +@Tag("captures") +class CaptureTags(View): + def get(self, id): + """ + Get tags associated with a single image capture + """ + microscope = find_component("org.openflexure.microscope") + capture_obj = microscope.camera.image_from_id(id) + + if not capture_obj: + return abort(404) # 404 Not Found + + return jsonify(capture_obj.tags) + + def put(self, id): + """ + Add tags to a single image capture + """ + microscope = find_component("org.openflexure.microscope") + capture_obj = microscope.camera.image_from_id(id) + + if not capture_obj: + return abort(404) # 404 Not Found + + # TODO: Replace with normal Flask request JSON thing + data_dict = JsonResponse(request).json + + if type(data_dict) != list: + return abort(400) + + capture_obj.put_tags(data_dict) + + return jsonify(capture_obj.tags) + + def delete(self, capture_id): + """ + Delete tags from a single image capture + """ + microscope = find_component("org.openflexure.microscope") + capture_obj = microscope.camera.image_from_id(id) + + if not capture_obj: + return abort(404) # 404 Not Found + + data_dict = JsonResponse(request).json + + if type(data_dict) != list: + return abort(400) + + for tag in data_dict: + capture_obj.delete_tag(str(tag)) + + return jsonify(capture_obj.tags) + + +@Tag("captures") +class CaptureMetadata(View): + def get(self, id): + """ + Get metadata associated with a single image capture + """ + microscope = find_component("org.openflexure.microscope") + capture_obj = microscope.camera.image_from_id(id) + + if not capture_obj: + return abort(404) # 404 Not Found + + return jsonify(capture_obj.metadata) + + def put(self, id): + """ + Update metadata for a single image capture + """ + microscope = find_component("org.openflexure.microscope") + capture_obj = microscope.camera.image_from_id(id) + + if not capture_obj: + return abort(404) # 404 Not Found + + data_dict = JsonResponse(request).json + logging.debug(data_dict) + + if type(data_dict) != dict: + return abort(400) + + # TODO: Allow putting system metadata maybe? + capture_obj.put_metadata(data_dict) + + return jsonify(capture_obj.metadata) diff --git a/openflexure_microscope/api/v2/views/state.py b/openflexure_microscope/api/v2/views/state.py new file mode 100644 index 00000000..b46b4e63 --- /dev/null +++ b/openflexure_microscope/api/v2/views/state.py @@ -0,0 +1,103 @@ +from openflexure_microscope.api.utilities import JsonResponse + +from openflexure_microscope.common.labthings_core.utilities import ( + get_by_path, + set_by_path, + create_from_path, +) + +from openflexure_microscope.common.flask_labthings.find import find_component +from openflexure_microscope.common.flask_labthings.view import View + +from openflexure_microscope.common.flask_labthings.decorators import ThingProperty, Tag, doc_response + +from flask import jsonify, request, abort +import logging + + +@ThingProperty +class SettingsProperty(View): + def get(self): + """ + Current microscope settings, including camera and stage + """ + microscope = find_component("org.openflexure.microscope") + return jsonify(microscope.read_settings()) + + def put(self): + """ + Update current microscope settings, including camera and stage + """ + microscope = find_component("org.openflexure.microscope") + payload = JsonResponse(request) + + logging.debug("Updating settings from PUT request:") + logging.debug(payload.json) + + microscope.apply_settings(payload.json) + microscope.save_settings() + + return self.get() + + +@Tag("properties") +class NestedSettingsProperty(View): + @doc_response(404, description="Settings key cannot be found") + def get(self, route): + """ + Show a nested section of the current microscope settings + """ + microscope = find_component("org.openflexure.microscope") + keys = route.split("/") + + try: + value = get_by_path(microscope.read_settings(), keys) + except KeyError: + return abort(404) + + return jsonify(value) + + @doc_response(404, description="Settings key cannot be found") + def put(self, route): + """ + Update a nested section of the current microscope settings + """ + microscope = find_component("org.openflexure.microscope") + keys = route.split("/") + payload = JsonResponse(request) + + dictionary = create_from_path(keys) + set_by_path(dictionary, keys, payload.json) + + microscope.apply_settings(dictionary) + microscope.save_settings() + + return self.get(route) + + +@ThingProperty +class StatusProperty(View): + def get(self): + """ + Show current read-only state of the microscope + """ + microscope = find_component("org.openflexure.microscope") + return jsonify(microscope.status) + + +@Tag("properties") +class NestedStatusProperty(View): + @doc_response(404, description="Status key cannot be found") + def get(self, route): + """ + Show a nested section of the current microscope state + """ + microscope = find_component("org.openflexure.microscope") + keys = route.split("/") + + try: + value = get_by_path(microscope.status, keys) + except KeyError: + return abort(404) + + return jsonify(value) diff --git a/openflexure_microscope/api/v2/views/streams.py b/openflexure_microscope/api/v2/views/streams.py new file mode 100644 index 00000000..8234d52f --- /dev/null +++ b/openflexure_microscope/api/v2/views/streams.py @@ -0,0 +1,51 @@ +from openflexure_microscope.api.utilities import gen, JsonResponse + +from openflexure_microscope.common.labthings_core.utilities import ( + get_by_path, + set_by_path, + create_from_path, +) + +from openflexure_microscope.common.flask_labthings.find import find_component +from openflexure_microscope.common.flask_labthings.view import View +from openflexure_microscope.common.flask_labthings.decorators import doc_response, ThingProperty + +from flask import Response + + +@ThingProperty +class MjpegStream(View): + """ + Real-time MJPEG stream from the microscope camera + """ + + @doc_response(200, mimetype="multipart/x-mixed-replace") + def get(self): + """ + MJPEG stream from the microscope camera + """ + microscope = find_component("org.openflexure.microscope") + # Restart stream worker thread + microscope.camera.start_worker() + + return Response( + gen(microscope.camera), mimetype="multipart/x-mixed-replace; boundary=frame" + ) + + +@ThingProperty +class SnapshotStream(View): + """ + Single JPEG snapshot from the camera stream + """ + + @doc_response(200, description="Snapshot taken", mimetype="image/jpeg") + def get(self): + """ + Single snapshot from the camera stream + """ + microscope = find_component("org.openflexure.microscope") + # Restart stream worker thread + microscope.camera.start_worker() + + return Response(microscope.camera.get_frame(), mimetype="image/jpeg") diff --git a/openflexure_microscope/api/views.py b/openflexure_microscope/api/views.py deleted file mode 100644 index 1ec58243..00000000 --- a/openflexure_microscope/api/views.py +++ /dev/null @@ -1,28 +0,0 @@ -from flask.views import MethodView - - -class MicroscopeView(MethodView): - """ - Create a generic MethodView with a globally available - microscope object passed as an argument. - """ - - def __init__(self, microscope, **kwargs): - - self.microscope = microscope - - MethodView.__init__(self, **kwargs) - - -class MicroscopeViewPlugin(MicroscopeView): - """ - Create a generic MethodView with a globally available - microscope object passed as an argument, and a plugin - reference stored in 'self'. Initially None. - """ - - def __init__(self, microscope, plugin=None, **kwargs): - - self.plugin = plugin - - MicroscopeView.__init__(self, microscope=microscope, **kwargs) diff --git a/openflexure_microscope/camera/base.py b/openflexure_microscope/camera/base.py index ebce1bf8..ff1c3527 100644 --- a/openflexure_microscope/camera/base.py +++ b/openflexure_microscope/camera/base.py @@ -9,8 +9,8 @@ import logging from abc import ABCMeta, abstractmethod from .capture import CaptureObject -from openflexure_microscope.utilities import entry_by_id -from openflexure_microscope.common.lock import StrictLock +from openflexure_microscope.utilities import entry_by_uuid +from openflexure_microscope.common.labthings_core.lock import StrictLock BASE_CAPTURE_PATH = os.path.join(os.path.expanduser("~"), "micrographs") @@ -250,11 +250,11 @@ class BaseCamera(metaclass=ABCMeta): def image_from_id(self, image_id): """Return an image StreamObject with a matching ID.""" - return entry_by_id(image_id, self.images) + return entry_by_uuid(image_id, self.images) def video_from_id(self, video_id): """Return a video StreamObject with a matching ID.""" - return entry_by_id(video_id, self.videos) + return entry_by_uuid(video_id, self.videos) # CREATING NEW CAPTURES diff --git a/openflexure_microscope/camera/capture.py b/openflexure_microscope/camera/capture.py index e45dde80..20ec71da 100644 --- a/openflexure_microscope/camera/capture.py +++ b/openflexure_microscope/camera/capture.py @@ -129,7 +129,7 @@ class CaptureObject(object): """Create a new StreamObject, to manage capture data.""" # Store a nice ID - self.id = uuid.uuid4().hex #: str: Unique capture ID + self.id = uuid.uuid4() #: str: Unique capture ID logging.debug("Created StreamObject {}".format(self.id)) self.timestring = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S") diff --git a/openflexure_microscope/camera/pi.py b/openflexure_microscope/camera/pi.py index a3e1fc47..d73d0c02 100644 --- a/openflexure_microscope/camera/pi.py +++ b/openflexure_microscope/camera/pi.py @@ -317,9 +317,6 @@ class PiCameraStreamer(BaseCamera): """ Change the camera zoom, handling re-centering and scaling. """ - logging.warning( - "set_zoom is deprecated. Please use the 'zoom' property in picamera_settings." - ) with self.lock: self.status["zoom_value"] = float(zoom_value) if self.status["zoom_value"] < 1: diff --git a/openflexure_microscope/common/__init__.py b/openflexure_microscope/common/__init__.py index e69de29b..c9450d14 100644 --- a/openflexure_microscope/common/__init__.py +++ b/openflexure_microscope/common/__init__.py @@ -0,0 +1,2 @@ +from . import flask_labthings +from openflexure_microscope.common.labthings_core import tasks diff --git a/openflexure_microscope/common/flask_labthings/__init__.py b/openflexure_microscope/common/flask_labthings/__init__.py new file mode 100644 index 00000000..3b4b1d5b --- /dev/null +++ b/openflexure_microscope/common/flask_labthings/__init__.py @@ -0,0 +1 @@ +EXTENSION_NAME = "flask-lab" diff --git a/openflexure_microscope/common/flask_labthings/decorators.py b/openflexure_microscope/common/flask_labthings/decorators.py new file mode 100644 index 00000000..7d6bf2c6 --- /dev/null +++ b/openflexure_microscope/common/flask_labthings/decorators.py @@ -0,0 +1,178 @@ +from webargs import flaskparser +from functools import wraps, update_wrapper +from flask import make_response +from http import HTTPStatus + +from openflexure_microscope.common.labthings_core.utilities import rupdate + +from .spec import update_spec +from .schema import TaskSchema + + +def unpack(value): + """Return a three tuple of data, code, and headers""" + if not isinstance(value, tuple): + return value, 200, {} + + try: + data, code, headers = value + return data, code, headers + except ValueError: + pass + + try: + data, code = value + return data, code, {} + except ValueError: + pass + + return value, 200, {} + + +class marshal_with(object): + def __init__(self, schema, code=200): + """ + :param schema: a dict of whose keys will make up the final + serialized response output + """ + self.schema = schema + self.code = code + + def __call__(self, f): + # Pass params to call function attribute for external access + update_spec(f, {"_schema": {self.code: self.schema}}) + # Wrapper function + @wraps(f) + def wrapper(*args, **kwargs): + resp = f(*args, **kwargs) + if isinstance(resp, tuple): + data, code, headers = unpack(resp) + return make_response(self.schema.jsonify(data), code, headers) + else: + return make_response(self.schema.jsonify(resp)) + + return wrapper + + +def marshal_task(f): + # Pass params to call function attribute for external access + update_spec(f, {"responses": {201: {"description": "Task started successfully"}}}) + update_spec(f, {"_schema": {201: TaskSchema()}}) + # Wrapper function + @wraps(f) + def wrapper(*args, **kwargs): + resp = f(*args, **kwargs) + if isinstance(resp, tuple): + data, code, headers = unpack(resp) + return make_response(TaskSchema().jsonify(data), code, headers) + else: + return make_response(TaskSchema().jsonify(resp)) + + return wrapper + + +def ThingAction(viewcls): + # Pass params to call function attribute for external access + update_spec(viewcls, {"tags": ["actions"]}) + update_spec(viewcls, {"_groups": ["actions"]}) + return viewcls + + +thing_action = ThingAction + + +def ThingProperty(viewcls): + # Pass params to call function attribute for external access + update_spec(viewcls, {"tags": ["properties"]}) + update_spec(viewcls, {"_groups": ["properties"]}) + return viewcls + + +thing_property = ThingProperty + + +class use_args(object): + def __init__(self, schema, **kwargs): + """ + Equivalent to webargs.flask_parser.use_args + """ + self.schema = schema + self.wrapper = flaskparser.use_args(schema, **kwargs) + + def __call__(self, f): + # Pass params to call function attribute for external access + update_spec(f, {"_params": self.schema}) + # Wrapper function + update_wrapper(self.wrapper, f) + return self.wrapper(f) + + +class use_kwargs(use_args): + def __init__(self, schema, **kwargs): + """ + Equivalent to webargs.flask_parser.use_kwargs + """ + kwargs["as_kwargs"] = True + use_args.__init__(self, schema, **kwargs) + + +class Doc(object): + def __init__(self, **kwargs): + self.kwargs = kwargs + + def __call__(self, f): + # Pass params to call function attribute for external access + update_spec(f, self.kwargs) + return f + + +doc = Doc + + +class Tag(object): + def __init__(self, tags): + if isinstance(tags, str): + self.tags = [tags] + elif isinstance(tags, list) and all([isinstance(e, str) for e in tags]): + self.tags = tags + else: + raise TypeError("Tags must be a string or list of strings") + + def __call__(self, f): + # Pass params to call function attribute for external access + update_spec(f, {"tags": self.tags}) + return f + + +tag = Tag + + +class doc_response(object): + def __init__(self, code, description=None, mimetype=None, **kwargs): + self.code = code + self.description = description + self.kwargs = kwargs + self.mimetype = mimetype + + self.response_dict = { + "responses": { + self.code: { + "description": self.description or HTTPStatus(self.code).phrase, + **self.kwargs, + } + } + } + + if self.mimetype: + self.response_dict.update({ + "responses": { + self.code: { + "content": {self.mimetype: {}} + } + } + }) + + def __call__(self, f): + # Pass params to call function attribute for external access + update_spec(f, self.response_dict) + return f diff --git a/openflexure_microscope/api/exceptions.py b/openflexure_microscope/common/flask_labthings/exceptions.py similarity index 93% rename from openflexure_microscope/api/exceptions.py rename to openflexure_microscope/common/flask_labthings/exceptions.py index 624d5dba..bbb83d1e 100644 --- a/openflexure_microscope/api/exceptions.py +++ b/openflexure_microscope/common/flask_labthings/exceptions.py @@ -24,7 +24,7 @@ class JSONExceptionHandler(object): status_code = error.code if isinstance(error, HTTPException) else 500 - response = {"status_code": status_code, "message": escape(message)} + response = {"code": status_code, "message": escape(message)} return jsonify(response), status_code def init_app(self, app): diff --git a/openflexure_microscope/common/flask_labthings/extensions.py b/openflexure_microscope/common/flask_labthings/extensions.py new file mode 100644 index 00000000..37bff401 --- /dev/null +++ b/openflexure_microscope/common/flask_labthings/extensions.py @@ -0,0 +1,142 @@ +import logging +import collections +import copy + +from importlib import util +import sys +import os +import glob + +from openflexure_microscope.common.labthings_core.utilities import ( + get_docstring, + camel_to_snake, + snake_to_spine, +) + + +class BaseExtension: + """ + Parent class for all extensions. + + Handles binding route views and forms. + """ + # TODO: Allow adding components to extensions + + def __init__(self, name: str, description="", version="0.0.0"): + self._views = ( + {} + ) # Key: Full, Python-safe ID. Val: Original rule, and view class + self._rules = {} # Key: Original rule. Val: View class + self._meta = {} # Extra metadata to add to the extension description + + self._cls = str(self) # String description of extension instance + + self.actions = [] + self.properties = [] + + self.name = name + self.description = get_docstring(self) + self.version = str(version) + + self.methods = {} + + @property + def views(self): + return self._views + + def add_view(self, view_class, rule, **kwargs): + # Remove all leading slashes from view route + cleaned_rule = rule + while cleaned_rule[0] == "/": + cleaned_rule = cleaned_rule[1:] + + # Expand the rule to include extension name + full_rule = "/{}/{}".format(self._name_uri_safe, cleaned_rule) + + view_id = cleaned_rule.replace("/", "_").replace("<", "").replace(">", "") + + # Create a Python-safe route ID + logging.debug(view_id) + + # Store route information in a dictionary + d = {"rule": full_rule, "view": view_class, "kwargs": kwargs} + + # Add view to private views dictionary + self._views[view_id] = d + # Store the rule expansion information + self._rules[rule] = self._views[view_id] + + @property + def meta(self): + d = {} + for k, v in self._meta.items(): + if callable(v): + d[k] = v() + else: + d[k] = v + return d + + def add_meta(self, key, val): + self._meta[key] = val + + @property + def _name(self): + return self.name + + @property + def _name_python_safe(self): + name = camel_to_snake(self._name) # Camel to snake + name = name.replace(" ", "_") # Spaces to snake + return name + + @property + def _name_uri_safe(self): + return snake_to_spine(self._name_python_safe) + + def add_method(self, method, method_name): + self.methods[method_name] = method + + if not hasattr(self, method_name): + setattr(self, method_name, method) + else: + logging.warning( + "Unable to bind method to extension. Method name already exists." + ) + + +def find_instances_in_module(module, class_to_find): + objs = [] + for attribute in dir(module): + if not attribute.startswith("__"): + if isinstance(getattr(module, attribute), class_to_find): + objs.append(getattr(module, attribute)) + return objs + + +def find_extensions_in_file(extension_path: str, module_name="extensions"): + logging.debug(f"Loading extensions from {extension_path}") + + spec = util.spec_from_file_location(module_name, extension_path) + mod = util.module_from_spec(spec) + sys.modules[spec.name] = mod + + spec.loader.exec_module(mod) + + if hasattr(mod, "__extensions__"): + return [getattr(mod, ext_name) for ext_name in mod.__extensions__] + else: + return find_instances_in_module(mod, BaseExtension) + + +def find_extensions(extension_dir: str, module_name="extensions"): + logging.debug(f"Loading extensions from {extension_dir}") + + extensions = [] + extension_paths = glob.glob(os.path.join(extension_dir, "*.py")) + + for extension_path in extension_paths: + extensions.extend( + find_extensions_in_file(extension_path, module_name=module_name) + ) + + return extensions diff --git a/openflexure_microscope/common/flask_labthings/fields.py b/openflexure_microscope/common/flask_labthings/fields.py new file mode 100644 index 00000000..68149fcd --- /dev/null +++ b/openflexure_microscope/common/flask_labthings/fields.py @@ -0,0 +1,2 @@ +# Marshmallow fields +from marshmallow.fields import * diff --git a/openflexure_microscope/common/flask_labthings/find.py b/openflexure_microscope/common/flask_labthings/find.py new file mode 100644 index 00000000..b866f992 --- /dev/null +++ b/openflexure_microscope/common/flask_labthings/find.py @@ -0,0 +1,50 @@ +import logging +from flask import current_app + +from . import EXTENSION_NAME + + +def current_labthing(): + app = current_app._get_current_object() + if not app: + return None + logging.debug("Active app extensions:") + logging.debug(app.extensions) + logging.debug("Active labthing:") + logging.debug(app.extensions[EXTENSION_NAME]) + return app.extensions[EXTENSION_NAME] + + +def registered_extensions(labthing_instance=None): + if not labthing_instance: + labthing_instance = current_labthing() + return labthing_instance.extensions + + +def registered_components(labthing_instance=None): + if not labthing_instance: + labthing_instance = current_labthing() + return labthing_instance.components + + +def find_component(device_name, labthing_instance=None): + if not labthing_instance: + labthing_instance = current_labthing() + + if device_name in labthing_instance.components: + return labthing_instance.components[device_name] + else: + return None + + +def find_extension(extension_name, labthing_instance=None): + if not labthing_instance: + labthing_instance = current_labthing() + + logging.debug("Current labthing:") + logging.debug(current_labthing()) + + if extension_name in labthing_instance.extensions: + return labthing_instance.extensions[extension_name] + else: + return None diff --git a/openflexure_microscope/common/flask_labthings/labthing.py b/openflexure_microscope/common/flask_labthings/labthing.py new file mode 100644 index 00000000..ce1e17f3 --- /dev/null +++ b/openflexure_microscope/common/flask_labthings/labthing.py @@ -0,0 +1,288 @@ +from flask import url_for, jsonify +from apispec import APISpec +from apispec.ext.marshmallow import MarshmallowPlugin + +from . import EXTENSION_NAME # TODO: Move into .names +from .names import TASK_ENDPOINT, TASK_LIST_ENDPOINT, EXTENSION_LIST_ENDPOINT +from .extensions import BaseExtension +from .utilities import description_from_view +from .spec import rule2path, get_spec +from .decorators import tag + +from .views.extensions import ExtensionList +from .views.tasks import TaskList, TaskView +from .views.docs import docs_blueprint, SwaggerUIView, W3CThingDescriptionView + +from openflexure_microscope.common.labthings_core.utilities import get_docstring + +import logging + + +class LabThing(object): + def __init__( + self, + app=None, + prefix: str = "", + title: str = "", + description: str = "", + version: str = "0.0.0", + ): + self.app = app + + self.components = {} + + self.extensions = {} + + self.views = [] + self.properties = {} + self.actions = {} + + self.custom_root_links = {} + + self.endpoints = set() + + self.url_prefix = prefix + self._description = description + self._title = title + self._version = version + + # Store handlers for things like errors and CORS + self.handlers = {} + + self.spec = APISpec( + title=self.title, + version=self.version, + openapi_version="3.0.2", + plugins=[MarshmallowPlugin()], + ) + + if app is not None: + self.init_app(app) + + @property + def description(self,): + return self._description + + @description.setter + def description(self, description: str): + self._description = description + self.spec.description = description + + @property + def title(self,): + return self._title + + @title.setter + def title(self, title: str): + self._title = title + self.spec.title = title + + @property + def version(self,): + return str(self._version) + + @version.setter + def version(self, version: str): + self._version = version + self.spec.version = version + + ### Flask stuff + + def init_app(self, app): + app.teardown_appcontext(self.teardown) + + # Register Flask extension + app.extensions = getattr(app, "extensions", {}) + app.extensions[EXTENSION_NAME] = self + + # Add resources, if registered before tying to a Flask app + if len(self.views) > 0: + for resource, urls, endpoint, kwargs in self.views: + self._register_view(app, resource, *urls, endpoint=endpoint, **kwargs) + + # Create base routes + self._create_base_routes() + + def teardown(self, exception): + pass + + def _create_base_routes(self): + # Add root representation + self.app.add_url_rule(self._complete_url("/", ""), "rootrep", self.rootrep) + # Add thing descriptions + self.app.register_blueprint(docs_blueprint, url_prefix=self.url_prefix) + + # Add extension overview + self.add_view(ExtensionList, "/extensions", endpoint=EXTENSION_LIST_ENDPOINT) + # Add task routes + self.add_view(TaskList, "/tasks", endpoint=TASK_LIST_ENDPOINT) + self.add_view(TaskView, "/tasks/", endpoint=TASK_ENDPOINT) + + ### Device stuff + + def add_component(self, device_object, device_name: str): + self.components[device_name] = device_object + + ### Extension stuff + + def register_extension(self, extension_object): + if isinstance(extension_object, BaseExtension): + self.extensions[extension_object.name] = extension_object + else: + raise TypeError("Extension object must be an instance of BaseExtension") + + for extension_view_id, extension_view in extension_object.views.items(): + # Add route to the extensions blueprint + self.add_view( + tag("extensions")(extension_view["view"]), + "/extensions" + extension_view["rule"], + **extension_view["kwargs"], + ) + + ### Resource stuff + + def _complete_url(self, url_part, registration_prefix): + """This method is used to defer the construction of the final url in + the case that the Api is created with a Blueprint. + :param url_part: The part of the url the endpoint is registered with + :param registration_prefix: The part of the url contributed by the + blueprint. Generally speaking, BlueprintSetupState.url_prefix + """ + parts = [registration_prefix, self.url_prefix, url_part] + return "".join([part for part in parts if part]) + + def add_view(self, resource, *urls, endpoint=None, **kwargs): + """Adds a view to the api. + :param resource: the class name of your resource + :type resource: :class:`Type[Resource]` + :param urls: one or more url routes to match for the resource, standard + flask routing rules apply. Any url variables will be + passed to the resource method as args. + :type urls: str + :param endpoint: endpoint name (defaults to :meth:`Resource.__name__` + Can be used to reference this route in :class:`fields.Url` fields + :type endpoint: str + :param resource_class_args: args to be forwarded to the constructor of + the resource. + :type resource_class_args: tuple + :param resource_class_kwargs: kwargs to be forwarded to the constructor + of the resource. + :type resource_class_kwargs: dict + Additional keyword arguments not specified above will be passed as-is + to :meth:`flask.Flask.add_url_rule`. + Examples:: + api.add_resource(HelloWorld, '/', '/hello') + api.add_resource(Foo, '/foo', endpoint="foo") + api.add_resource(FooSpecial, '/special/foo', endpoint="foo") + """ + endpoint = endpoint or resource.__name__.lower() + + logging.debug(f"{endpoint}: {type(resource)}") + + if self.app is not None: + self._register_view(self.app, resource, *urls, endpoint=endpoint, **kwargs) + + self.views.append((resource, urls, endpoint, kwargs)) + + def view(self, *urls, **kwargs): + def decorator(cls): + self.add_view(cls, *urls, **kwargs) + return cls + + return decorator + + def _register_view(self, app, view, *urls, endpoint=None, **kwargs): + endpoint = endpoint or view.__name__.lower() + self.endpoints.add(endpoint) + resource_class_args = kwargs.pop("resource_class_args", ()) + resource_class_kwargs = kwargs.pop("resource_class_kwargs", {}) + + # NOTE: 'view_functions' is cleaned up from Blueprint class in Flask 1.0 + if endpoint in getattr(app, "view_functions", {}): + previous_view_class = app.view_functions[endpoint].__dict__["view_class"] + + # if you override the endpoint with a different class, avoid the collision by raising an exception + if previous_view_class != view: + raise ValueError( + "This endpoint (%s) is already set to the class %s." + % (endpoint, previous_view_class.__name__) + ) + + view.endpoint = endpoint + resource_func = view.as_view( + endpoint, *resource_class_args, **resource_class_kwargs + ) + + for url in urls: + # If we've got no Blueprint, just build a url with no prefix + rule = self._complete_url(url, "") + # Add the url to the application or blueprint + app.add_url_rule(rule, view_func=resource_func, **kwargs) + # Add the resource to our API spec + #self.spec.path(**view2path(rule, view, self.spec)) + + # TEST: Getting Flask rule objects + flask_rules = app.url_map._rules_by_endpoint.get(endpoint) + for flask_rule in flask_rules: + self.spec.path(**rule2path(flask_rule, view, self.spec)) + + # Handle resource groups listed in API spec + view_spec = get_spec(view) + view_groups = view_spec.get("_groups", {}) + if "actions" in view_groups: + self.actions[view.endpoint] = view + if "properties" in view_groups: + self.properties[view.endpoint] = view + + ### Utilities + + def url_for(self, view, **values): + """Generates a URL to the given resource. + Works like :func:`flask.url_for`.""" + endpoint = view.endpoint + return url_for(endpoint, **values) + + def owns_endpoint(self, endpoint): + return endpoint in self.endpoints + + def add_root_link(self, view, title, kwargs={}): + self.custom_root_links[title] = (view, kwargs) + + ### Description + def rootrep(self): + """ + Root representation + """ + # TODO: Allow custom root representations + + rr = { + "id": url_for("rootrep", _external=True), + "title": self.title, + "description": self.description, + "links": { + "thingDescription": { + "href": url_for("labthings_docs.w3c_td", _external=True), + "description": get_docstring(W3CThingDescriptionView), + }, + "swaggerUI": { + "href": url_for("labthings_docs.swagger_ui", _external=True), + **description_from_view(SwaggerUIView), + }, + "extensions": { + "href": self.url_for(ExtensionList, _external=True), + **description_from_view(ExtensionList), + }, + "tasks": { + "href": self.url_for(TaskList, _external=True), + **description_from_view(TaskList), + }, + }, + } + + for title, (view, kwargs) in self.custom_root_links.items(): + rr["links"][title] = { + "href": self.url_for(view, **kwargs, _external=True), + **description_from_view(view), + } + + return jsonify(rr) diff --git a/openflexure_microscope/common/flask_labthings/names.py b/openflexure_microscope/common/flask_labthings/names.py new file mode 100644 index 00000000..417c5138 --- /dev/null +++ b/openflexure_microscope/common/flask_labthings/names.py @@ -0,0 +1,3 @@ +TASK_ENDPOINT = "labthing_task" +TASK_LIST_ENDPOINT = "labthing_task_list" +EXTENSION_LIST_ENDPOINT = "labthing_extension_list" diff --git a/openflexure_microscope/common/flask_labthings/quick.py b/openflexure_microscope/common/flask_labthings/quick.py new file mode 100644 index 00000000..2f9f5ed4 --- /dev/null +++ b/openflexure_microscope/common/flask_labthings/quick.py @@ -0,0 +1,41 @@ +from flask import Flask +from flask_cors import CORS + +from .labthing import LabThing +from .exceptions import JSONExceptionHandler + + +def create_app( + import_name, + prefix: str = "", + title: str = "", + description: str = "", + version: str = "0.0.0", + handle_errors: bool = True, + handle_cors: bool = True, + flask_kwargs: dict = {}, +): + app = Flask(import_name, **flask_kwargs) + app.url_map.strict_slashes = False + + # Handle CORS + if handle_cors: + cors_handler = CORS(app, resources=f"{prefix}/*") + + # Handle errors + if handle_errors: + error_handler = JSONExceptionHandler() + error_handler.init_app(app) + + # Create a LabThing + labthing = LabThing( + app, prefix=prefix, title=title, description=description, version=str(version) + ) + + # Store references to added-in handlers + if cors_handler: + labthing.handlers["cors"] = cors_handler + if error_handler: + labthing.handlers["error"] = error_handler + + return app, labthing diff --git a/openflexure_microscope/common/flask_labthings/schema.py b/openflexure_microscope/common/flask_labthings/schema.py new file mode 100644 index 00000000..7a8166e7 --- /dev/null +++ b/openflexure_microscope/common/flask_labthings/schema.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- +from flask import jsonify, url_for +import marshmallow + +from .names import TASK_ENDPOINT, TASK_LIST_ENDPOINT, EXTENSION_LIST_ENDPOINT +from .utilities import view_class_from_endpoint, description_from_view +from . import fields + +MARSHMALLOW_VERSION_INFO = tuple( + [int(part) for part in marshmallow.__version__.split(".") if part.isdigit()] +) + +sentinel = object() + + +class Schema(marshmallow.Schema): + """Base serializer with which to define custom serializers. + See `marshmallow.Schema` for more details about the `Schema` API. + """ + + def jsonify(self, obj, many=sentinel, *args, **kwargs): + """Return a JSON response containing the serialized data. + :param obj: Object to serialize. + :param bool many: Whether `obj` should be serialized as an instance + or as a collection. If unset, defaults to the value of the + `many` attribute on this Schema. + :param kwargs: Additional keyword arguments passed to `flask.jsonify`. + .. versionchanged:: 0.6.0 + Takes the same arguments as `marshmallow.Schema.dump`. Additional + keyword arguments are passed to `flask.jsonify`. + .. versionchanged:: 0.6.3 + The `many` argument for this method defaults to the value of + the `many` attribute on the Schema. Previously, the `many` + argument of this method defaulted to False, regardless of the + value of `Schema.many`. + """ + if many is sentinel: + many = self.many + if MARSHMALLOW_VERSION_INFO[0] >= 3: + data = self.dump(obj, many=many) + else: + data = self.dump(obj, many=many).data + return jsonify(data, *args, **kwargs) + + +class TaskSchema(Schema): + _ID = fields.String(data_key="id") + target_string = fields.String(data_key="function") + _status = fields.String(data_key="status") + progress = fields.String() + data = fields.Raw() + _return_value = fields.Raw(data_key="return") + _start_time = fields.String(data_key="start_time") + _end_time = fields.String(data_key="end_time") + + links = fields.Dict() + + @marshmallow.pre_dump + def generate_links(self, data, **kwargs): + data.links = { + "self": { + "href": url_for(TASK_ENDPOINT, id=data.id, _external=True), + "mimetype": "application/json", + **description_from_view(view_class_from_endpoint(TASK_ENDPOINT)), + } + } + return data + + +class ExtensionSchema(Schema): + name = fields.String(data_key="title") + _name_python_safe = fields.String(data_key="pythonName") + _cls = fields.String(data_key="pythonObject") + meta = fields.Dict() + description = fields.String() + + links = fields.Dict() + + @marshmallow.pre_dump + def generate_links(self, data, **kwargs): + d = {} + for view_id, view_data in data.views.items(): + view_cls = view_data["view"] + view_rule = view_data["rule"] + # Make links dictionary if it doesn't yet exist + d[view_id] = { + "href": url_for(EXTENSION_LIST_ENDPOINT, _external=True) + view_rule, + **description_from_view(view_cls), + } + + data.links = d + + return data diff --git a/openflexure_microscope/common/flask_labthings/spec/__init__.py b/openflexure_microscope/common/flask_labthings/spec/__init__.py new file mode 100644 index 00000000..eb5bef96 --- /dev/null +++ b/openflexure_microscope/common/flask_labthings/spec/__init__.py @@ -0,0 +1,164 @@ +from ..view import View +from apispec import APISpec +from apispec.ext.marshmallow import MarshmallowPlugin + +from openflexure_microscope.common.labthings_core.utilities import ( + get_docstring, + get_summary, + rupdate, +) + +from ..fields import Field +from marshmallow import Schema as BaseSchema + +from .paths import rule_to_path, rule_to_params + +from werkzeug.routing import Rule +from collections import Mapping +from http import HTTPStatus + + +def update_spec(obj, spec): + obj.__apispec__ = obj.__dict__.get("__apispec__", {}) + rupdate(obj.__apispec__, spec) + return obj.__apispec__ + + +def get_spec(obj): + obj.__apispec__ = obj.__dict__.get("__apispec__", {}) + return obj.__apispec__ + + +def rule2path(rule: Rule, view: View, spec: APISpec): + params = { + "path": rule_to_path(rule), + "operations": view2operations(view, spec), + "description": get_docstring(view), + "summary": get_summary(view), + } + + # Add URL arguments + if rule.arguments: + for op in params.get("operations").keys(): + params["operations"][op].update({ + "parameters": rule_to_params(rule) + }) + + # Add extra parameters + if hasattr(view, "__apispec__"): + # Recursively update params + rupdate(params, view.__apispec__) + + return params + + +def view2operations(view: View, spec: APISpec): + # Operations inherit tags from parent + inherited_tags = [] + if hasattr(view, "__apispec__"): + inherited_tags = getattr(view, "__apispec__").get("tags", []) + + # Build dictionary of operations (HTTP methods) + ops = {} + for method in View.methods: + if hasattr(view, method): + ops[method] = {} + + rupdate( + ops[method], + { + "description": get_docstring(getattr(view, method)), + "summary": get_summary(getattr(view, method)), + "tags": inherited_tags, + }, + ) + + rupdate(ops[method], method2operation(getattr(view, method), spec)) + + return ops + + +def method2operation(method: callable, spec: APISpec): + if hasattr(method, "__apispec__"): + apispec = getattr(method, "__apispec__") + else: + apispec = {} + + op = {} + if "_params" in apispec: + rupdate( + op, + { + "requestBody": { + "content": { + "application/json": { + "schema": convert_schema(apispec.get("_params"), spec) + } + } + } + }, + ) + + if "_schema" in apispec: + for code, schema in apispec.get("_schema", {}).items(): + rupdate( + op, + { + "responses": { + code: { + "description": HTTPStatus(code).phrase, + "content": { + "application/json": { + "schema": convert_schema(schema, spec) + } + }, + } + } + }, + ) + else: + # If no explicit responses are known, populate with defaults + rupdate( + op, + { + "responses": { + 200: {"description": get_summary(method) or HTTPStatus(200).phrase} + } + }, + ) + + # Bung in any extra swagger fields supplied + for key, val in apispec.items(): + if not key in ["_params", "_schema"]: + rupdate(op, {key: val}) + + return op + + +def convert_schema(schema, spec: APISpec): + if isinstance(schema, BaseSchema): + return schema + elif isinstance(schema, Mapping): + return map2properties(schema, spec) + else: + raise TypeError( + "Unsupported schema type. Ensure schema is a Schema class, or dictionary of Field objects" + ) + + +def map2properties(schema, spec: APISpec): + marshmallow_plugin = next( + plugin for plugin in spec.plugins if isinstance(plugin, MarshmallowPlugin) + ) + converter = marshmallow_plugin.converter + + d = {} + for k, v in schema.items(): + if isinstance(v, Field): + d[k] = converter.field2property(v) + elif isinstance(v, Mapping): + d[k] = map2properties(v, spec) + else: + d[k] = v + + return {"properties": d} diff --git a/openflexure_microscope/common/flask_labthings/spec/paths.py b/openflexure_microscope/common/flask_labthings/spec/paths.py new file mode 100644 index 00000000..6c70c2c4 --- /dev/null +++ b/openflexure_microscope/common/flask_labthings/spec/paths.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- + +import re + +import werkzeug.routing + +PATH_RE = re.compile(r'<(?:[^:<>]+:)?([^<>]+)>') + + +def rule_to_path(rule): + return PATH_RE.sub(r'{\1}', rule.rule) + + +# Conversion map of werkzeug rule converters to Javascript schema types +CONVERTER_MAPPING = { + werkzeug.routing.UnicodeConverter: ('string', None), + werkzeug.routing.IntegerConverter: ('integer', 'int32'), + werkzeug.routing.FloatConverter: ('number', 'float'), +} + +DEFAULT_TYPE = ('string', None) + + +def rule_to_params(rule, overrides=None): + overrides = (overrides or {}) + result = [ + argument_to_param(argument, rule, overrides.get(argument, {})) + for argument in rule.arguments + ] + for key in overrides.keys(): + if overrides[key].get('in') in ('header', 'query'): + overrides[key]['name'] = overrides[key].get('name', key) + result.append(overrides[key]) + return result + + +def argument_to_param(argument, rule, override=None): + param = { + 'in': 'path', + 'name': argument, + 'required': True, + } + type_, format_ = CONVERTER_MAPPING.get(type(rule._converters[argument]), DEFAULT_TYPE) + param['type'] = type_ + if format_ is not None: + param['format'] = format_ + if rule.defaults and argument in rule.defaults: + param['default'] = rule.defaults[argument] + param.update(override or {}) + return param diff --git a/openflexure_microscope/common/flask_labthings/types.py b/openflexure_microscope/common/flask_labthings/types.py new file mode 100644 index 00000000..b0304368 --- /dev/null +++ b/openflexure_microscope/common/flask_labthings/types.py @@ -0,0 +1,66 @@ +# Marshmallow fields to JSON schema types +# Note: We shouldn't ever need to use this directly. We should go via the apispec converter +from apispec.ext.marshmallow.field_converter import DEFAULT_FIELD_MAPPING + +from . import fields + +# Extra standard library Python types +from datetime import date, datetime, time, timedelta +from decimal import Decimal +from typing import Dict, List, Tuple, Union +from uuid import UUID + +""" +TODO: Use this to convert arbitrary dictionary into its own schema, for W3C TD + +First: Convert Python non-builtins to builtins using DEFAULT_BUILTIN_CONVERSIONS +Then match types of each element to Field using DEFAULT_TYPE_MAPPING +Finally convert Fields to JSON using converter (preferred due to extra metadata), or DEFAULT_FIELD_MAPPING +""" +# Python types to Marshmallow fields +DEFAULT_TYPE_MAPPING = { + bool: fields.Boolean, + date: fields.Date, + datetime: fields.DateTime, + Decimal: fields.Decimal, + float: fields.Float, + int: fields.Integer, + str: fields.String, + time: fields.Time, + timedelta: fields.TimeDelta, + UUID: fields.UUID, + dict: fields.Dict, + Dict: fields.Dict, +} + +# Functions to handle conversion of common Python types into serialisable Python types + + +def ndarray_to_list(o): + return o.tolist() + + +def to_int(o): + return int(o) + + +def to_float(o): + return float(o) + + +def to_string(o): + return str(o) + + +# Map of Python type conversions +DEFAULT_BUILTIN_CONVERSIONS = { + "numpy.ndarray": ndarray_to_list, + "numpy.int": to_int, + "fractions.Fraction": to_float, +} + +# TODO: Deserialiser with inverse defaults +# TODO: Option to switch to .npy serialisation/deserialisation (or look for a better common array format) + +# Use with [x.__module__+"."+x.__name__ for x in inspect.getmro(type(POSSIBLE_MATCHER))] +# Resulting array will contain strings with the same format as keys in DEFAULT_BUILTIN_CONVERSIONS diff --git a/openflexure_microscope/common/flask_labthings/utilities.py b/openflexure_microscope/common/flask_labthings/utilities.py new file mode 100644 index 00000000..86c904da --- /dev/null +++ b/openflexure_microscope/common/flask_labthings/utilities.py @@ -0,0 +1,29 @@ +from openflexure_microscope.common.labthings_core.utilities import ( + get_docstring, + get_summary, +) + +from .view import View + +from flask import current_app + + +def description_from_view(view_class): + summary = get_summary(view_class) + + methods = [] + for method_key in View.methods: + if hasattr(view_class, method_key): + methods.append(method_key.upper()) + + # If no class summary was given, try using summaries from method functions + if not summary: + summary = get_summary(getattr(view_class, method_key)) + + d = {"methods": methods, "description": summary} + + return d + + +def view_class_from_endpoint(endpoint: str): + return current_app.view_functions[endpoint].view_class diff --git a/openflexure_microscope/common/flask_labthings/view.py b/openflexure_microscope/common/flask_labthings/view.py new file mode 100644 index 00000000..0998a199 --- /dev/null +++ b/openflexure_microscope/common/flask_labthings/view.py @@ -0,0 +1,27 @@ +from flask.views import MethodView + + +class View(MethodView): + """ + A LabThing Resource class should make use of functions get(), put(), post(), and delete() + corresponding to HTTP methods. + + These functions will allow for automated documentation generation + """ + + methods = ["get", "post", "put", "delete"] + endpoint = None + + def __init__(self, *args, **kwargs): + MethodView.__init__(self, *args, **kwargs) + + def doc(self): + docs = {"operations": {}} + if hasattr(self, "__apispec__"): + docs.update(self.__apispec__) + + for meth in View.methods: + if hasattr(self, meth) and hasattr(getattr(self, meth), "__apispec__"): + docs["operations"][meth] = {} + docs["operations"][meth] = getattr(self, meth).__apispec__ + return docs diff --git a/openflexure_microscope/api/v1/__init__.py b/openflexure_microscope/common/flask_labthings/views/__init__.py similarity index 100% rename from openflexure_microscope/api/v1/__init__.py rename to openflexure_microscope/common/flask_labthings/views/__init__.py diff --git a/openflexure_microscope/common/flask_labthings/views/docs/__init__.py b/openflexure_microscope/common/flask_labthings/views/docs/__init__.py new file mode 100644 index 00000000..4da62d29 --- /dev/null +++ b/openflexure_microscope/common/flask_labthings/views/docs/__init__.py @@ -0,0 +1,113 @@ +from flask import abort, url_for, jsonify, render_template, Blueprint, current_app, request + +from openflexure_microscope.common.labthings_core.utilities import get_docstring + +from ...view import View +from ...find import current_labthing +from ...spec import rule_to_path, rule_to_params + +import os + + +class APISpecView(View): + """ + OpenAPI v3 documentation + """ + + def get(self): + """ + OpenAPI v3 documentation + """ + return jsonify(current_labthing().spec.to_dict()) + + +class SwaggerUIView(View): + """ + Swagger UI documentation + """ + + def get(self): + return render_template("swagger-ui.html") + + +class W3CThingDescriptionView(View): + """ + W3C-style Thing Description + """ + + def get(self): + base_url = request.host_url.rstrip('/') + + props = {} + for key, prop in current_labthing().properties.items(): + prop_rules = current_app.url_map._rules_by_endpoint.get(prop.endpoint) + prop_urls = [rule_to_path(rule) for rule in prop_rules] + + props[key] = {} + props[key]["title"] = prop.__name__ + # TODO: Get description from __apispec__ preferentially + props[key]["description"] = get_docstring(prop) or ( + get_docstring(prop.get) if hasattr(prop, "get") else "" + ) + props[key]["readOnly"] = not ( + hasattr(prop, "post") or hasattr(prop, "put") or hasattr(prop, "delete") + ) + props[key]["writeOnly"] = not hasattr(prop, "get") + props[key]["links"] = [ + {"href": f"{base_url}{url}"} for url in prop_urls + ] + + props[key]["uriVariables"] = {} + for prop_rule in prop_rules: + params = rule_to_params(prop_rule) + params_dict = {} + for param in params: + params_dict.update({ + param.get("name"): { + "type": param.get("type") + } + }) + props[key]["uriVariables"].update(params_dict) + if not props[key]["uriVariables"]: + del props[key]["uriVariables"] + + actions = {} + for key, action in current_labthing().actions.items(): + action_rules = current_app.url_map._rules_by_endpoint.get(action.endpoint) + action_urls = [rule_to_path(rule) for rule in action_rules] + + actions[key] = {} + actions[key]["title"] = action.__name__ + # TODO: Get description from __apispec__ preferentially + actions[key]["description"] = get_docstring(action) or ( + get_docstring(action.post) if hasattr(action, "post") else "" + ) + actions[key]["links"] = [ + {"href": f"{base_url}{url}"} for url in action_urls + ] + + td = { + "@context": "https://www.w3.org/2019/wot/td/v1", + "id": url_for("labthings_docs.w3c_td", _external=True), + "title": current_labthing().title, + "description": current_labthing().description, + "properties": props, + "actions": actions, + } + + return jsonify(td) + + +docs_blueprint = Blueprint( + "labthings_docs", __name__, static_folder="./static", template_folder="./templates" +) + +docs_blueprint.add_url_rule( + "/swagger", view_func=APISpecView.as_view("swagger_json") +) +docs_blueprint.add_url_rule( + "/swagger-ui", view_func=SwaggerUIView.as_view("swagger_ui") +) +docs_blueprint.add_url_rule( + "/td", view_func=W3CThingDescriptionView.as_view("w3c_td") +) diff --git a/openflexure_microscope/common/flask_labthings/views/docs/static/favicon-16x16.png b/openflexure_microscope/common/flask_labthings/views/docs/static/favicon-16x16.png new file mode 100644 index 00000000..8b194e61 Binary files /dev/null and b/openflexure_microscope/common/flask_labthings/views/docs/static/favicon-16x16.png differ diff --git a/openflexure_microscope/common/flask_labthings/views/docs/static/favicon-32x32.png b/openflexure_microscope/common/flask_labthings/views/docs/static/favicon-32x32.png new file mode 100644 index 00000000..249737fe Binary files /dev/null and b/openflexure_microscope/common/flask_labthings/views/docs/static/favicon-32x32.png differ diff --git a/openflexure_microscope/common/flask_labthings/views/docs/static/index.html b/openflexure_microscope/common/flask_labthings/views/docs/static/index.html new file mode 100644 index 00000000..32169e36 --- /dev/null +++ b/openflexure_microscope/common/flask_labthings/views/docs/static/index.html @@ -0,0 +1,60 @@ + + + + + + Swagger UI + + + + + + + +
+ + + + + + diff --git a/openflexure_microscope/common/flask_labthings/views/docs/static/oauth2-redirect.html b/openflexure_microscope/common/flask_labthings/views/docs/static/oauth2-redirect.html new file mode 100644 index 00000000..a013fc82 --- /dev/null +++ b/openflexure_microscope/common/flask_labthings/views/docs/static/oauth2-redirect.html @@ -0,0 +1,68 @@ + + +Swagger UI: OAuth2 Redirect + + + + diff --git a/openflexure_microscope/common/flask_labthings/views/docs/static/swagger-ui-bundle.js b/openflexure_microscope/common/flask_labthings/views/docs/static/swagger-ui-bundle.js new file mode 100644 index 00000000..559df3ca --- /dev/null +++ b/openflexure_microscope/common/flask_labthings/views/docs/static/swagger-ui-bundle.js @@ -0,0 +1,134 @@ +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(function(){try{return require("esprima")}catch(e){}}()):"function"==typeof define&&define.amd?define(["esprima"],t):"object"==typeof exports?exports.SwaggerUIBundle=t(function(){try{return require("esprima")}catch(e){}}()):e.SwaggerUIBundle=t(e.esprima)}(window,function(e){return function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/dist",n(n.s=488)}([function(e,t,n){"use strict";e.exports=n(104)},function(e,t,n){e.exports=function(){"use strict";var e=Array.prototype.slice;function t(e,t){t&&(e.prototype=Object.create(t.prototype)),e.prototype.constructor=e}function n(e){return a(e)?e:J(e)}function r(e){return s(e)?e:K(e)}function o(e){return u(e)?e:Y(e)}function i(e){return a(e)&&!c(e)?e:$(e)}function a(e){return!(!e||!e[p])}function s(e){return!(!e||!e[f])}function u(e){return!(!e||!e[h])}function c(e){return s(e)||u(e)}function l(e){return!(!e||!e[d])}t(r,n),t(o,n),t(i,n),n.isIterable=a,n.isKeyed=s,n.isIndexed=u,n.isAssociative=c,n.isOrdered=l,n.Keyed=r,n.Indexed=o,n.Set=i;var p="@@__IMMUTABLE_ITERABLE__@@",f="@@__IMMUTABLE_KEYED__@@",h="@@__IMMUTABLE_INDEXED__@@",d="@@__IMMUTABLE_ORDERED__@@",m=5,v=1<>>0;if(""+n!==t||4294967295===n)return NaN;t=n}return t<0?C(e)+t:t}function O(){return!0}function A(e,t,n){return(0===e||void 0!==n&&e<=-n)&&(void 0===t||void 0!==n&&t>=n)}function T(e,t){return P(e,t,0)}function j(e,t){return P(e,t,t)}function P(e,t,n){return void 0===e?n:e<0?Math.max(0,t+e):void 0===t?e:Math.min(t,e)}var I=0,M=1,N=2,R="function"==typeof Symbol&&Symbol.iterator,D="@@iterator",L=R||D;function U(e){this.next=e}function q(e,t,n,r){var o=0===e?t:1===e?n:[t,n];return r?r.value=o:r={value:o,done:!1},r}function F(){return{value:void 0,done:!0}}function B(e){return!!H(e)}function z(e){return e&&"function"==typeof e.next}function V(e){var t=H(e);return t&&t.call(e)}function H(e){var t=e&&(R&&e[R]||e[D]);if("function"==typeof t)return t}function W(e){return e&&"number"==typeof e.length}function J(e){return null==e?ie():a(e)?e.toSeq():function(e){var t=ue(e)||"object"==typeof e&&new te(e);if(!t)throw new TypeError("Expected Array or iterable object of values, or keyed object: "+e);return t}(e)}function K(e){return null==e?ie().toKeyedSeq():a(e)?s(e)?e.toSeq():e.fromEntrySeq():ae(e)}function Y(e){return null==e?ie():a(e)?s(e)?e.entrySeq():e.toIndexedSeq():se(e)}function $(e){return(null==e?ie():a(e)?s(e)?e.entrySeq():e:se(e)).toSetSeq()}U.prototype.toString=function(){return"[Iterator]"},U.KEYS=I,U.VALUES=M,U.ENTRIES=N,U.prototype.inspect=U.prototype.toSource=function(){return this.toString()},U.prototype[L]=function(){return this},t(J,n),J.of=function(){return J(arguments)},J.prototype.toSeq=function(){return this},J.prototype.toString=function(){return this.__toString("Seq {","}")},J.prototype.cacheResult=function(){return!this._cache&&this.__iterateUncached&&(this._cache=this.entrySeq().toArray(),this.size=this._cache.length),this},J.prototype.__iterate=function(e,t){return ce(this,e,t,!0)},J.prototype.__iterator=function(e,t){return le(this,e,t,!0)},t(K,J),K.prototype.toKeyedSeq=function(){return this},t(Y,J),Y.of=function(){return Y(arguments)},Y.prototype.toIndexedSeq=function(){return this},Y.prototype.toString=function(){return this.__toString("Seq [","]")},Y.prototype.__iterate=function(e,t){return ce(this,e,t,!1)},Y.prototype.__iterator=function(e,t){return le(this,e,t,!1)},t($,J),$.of=function(){return $(arguments)},$.prototype.toSetSeq=function(){return this},J.isSeq=oe,J.Keyed=K,J.Set=$,J.Indexed=Y;var G,Z,X,Q="@@__IMMUTABLE_SEQ__@@";function ee(e){this._array=e,this.size=e.length}function te(e){var t=Object.keys(e);this._object=e,this._keys=t,this.size=t.length}function ne(e){this._iterable=e,this.size=e.length||e.size}function re(e){this._iterator=e,this._iteratorCache=[]}function oe(e){return!(!e||!e[Q])}function ie(){return G||(G=new ee([]))}function ae(e){var t=Array.isArray(e)?new ee(e).fromEntrySeq():z(e)?new re(e).fromEntrySeq():B(e)?new ne(e).fromEntrySeq():"object"==typeof e?new te(e):void 0;if(!t)throw new TypeError("Expected Array or iterable object of [k, v] entries, or keyed object: "+e);return t}function se(e){var t=ue(e);if(!t)throw new TypeError("Expected Array or iterable object of values: "+e);return t}function ue(e){return W(e)?new ee(e):z(e)?new re(e):B(e)?new ne(e):void 0}function ce(e,t,n,r){var o=e._cache;if(o){for(var i=o.length-1,a=0;a<=i;a++){var s=o[n?i-a:a];if(!1===t(s[1],r?s[0]:a,e))return a+1}return a}return e.__iterateUncached(t,n)}function le(e,t,n,r){var o=e._cache;if(o){var i=o.length-1,a=0;return new U(function(){var e=o[n?i-a:a];return a++>i?{value:void 0,done:!0}:q(t,r?e[0]:a-1,e[1])})}return e.__iteratorUncached(t,n)}function pe(e,t){return t?function e(t,n,r,o){return Array.isArray(n)?t.call(o,r,Y(n).map(function(r,o){return e(t,r,o,n)})):he(n)?t.call(o,r,K(n).map(function(r,o){return e(t,r,o,n)})):n}(t,e,"",{"":e}):fe(e)}function fe(e){return Array.isArray(e)?Y(e).map(fe).toList():he(e)?K(e).map(fe).toMap():e}function he(e){return e&&(e.constructor===Object||void 0===e.constructor)}function de(e,t){if(e===t||e!=e&&t!=t)return!0;if(!e||!t)return!1;if("function"==typeof e.valueOf&&"function"==typeof t.valueOf){if((e=e.valueOf())===(t=t.valueOf())||e!=e&&t!=t)return!0;if(!e||!t)return!1}return!("function"!=typeof e.equals||"function"!=typeof t.equals||!e.equals(t))}function me(e,t){if(e===t)return!0;if(!a(t)||void 0!==e.size&&void 0!==t.size&&e.size!==t.size||void 0!==e.__hash&&void 0!==t.__hash&&e.__hash!==t.__hash||s(e)!==s(t)||u(e)!==u(t)||l(e)!==l(t))return!1;if(0===e.size&&0===t.size)return!0;var n=!c(e);if(l(e)){var r=e.entries();return t.every(function(e,t){var o=r.next().value;return o&&de(o[1],e)&&(n||de(o[0],t))})&&r.next().done}var o=!1;if(void 0===e.size)if(void 0===t.size)"function"==typeof e.cacheResult&&e.cacheResult();else{o=!0;var i=e;e=t,t=i}var p=!0,f=t.__iterate(function(t,r){if(n?!e.has(t):o?!de(t,e.get(r,y)):!de(e.get(r,y),t))return p=!1,!1});return p&&e.size===f}function ve(e,t){if(!(this instanceof ve))return new ve(e,t);if(this._value=e,this.size=void 0===t?1/0:Math.max(0,t),0===this.size){if(Z)return Z;Z=this}}function ge(e,t){if(!e)throw new Error(t)}function ye(e,t,n){if(!(this instanceof ye))return new ye(e,t,n);if(ge(0!==n,"Cannot step a Range by 0"),e=e||0,void 0===t&&(t=1/0),n=void 0===n?1:Math.abs(n),tr?{value:void 0,done:!0}:q(e,o,n[t?r-o++:o++])})},t(te,K),te.prototype.get=function(e,t){return void 0===t||this.has(e)?this._object[e]:t},te.prototype.has=function(e){return this._object.hasOwnProperty(e)},te.prototype.__iterate=function(e,t){for(var n=this._object,r=this._keys,o=r.length-1,i=0;i<=o;i++){var a=r[t?o-i:i];if(!1===e(n[a],a,this))return i+1}return i},te.prototype.__iterator=function(e,t){var n=this._object,r=this._keys,o=r.length-1,i=0;return new U(function(){var a=r[t?o-i:i];return i++>o?{value:void 0,done:!0}:q(e,a,n[a])})},te.prototype[d]=!0,t(ne,Y),ne.prototype.__iterateUncached=function(e,t){if(t)return this.cacheResult().__iterate(e,t);var n=V(this._iterable),r=0;if(z(n))for(var o;!(o=n.next()).done&&!1!==e(o.value,r++,this););return r},ne.prototype.__iteratorUncached=function(e,t){if(t)return this.cacheResult().__iterator(e,t);var n=V(this._iterable);if(!z(n))return new U(F);var r=0;return new U(function(){var t=n.next();return t.done?t:q(e,r++,t.value)})},t(re,Y),re.prototype.__iterateUncached=function(e,t){if(t)return this.cacheResult().__iterate(e,t);for(var n,r=this._iterator,o=this._iteratorCache,i=0;i=r.length){var t=n.next();if(t.done)return t;r[o]=t.value}return q(e,o,r[o++])})},t(ve,Y),ve.prototype.toString=function(){return 0===this.size?"Repeat []":"Repeat [ "+this._value+" "+this.size+" times ]"},ve.prototype.get=function(e,t){return this.has(e)?this._value:t},ve.prototype.includes=function(e){return de(this._value,e)},ve.prototype.slice=function(e,t){var n=this.size;return A(e,t,n)?this:new ve(this._value,j(t,n)-T(e,n))},ve.prototype.reverse=function(){return this},ve.prototype.indexOf=function(e){return de(this._value,e)?0:-1},ve.prototype.lastIndexOf=function(e){return de(this._value,e)?this.size:-1},ve.prototype.__iterate=function(e,t){for(var n=0;n=0&&t=0&&nn?{value:void 0,done:!0}:q(e,i++,a)})},ye.prototype.equals=function(e){return e instanceof ye?this._start===e._start&&this._end===e._end&&this._step===e._step:me(this,e)},t(be,n),t(_e,be),t(we,be),t(xe,be),be.Keyed=_e,be.Indexed=we,be.Set=xe;var Ee="function"==typeof Math.imul&&-2===Math.imul(4294967295,2)?Math.imul:function(e,t){var n=65535&(e|=0),r=65535&(t|=0);return n*r+((e>>>16)*r+n*(t>>>16)<<16>>>0)|0};function Se(e){return e>>>1&1073741824|3221225471&e}function Ce(e){if(!1===e||null==e)return 0;if("function"==typeof e.valueOf&&(!1===(e=e.valueOf())||null==e))return 0;if(!0===e)return 1;var t=typeof e;if("number"===t){if(e!=e||e===1/0)return 0;var n=0|e;for(n!==e&&(n^=4294967295*e);e>4294967295;)n^=e/=4294967295;return Se(n)}if("string"===t)return e.length>Me?function(e){var t=De[e];return void 0===t&&(t=ke(e),Re===Ne&&(Re=0,De={}),Re++,De[e]=t),t}(e):ke(e);if("function"==typeof e.hashCode)return e.hashCode();if("object"===t)return function(e){var t;if(je&&void 0!==(t=Oe.get(e)))return t;if(void 0!==(t=e[Ie]))return t;if(!Te){if(void 0!==(t=e.propertyIsEnumerable&&e.propertyIsEnumerable[Ie]))return t;if(void 0!==(t=function(e){if(e&&e.nodeType>0)switch(e.nodeType){case 1:return e.uniqueID;case 9:return e.documentElement&&e.documentElement.uniqueID}}(e)))return t}if(t=++Pe,1073741824&Pe&&(Pe=0),je)Oe.set(e,t);else{if(void 0!==Ae&&!1===Ae(e))throw new Error("Non-extensible objects are not allowed as keys.");if(Te)Object.defineProperty(e,Ie,{enumerable:!1,configurable:!1,writable:!1,value:t});else if(void 0!==e.propertyIsEnumerable&&e.propertyIsEnumerable===e.constructor.prototype.propertyIsEnumerable)e.propertyIsEnumerable=function(){return this.constructor.prototype.propertyIsEnumerable.apply(this,arguments)},e.propertyIsEnumerable[Ie]=t;else{if(void 0===e.nodeType)throw new Error("Unable to set a non-enumerable property on object.");e[Ie]=t}}return t}(e);if("function"==typeof e.toString)return ke(e.toString());throw new Error("Value type "+t+" cannot be hashed.")}function ke(e){for(var t=0,n=0;n=t.length)throw new Error("Missing value for key: "+t[n]);e.set(t[n],t[n+1])}})},Ue.prototype.toString=function(){return this.__toString("Map {","}")},Ue.prototype.get=function(e,t){return this._root?this._root.get(0,void 0,e,t):t},Ue.prototype.set=function(e,t){return Qe(this,e,t)},Ue.prototype.setIn=function(e,t){return this.updateIn(e,y,function(){return t})},Ue.prototype.remove=function(e){return Qe(this,e,y)},Ue.prototype.deleteIn=function(e){return this.updateIn(e,function(){return y})},Ue.prototype.update=function(e,t,n){return 1===arguments.length?e(this):this.updateIn([e],t,n)},Ue.prototype.updateIn=function(e,t,n){n||(n=t,t=void 0);var r=function e(t,n,r,o){var i=t===y,a=n.next();if(a.done){var s=i?r:t,u=o(s);return u===s?t:u}ge(i||t&&t.set,"invalid keyPath");var c=a.value,l=i?y:t.get(c,y),p=e(l,n,r,o);return p===l?t:p===y?t.remove(c):(i?Xe():t).set(c,p)}(this,rn(e),t,n);return r===y?void 0:r},Ue.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._root=null,this.__hash=void 0,this.__altered=!0,this):Xe()},Ue.prototype.merge=function(){return rt(this,void 0,arguments)},Ue.prototype.mergeWith=function(t){var n=e.call(arguments,1);return rt(this,t,n)},Ue.prototype.mergeIn=function(t){var n=e.call(arguments,1);return this.updateIn(t,Xe(),function(e){return"function"==typeof e.merge?e.merge.apply(e,n):n[n.length-1]})},Ue.prototype.mergeDeep=function(){return rt(this,ot,arguments)},Ue.prototype.mergeDeepWith=function(t){var n=e.call(arguments,1);return rt(this,it(t),n)},Ue.prototype.mergeDeepIn=function(t){var n=e.call(arguments,1);return this.updateIn(t,Xe(),function(e){return"function"==typeof e.mergeDeep?e.mergeDeep.apply(e,n):n[n.length-1]})},Ue.prototype.sort=function(e){return Tt(Jt(this,e))},Ue.prototype.sortBy=function(e,t){return Tt(Jt(this,t,e))},Ue.prototype.withMutations=function(e){var t=this.asMutable();return e(t),t.wasAltered()?t.__ensureOwner(this.__ownerID):this},Ue.prototype.asMutable=function(){return this.__ownerID?this:this.__ensureOwner(new E)},Ue.prototype.asImmutable=function(){return this.__ensureOwner()},Ue.prototype.wasAltered=function(){return this.__altered},Ue.prototype.__iterator=function(e,t){return new Ye(this,e,t)},Ue.prototype.__iterate=function(e,t){var n=this,r=0;return this._root&&this._root.iterate(function(t){return r++,e(t[1],t[0],n)},t),r},Ue.prototype.__ensureOwner=function(e){return e===this.__ownerID?this:e?Ze(this.size,this._root,e,this.__hash):(this.__ownerID=e,this.__altered=!1,this)},Ue.isMap=qe;var Fe,Be="@@__IMMUTABLE_MAP__@@",ze=Ue.prototype;function Ve(e,t){this.ownerID=e,this.entries=t}function He(e,t,n){this.ownerID=e,this.bitmap=t,this.nodes=n}function We(e,t,n){this.ownerID=e,this.count=t,this.nodes=n}function Je(e,t,n){this.ownerID=e,this.keyHash=t,this.entries=n}function Ke(e,t,n){this.ownerID=e,this.keyHash=t,this.entry=n}function Ye(e,t,n){this._type=t,this._reverse=n,this._stack=e._root&&Ge(e._root)}function $e(e,t){return q(e,t[0],t[1])}function Ge(e,t){return{node:e,index:0,__prev:t}}function Ze(e,t,n,r){var o=Object.create(ze);return o.size=e,o._root=t,o.__ownerID=n,o.__hash=r,o.__altered=!1,o}function Xe(){return Fe||(Fe=Ze(0))}function Qe(e,t,n){var r,o;if(e._root){var i=w(b),a=w(_);if(r=et(e._root,e.__ownerID,0,void 0,t,n,i,a),!a.value)return e;o=e.size+(i.value?n===y?-1:1:0)}else{if(n===y)return e;o=1,r=new Ve(e.__ownerID,[[t,n]])}return e.__ownerID?(e.size=o,e._root=r,e.__hash=void 0,e.__altered=!0,e):r?Ze(o,r):Xe()}function et(e,t,n,r,o,i,a,s){return e?e.update(t,n,r,o,i,a,s):i===y?e:(x(s),x(a),new Ke(t,r,[o,i]))}function tt(e){return e.constructor===Ke||e.constructor===Je}function nt(e,t,n,r,o){if(e.keyHash===r)return new Je(t,r,[e.entry,o]);var i,a=(0===n?e.keyHash:e.keyHash>>>n)&g,s=(0===n?r:r>>>n)&g;return new He(t,1<>1&1431655765))+(e>>2&858993459))+(e>>4)&252645135,e+=e>>8,127&(e+=e>>16)}function ut(e,t,n,r){var o=r?e:S(e);return o[t]=n,o}ze[Be]=!0,ze.delete=ze.remove,ze.removeIn=ze.deleteIn,Ve.prototype.get=function(e,t,n,r){for(var o=this.entries,i=0,a=o.length;i=ct)return function(e,t,n,r){e||(e=new E);for(var o=new Ke(e,Ce(n),[n,r]),i=0;i>>e)&g),i=this.bitmap;return 0==(i&o)?r:this.nodes[st(i&o-1)].get(e+m,t,n,r)},He.prototype.update=function(e,t,n,r,o,i,a){void 0===n&&(n=Ce(r));var s=(0===t?n:n>>>t)&g,u=1<=lt)return function(e,t,n,r,o){for(var i=0,a=new Array(v),s=0;0!==n;s++,n>>>=1)a[s]=1&n?t[i++]:void 0;return a[r]=o,new We(e,i+1,a)}(e,f,c,s,d);if(l&&!d&&2===f.length&&tt(f[1^p]))return f[1^p];if(l&&d&&1===f.length&&tt(d))return d;var b=e&&e===this.ownerID,_=l?d?c:c^u:c|u,w=l?d?ut(f,p,d,b):function(e,t,n){var r=e.length-1;if(n&&t===r)return e.pop(),e;for(var o=new Array(r),i=0,a=0;a>>e)&g,i=this.nodes[o];return i?i.get(e+m,t,n,r):r},We.prototype.update=function(e,t,n,r,o,i,a){void 0===n&&(n=Ce(r));var s=(0===t?n:n>>>t)&g,u=o===y,c=this.nodes,l=c[s];if(u&&!l)return this;var p=et(l,e,t+m,n,r,o,i,a);if(p===l)return this;var f=this.count;if(l){if(!p&&--f0&&r=0&&e=e.size||t<0)return e.withMutations(function(e){t<0?kt(e,t).set(0,n):kt(e,0,t+1).set(t,n)});t+=e._origin;var r=e._tail,o=e._root,i=w(_);return t>=At(e._capacity)?r=Et(r,e.__ownerID,0,t,n,i):o=Et(o,e.__ownerID,e._level,t,n,i),i.value?e.__ownerID?(e._root=o,e._tail=r,e.__hash=void 0,e.__altered=!0,e):wt(e._origin,e._capacity,e._level,o,r):e}(this,e,t)},ft.prototype.remove=function(e){return this.has(e)?0===e?this.shift():e===this.size-1?this.pop():this.splice(e,1):this},ft.prototype.insert=function(e,t){return this.splice(e,0,t)},ft.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=this._origin=this._capacity=0,this._level=m,this._root=this._tail=null,this.__hash=void 0,this.__altered=!0,this):xt()},ft.prototype.push=function(){var e=arguments,t=this.size;return this.withMutations(function(n){kt(n,0,t+e.length);for(var r=0;r>>t&g;if(r>=this.array.length)return new vt([],e);var o,i=0===r;if(t>0){var a=this.array[r];if((o=a&&a.removeBefore(e,t-m,n))===a&&i)return this}if(i&&!o)return this;var s=St(this,e);if(!i)for(var u=0;u>>t&g;if(o>=this.array.length)return this;if(t>0){var i=this.array[o];if((r=i&&i.removeAfter(e,t-m,n))===i&&o===this.array.length-1)return this}var a=St(this,e);return a.array.splice(o+1),r&&(a.array[o]=r),a};var gt,yt,bt={};function _t(e,t){var n=e._origin,r=e._capacity,o=At(r),i=e._tail;return a(e._root,e._level,0);function a(e,s,u){return 0===s?function(e,a){var s=a===o?i&&i.array:e&&e.array,u=a>n?0:n-a,c=r-a;return c>v&&(c=v),function(){if(u===c)return bt;var e=t?--c:u++;return s&&s[e]}}(e,u):function(e,o,i){var s,u=e&&e.array,c=i>n?0:n-i>>o,l=1+(r-i>>o);return l>v&&(l=v),function(){for(;;){if(s){var e=s();if(e!==bt)return e;s=null}if(c===l)return bt;var n=t?--l:c++;s=a(u&&u[n],o-m,i+(n<>>n&g,u=e&&s0){var c=e&&e.array[s],l=Et(c,t,n-m,r,o,i);return l===c?e:((a=St(e,t)).array[s]=l,a)}return u&&e.array[s]===o?e:(x(i),a=St(e,t),void 0===o&&s===a.array.length-1?a.array.pop():a.array[s]=o,a)}function St(e,t){return t&&e&&t===e.ownerID?e:new vt(e?e.array.slice():[],t)}function Ct(e,t){if(t>=At(e._capacity))return e._tail;if(t<1<0;)n=n.array[t>>>r&g],r-=m;return n}}function kt(e,t,n){void 0!==t&&(t|=0),void 0!==n&&(n|=0);var r=e.__ownerID||new E,o=e._origin,i=e._capacity,a=o+t,s=void 0===n?i:n<0?i+n:o+n;if(a===o&&s===i)return e;if(a>=s)return e.clear();for(var u=e._level,c=e._root,l=0;a+l<0;)c=new vt(c&&c.array.length?[void 0,c]:[],r),l+=1<<(u+=m);l&&(a+=l,o+=l,s+=l,i+=l);for(var p=At(i),f=At(s);f>=1<p?new vt([],r):h;if(h&&f>p&&am;y-=m){var b=p>>>y&g;v=v.array[b]=St(v.array[b],r)}v.array[p>>>m&g]=h}if(s=f)a-=f,s-=f,u=m,c=null,d=d&&d.removeBefore(r,0,a);else if(a>o||f>>u&g;if(_!==f>>>u&g)break;_&&(l+=(1<o&&(c=c.removeBefore(r,u,a-l)),c&&fi&&(i=c.size),a(u)||(c=c.map(function(e){return pe(e)})),r.push(c)}return i>e.size&&(e=e.setSize(i)),at(e,t,r)}function At(e){return e>>m<=v&&a.size>=2*i.size?(r=(o=a.filter(function(e,t){return void 0!==e&&s!==t})).toKeyedSeq().map(function(e){return e[0]}).flip().toMap(),e.__ownerID&&(r.__ownerID=o.__ownerID=e.__ownerID)):(r=i.remove(t),o=s===a.size-1?a.pop():a.set(s,void 0))}else if(u){if(n===a.get(s)[1])return e;r=i,o=a.set(s,[t,n])}else r=i.set(t,a.size),o=a.set(a.size,[t,n]);return e.__ownerID?(e.size=r.size,e._map=r,e._list=o,e.__hash=void 0,e):Pt(r,o)}function Nt(e,t){this._iter=e,this._useKeys=t,this.size=e.size}function Rt(e){this._iter=e,this.size=e.size}function Dt(e){this._iter=e,this.size=e.size}function Lt(e){this._iter=e,this.size=e.size}function Ut(e){var t=en(e);return t._iter=e,t.size=e.size,t.flip=function(){return e},t.reverse=function(){var t=e.reverse.apply(this);return t.flip=function(){return e.reverse()},t},t.has=function(t){return e.includes(t)},t.includes=function(t){return e.has(t)},t.cacheResult=tn,t.__iterateUncached=function(t,n){var r=this;return e.__iterate(function(e,n){return!1!==t(n,e,r)},n)},t.__iteratorUncached=function(t,n){if(t===N){var r=e.__iterator(t,n);return new U(function(){var e=r.next();if(!e.done){var t=e.value[0];e.value[0]=e.value[1],e.value[1]=t}return e})}return e.__iterator(t===M?I:M,n)},t}function qt(e,t,n){var r=en(e);return r.size=e.size,r.has=function(t){return e.has(t)},r.get=function(r,o){var i=e.get(r,y);return i===y?o:t.call(n,i,r,e)},r.__iterateUncached=function(r,o){var i=this;return e.__iterate(function(e,o,a){return!1!==r(t.call(n,e,o,a),o,i)},o)},r.__iteratorUncached=function(r,o){var i=e.__iterator(N,o);return new U(function(){var o=i.next();if(o.done)return o;var a=o.value,s=a[0];return q(r,s,t.call(n,a[1],s,e),o)})},r}function Ft(e,t){var n=en(e);return n._iter=e,n.size=e.size,n.reverse=function(){return e},e.flip&&(n.flip=function(){var t=Ut(e);return t.reverse=function(){return e.flip()},t}),n.get=function(n,r){return e.get(t?n:-1-n,r)},n.has=function(n){return e.has(t?n:-1-n)},n.includes=function(t){return e.includes(t)},n.cacheResult=tn,n.__iterate=function(t,n){var r=this;return e.__iterate(function(e,n){return t(e,n,r)},!n)},n.__iterator=function(t,n){return e.__iterator(t,!n)},n}function Bt(e,t,n,r){var o=en(e);return r&&(o.has=function(r){var o=e.get(r,y);return o!==y&&!!t.call(n,o,r,e)},o.get=function(r,o){var i=e.get(r,y);return i!==y&&t.call(n,i,r,e)?i:o}),o.__iterateUncached=function(o,i){var a=this,s=0;return e.__iterate(function(e,i,u){if(t.call(n,e,i,u))return s++,o(e,r?i:s-1,a)},i),s},o.__iteratorUncached=function(o,i){var a=e.__iterator(N,i),s=0;return new U(function(){for(;;){var i=a.next();if(i.done)return i;var u=i.value,c=u[0],l=u[1];if(t.call(n,l,c,e))return q(o,r?c:s++,l,i)}})},o}function zt(e,t,n,r){var o=e.size;if(void 0!==t&&(t|=0),void 0!==n&&(n===1/0?n=o:n|=0),A(t,n,o))return e;var i=T(t,o),a=j(n,o);if(i!=i||a!=a)return zt(e.toSeq().cacheResult(),t,n,r);var s,u=a-i;u==u&&(s=u<0?0:u);var c=en(e);return c.size=0===s?s:e.size&&s||void 0,!r&&oe(e)&&s>=0&&(c.get=function(t,n){return(t=k(this,t))>=0&&ts)return{value:void 0,done:!0};var e=o.next();return r||t===M?e:q(t,u-1,t===I?void 0:e.value[1],e)})},c}function Vt(e,t,n,r){var o=en(e);return o.__iterateUncached=function(o,i){var a=this;if(i)return this.cacheResult().__iterate(o,i);var s=!0,u=0;return e.__iterate(function(e,i,c){if(!s||!(s=t.call(n,e,i,c)))return u++,o(e,r?i:u-1,a)}),u},o.__iteratorUncached=function(o,i){var a=this;if(i)return this.cacheResult().__iterator(o,i);var s=e.__iterator(N,i),u=!0,c=0;return new U(function(){var e,i,l;do{if((e=s.next()).done)return r||o===M?e:q(o,c++,o===I?void 0:e.value[1],e);var p=e.value;i=p[0],l=p[1],u&&(u=t.call(n,l,i,a))}while(u);return o===N?e:q(o,i,l,e)})},o}function Ht(e,t){var n=s(e),o=[e].concat(t).map(function(e){return a(e)?n&&(e=r(e)):e=n?ae(e):se(Array.isArray(e)?e:[e]),e}).filter(function(e){return 0!==e.size});if(0===o.length)return e;if(1===o.length){var i=o[0];if(i===e||n&&s(i)||u(e)&&u(i))return i}var c=new ee(o);return n?c=c.toKeyedSeq():u(e)||(c=c.toSetSeq()),(c=c.flatten(!0)).size=o.reduce(function(e,t){if(void 0!==e){var n=t.size;if(void 0!==n)return e+n}},0),c}function Wt(e,t,n){var r=en(e);return r.__iterateUncached=function(r,o){var i=0,s=!1;return function e(u,c){var l=this;u.__iterate(function(o,u){return(!t||c0}function $t(e,t,r){var o=en(e);return o.size=new ee(r).map(function(e){return e.size}).min(),o.__iterate=function(e,t){for(var n,r=this.__iterator(M,t),o=0;!(n=r.next()).done&&!1!==e(n.value,o++,this););return o},o.__iteratorUncached=function(e,o){var i=r.map(function(e){return e=n(e),V(o?e.reverse():e)}),a=0,s=!1;return new U(function(){var n;return s||(n=i.map(function(e){return e.next()}),s=n.some(function(e){return e.done})),s?{value:void 0,done:!0}:q(e,a++,t.apply(null,n.map(function(e){return e.value})))})},o}function Gt(e,t){return oe(e)?t:e.constructor(t)}function Zt(e){if(e!==Object(e))throw new TypeError("Expected [K, V] tuple: "+e)}function Xt(e){return Le(e.size),C(e)}function Qt(e){return s(e)?r:u(e)?o:i}function en(e){return Object.create((s(e)?K:u(e)?Y:$).prototype)}function tn(){return this._iter.cacheResult?(this._iter.cacheResult(),this.size=this._iter.size,this):J.prototype.cacheResult.call(this)}function nn(e,t){return e>t?1:e=0;n--)t={value:arguments[n],next:t};return this.__ownerID?(this.size=e,this._head=t,this.__hash=void 0,this.__altered=!0,this):An(e,t)},En.prototype.pushAll=function(e){if(0===(e=o(e)).size)return this;Le(e.size);var t=this.size,n=this._head;return e.reverse().forEach(function(e){t++,n={value:e,next:n}}),this.__ownerID?(this.size=t,this._head=n,this.__hash=void 0,this.__altered=!0,this):An(t,n)},En.prototype.pop=function(){return this.slice(1)},En.prototype.unshift=function(){return this.push.apply(this,arguments)},En.prototype.unshiftAll=function(e){return this.pushAll(e)},En.prototype.shift=function(){return this.pop.apply(this,arguments)},En.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._head=void 0,this.__hash=void 0,this.__altered=!0,this):Tn()},En.prototype.slice=function(e,t){if(A(e,t,this.size))return this;var n=T(e,this.size);if(j(t,this.size)!==this.size)return we.prototype.slice.call(this,e,t);for(var r=this.size-n,o=this._head;n--;)o=o.next;return this.__ownerID?(this.size=r,this._head=o,this.__hash=void 0,this.__altered=!0,this):An(r,o)},En.prototype.__ensureOwner=function(e){return e===this.__ownerID?this:e?An(this.size,this._head,e,this.__hash):(this.__ownerID=e,this.__altered=!1,this)},En.prototype.__iterate=function(e,t){if(t)return this.reverse().__iterate(e);for(var n=0,r=this._head;r&&!1!==e(r.value,n++,this);)r=r.next;return n},En.prototype.__iterator=function(e,t){if(t)return this.reverse().__iterator(e);var n=0,r=this._head;return new U(function(){if(r){var t=r.value;return r=r.next,q(e,n++,t)}return{value:void 0,done:!0}})},En.isStack=Sn;var Cn,kn="@@__IMMUTABLE_STACK__@@",On=En.prototype;function An(e,t,n,r){var o=Object.create(On);return o.size=e,o._head=t,o.__ownerID=n,o.__hash=r,o.__altered=!1,o}function Tn(){return Cn||(Cn=An(0))}function jn(e,t){var n=function(n){e.prototype[n]=t[n]};return Object.keys(t).forEach(n),Object.getOwnPropertySymbols&&Object.getOwnPropertySymbols(t).forEach(n),e}On[kn]=!0,On.withMutations=ze.withMutations,On.asMutable=ze.asMutable,On.asImmutable=ze.asImmutable,On.wasAltered=ze.wasAltered,n.Iterator=U,jn(n,{toArray:function(){Le(this.size);var e=new Array(this.size||0);return this.valueSeq().__iterate(function(t,n){e[n]=t}),e},toIndexedSeq:function(){return new Rt(this)},toJS:function(){return this.toSeq().map(function(e){return e&&"function"==typeof e.toJS?e.toJS():e}).__toJS()},toJSON:function(){return this.toSeq().map(function(e){return e&&"function"==typeof e.toJSON?e.toJSON():e}).__toJS()},toKeyedSeq:function(){return new Nt(this,!0)},toMap:function(){return Ue(this.toKeyedSeq())},toObject:function(){Le(this.size);var e={};return this.__iterate(function(t,n){e[n]=t}),e},toOrderedMap:function(){return Tt(this.toKeyedSeq())},toOrderedSet:function(){return gn(s(this)?this.valueSeq():this)},toSet:function(){return cn(s(this)?this.valueSeq():this)},toSetSeq:function(){return new Dt(this)},toSeq:function(){return u(this)?this.toIndexedSeq():s(this)?this.toKeyedSeq():this.toSetSeq()},toStack:function(){return En(s(this)?this.valueSeq():this)},toList:function(){return ft(s(this)?this.valueSeq():this)},toString:function(){return"[Iterable]"},__toString:function(e,t){return 0===this.size?e+t:e+" "+this.toSeq().map(this.__toStringMapper).join(", ")+" "+t},concat:function(){var t=e.call(arguments,0);return Gt(this,Ht(this,t))},includes:function(e){return this.some(function(t){return de(t,e)})},entries:function(){return this.__iterator(N)},every:function(e,t){Le(this.size);var n=!0;return this.__iterate(function(r,o,i){if(!e.call(t,r,o,i))return n=!1,!1}),n},filter:function(e,t){return Gt(this,Bt(this,e,t,!0))},find:function(e,t,n){var r=this.findEntry(e,t);return r?r[1]:n},forEach:function(e,t){return Le(this.size),this.__iterate(t?e.bind(t):e)},join:function(e){Le(this.size),e=void 0!==e?""+e:",";var t="",n=!0;return this.__iterate(function(r){n?n=!1:t+=e,t+=null!=r?r.toString():""}),t},keys:function(){return this.__iterator(I)},map:function(e,t){return Gt(this,qt(this,e,t))},reduce:function(e,t,n){var r,o;return Le(this.size),arguments.length<2?o=!0:r=t,this.__iterate(function(t,i,a){o?(o=!1,r=t):r=e.call(n,r,t,i,a)}),r},reduceRight:function(e,t,n){var r=this.toKeyedSeq().reverse();return r.reduce.apply(r,arguments)},reverse:function(){return Gt(this,Ft(this,!0))},slice:function(e,t){return Gt(this,zt(this,e,t,!0))},some:function(e,t){return!this.every(Rn(e),t)},sort:function(e){return Gt(this,Jt(this,e))},values:function(){return this.__iterator(M)},butLast:function(){return this.slice(0,-1)},isEmpty:function(){return void 0!==this.size?0===this.size:!this.some(function(){return!0})},count:function(e,t){return C(e?this.toSeq().filter(e,t):this)},countBy:function(e,t){return function(e,t,n){var r=Ue().asMutable();return e.__iterate(function(o,i){r.update(t.call(n,o,i,e),0,function(e){return e+1})}),r.asImmutable()}(this,e,t)},equals:function(e){return me(this,e)},entrySeq:function(){var e=this;if(e._cache)return new ee(e._cache);var t=e.toSeq().map(Nn).toIndexedSeq();return t.fromEntrySeq=function(){return e.toSeq()},t},filterNot:function(e,t){return this.filter(Rn(e),t)},findEntry:function(e,t,n){var r=n;return this.__iterate(function(n,o,i){if(e.call(t,n,o,i))return r=[o,n],!1}),r},findKey:function(e,t){var n=this.findEntry(e,t);return n&&n[0]},findLast:function(e,t,n){return this.toKeyedSeq().reverse().find(e,t,n)},findLastEntry:function(e,t,n){return this.toKeyedSeq().reverse().findEntry(e,t,n)},findLastKey:function(e,t){return this.toKeyedSeq().reverse().findKey(e,t)},first:function(){return this.find(O)},flatMap:function(e,t){return Gt(this,function(e,t,n){var r=Qt(e);return e.toSeq().map(function(o,i){return r(t.call(n,o,i,e))}).flatten(!0)}(this,e,t))},flatten:function(e){return Gt(this,Wt(this,e,!0))},fromEntrySeq:function(){return new Lt(this)},get:function(e,t){return this.find(function(t,n){return de(n,e)},void 0,t)},getIn:function(e,t){for(var n,r=this,o=rn(e);!(n=o.next()).done;){var i=n.value;if((r=r&&r.get?r.get(i,y):y)===y)return t}return r},groupBy:function(e,t){return function(e,t,n){var r=s(e),o=(l(e)?Tt():Ue()).asMutable();e.__iterate(function(i,a){o.update(t.call(n,i,a,e),function(e){return(e=e||[]).push(r?[a,i]:i),e})});var i=Qt(e);return o.map(function(t){return Gt(e,i(t))})}(this,e,t)},has:function(e){return this.get(e,y)!==y},hasIn:function(e){return this.getIn(e,y)!==y},isSubset:function(e){return e="function"==typeof e.includes?e:n(e),this.every(function(t){return e.includes(t)})},isSuperset:function(e){return(e="function"==typeof e.isSubset?e:n(e)).isSubset(this)},keyOf:function(e){return this.findKey(function(t){return de(t,e)})},keySeq:function(){return this.toSeq().map(Mn).toIndexedSeq()},last:function(){return this.toSeq().reverse().first()},lastKeyOf:function(e){return this.toKeyedSeq().reverse().keyOf(e)},max:function(e){return Kt(this,e)},maxBy:function(e,t){return Kt(this,t,e)},min:function(e){return Kt(this,e?Dn(e):qn)},minBy:function(e,t){return Kt(this,t?Dn(t):qn,e)},rest:function(){return this.slice(1)},skip:function(e){return this.slice(Math.max(0,e))},skipLast:function(e){return Gt(this,this.toSeq().reverse().skip(e).reverse())},skipWhile:function(e,t){return Gt(this,Vt(this,e,t,!0))},skipUntil:function(e,t){return this.skipWhile(Rn(e),t)},sortBy:function(e,t){return Gt(this,Jt(this,t,e))},take:function(e){return this.slice(0,Math.max(0,e))},takeLast:function(e){return Gt(this,this.toSeq().reverse().take(e).reverse())},takeWhile:function(e,t){return Gt(this,function(e,t,n){var r=en(e);return r.__iterateUncached=function(r,o){var i=this;if(o)return this.cacheResult().__iterate(r,o);var a=0;return e.__iterate(function(e,o,s){return t.call(n,e,o,s)&&++a&&r(e,o,i)}),a},r.__iteratorUncached=function(r,o){var i=this;if(o)return this.cacheResult().__iterator(r,o);var a=e.__iterator(N,o),s=!0;return new U(function(){if(!s)return{value:void 0,done:!0};var e=a.next();if(e.done)return e;var o=e.value,u=o[0],c=o[1];return t.call(n,c,u,i)?r===N?e:q(r,u,c,e):(s=!1,{value:void 0,done:!0})})},r}(this,e,t))},takeUntil:function(e,t){return this.takeWhile(Rn(e),t)},valueSeq:function(){return this.toIndexedSeq()},hashCode:function(){return this.__hash||(this.__hash=function(e){if(e.size===1/0)return 0;var t=l(e),n=s(e),r=t?1:0;return function(e,t){return t=Ee(t,3432918353),t=Ee(t<<15|t>>>-15,461845907),t=Ee(t<<13|t>>>-13,5),t=Ee((t=(t+3864292196|0)^e)^t>>>16,2246822507),t=Se((t=Ee(t^t>>>13,3266489909))^t>>>16)}(e.__iterate(n?t?function(e,t){r=31*r+Fn(Ce(e),Ce(t))|0}:function(e,t){r=r+Fn(Ce(e),Ce(t))|0}:t?function(e){r=31*r+Ce(e)|0}:function(e){r=r+Ce(e)|0}),r)}(this))}});var Pn=n.prototype;Pn[p]=!0,Pn[L]=Pn.values,Pn.__toJS=Pn.toArray,Pn.__toStringMapper=Ln,Pn.inspect=Pn.toSource=function(){return this.toString()},Pn.chain=Pn.flatMap,Pn.contains=Pn.includes,jn(r,{flip:function(){return Gt(this,Ut(this))},mapEntries:function(e,t){var n=this,r=0;return Gt(this,this.toSeq().map(function(o,i){return e.call(t,[i,o],r++,n)}).fromEntrySeq())},mapKeys:function(e,t){var n=this;return Gt(this,this.toSeq().flip().map(function(r,o){return e.call(t,r,o,n)}).flip())}});var In=r.prototype;function Mn(e,t){return t}function Nn(e,t){return[t,e]}function Rn(e){return function(){return!e.apply(this,arguments)}}function Dn(e){return function(){return-e.apply(this,arguments)}}function Ln(e){return"string"==typeof e?JSON.stringify(e):String(e)}function Un(){return S(arguments)}function qn(e,t){return et?-1:0}function Fn(e,t){return e^t+2654435769+(e<<6)+(e>>2)|0}return In[f]=!0,In[L]=Pn.entries,In.__toJS=Pn.toObject,In.__toStringMapper=function(e,t){return JSON.stringify(t)+": "+Ln(e)},jn(o,{toKeyedSeq:function(){return new Nt(this,!1)},filter:function(e,t){return Gt(this,Bt(this,e,t,!1))},findIndex:function(e,t){var n=this.findEntry(e,t);return n?n[0]:-1},indexOf:function(e){var t=this.keyOf(e);return void 0===t?-1:t},lastIndexOf:function(e){var t=this.lastKeyOf(e);return void 0===t?-1:t},reverse:function(){return Gt(this,Ft(this,!1))},slice:function(e,t){return Gt(this,zt(this,e,t,!1))},splice:function(e,t){var n=arguments.length;if(t=Math.max(0|t,0),0===n||2===n&&!t)return this;e=T(e,e<0?this.count():this.size);var r=this.slice(0,e);return Gt(this,1===n?r:r.concat(S(arguments,2),this.slice(e+t)))},findLastIndex:function(e,t){var n=this.findLastEntry(e,t);return n?n[0]:-1},first:function(){return this.get(0)},flatten:function(e){return Gt(this,Wt(this,e,!1))},get:function(e,t){return(e=k(this,e))<0||this.size===1/0||void 0!==this.size&&e>this.size?t:this.find(function(t,n){return n===e},void 0,t)},has:function(e){return(e=k(this,e))>=0&&(void 0!==this.size?this.size===1/0||e5e3)return e.textContent;return function(e){for(var n,r,o,i,a,s=e.textContent,u=0,c=s[0],l=1,p=e.innerHTML="",f=0;r=n,n=f<7&&"\\"==n?1:l;){if(l=c,c=s[++u],i=p.length>1,!l||f>8&&"\n"==l||[/\S/.test(l),1,1,!/[$\w]/.test(l),("/"==n||"\n"==n)&&i,'"'==n&&i,"'"==n&&i,s[u-4]+r+n=="--\x3e",r+n=="*/"][f])for(p&&(e.appendChild(a=t.createElement("span")).setAttribute("style",["color: #555; font-weight: bold;","","","color: #555;",""][f?f<3?2:f>6?4:f>3?3:+/^(a(bstract|lias|nd|rguments|rray|s(m|sert)?|uto)|b(ase|egin|ool(ean)?|reak|yte)|c(ase|atch|har|hecked|lass|lone|ompl|onst|ontinue)|de(bugger|cimal|clare|f(ault|er)?|init|l(egate|ete)?)|do|double|e(cho|ls?if|lse(if)?|nd|nsure|num|vent|x(cept|ec|p(licit|ort)|te(nds|nsion|rn)))|f(allthrough|alse|inal(ly)?|ixed|loat|or(each)?|riend|rom|unc(tion)?)|global|goto|guard|i(f|mp(lements|licit|ort)|n(it|clude(_once)?|line|out|stanceof|t(erface|ernal)?)?|s)|l(ambda|et|ock|ong)|m(icrolight|odule|utable)|NaN|n(amespace|ative|ext|ew|il|ot|ull)|o(bject|perator|r|ut|verride)|p(ackage|arams|rivate|rotected|rotocol|ublic)|r(aise|e(adonly|do|f|gister|peat|quire(_once)?|scue|strict|try|turn))|s(byte|ealed|elf|hort|igned|izeof|tatic|tring|truct|ubscript|uper|ynchronized|witch)|t(emplate|hen|his|hrows?|ransient|rue|ry|ype(alias|def|id|name|of))|u(n(checked|def(ined)?|ion|less|signed|til)|se|sing)|v(ar|irtual|oid|olatile)|w(char_t|hen|here|hile|ith)|xor|yield)$/.test(p):0]),a.appendChild(t.createTextNode(p))),o=f&&f<7?f:o,p="",f=11;![1,/[\/{}[(\-+*=<>:;|\\.,?!&@~]/.test(l),/[\])]/.test(l),/[$\w]/.test(l),"/"==l&&o<2&&"<"!=n,'"'==l,"'"==l,l+c+s[u+1]+s[u+2]=="\x3c!--",l+c=="/*",l+c=="//","#"==l][--f];);p+=l}}(e)}function Q(e){var t;if([/filename\*=[^']+'\w*'"([^"]+)";?/i,/filename\*=[^']+'\w*'([^;]+);?/i,/filename="([^;]*);?"/i,/filename=([^;]*);?/i].some(function(n){return null!==(t=n.exec(e))}),null!==t&&t.length>1)try{return decodeURIComponent(t[1])}catch(e){console.error(e)}return null}function ee(e){return t=e.replace(/\.[^.\/]*$/,""),b()(g()(t));var t}var te=function(e,t){if(e>t)return"Value must be less than Maximum"},ne=function(e,t){if(et)return"Value must be less than MaxLength"},pe=function(e,t){if(e.length2&&void 0!==arguments[2]?arguments[2]:{},r=n.isOAS3,o=void 0!==r&&r,i=n.bypassRequiredCheck,a=void 0!==i&&i,s=[],u=e.get("required"),c=Object(P.a)(e,{isOAS3:o}),p=c.schema,h=c.parameterContentMediaType;if(!p)return s;var m=p.get("required"),v=p.get("maximum"),g=p.get("minimum"),y=p.get("type"),b=p.get("format"),_=p.get("maxLength"),w=p.get("minLength"),x=p.get("pattern");if(y&&(u||m||t)){var E="string"===y&&t,S="array"===y&&l()(t)&&t.length,C="array"===y&&d.a.List.isList(t)&&t.count(),k="array"===y&&"string"==typeof t&&t,O="file"===y&&t instanceof A.a.File,T="boolean"===y&&(t||!1===t),j="number"===y&&(t||0===t),I="integer"===y&&(t||0===t),M="object"===y&&"object"===f()(t)&&null!==t,N="object"===y&&"string"==typeof t&&t,R=[E,S,C,k,O,T,j,I,M,N],D=R.some(function(e){return!!e});if((u||m)&&!D&&!a)return s.push("Required field is not provided"),s;if("object"===y&&"string"==typeof t&&(null===h||"application/json"===h))try{JSON.parse(t)}catch(e){return s.push("Parameter string value must be valid JSON"),s}if(x){var L=fe(t,x);L&&s.push(L)}if(_||0===_){var U=le(t,_);U&&s.push(U)}if(w){var q=pe(t,w);q&&s.push(q)}if(v||0===v){var F=te(t,v);F&&s.push(F)}if(g||0===g){var B=ne(t,g);B&&s.push(B)}if("string"===y){var z;if(!(z="date-time"===b?ue(t):"uuid"===b?ce(t):se(t)))return s;s.push(z)}else if("boolean"===y){var V=ae(t);if(!V)return s;s.push(V)}else if("number"===y){var H=re(t);if(!H)return s;s.push(H)}else if("integer"===y){var W=oe(t);if(!W)return s;s.push(W)}else if("array"===y){var J;if(!C||!t.count())return s;J=p.getIn(["items","type"]),t.forEach(function(e,t){var n;"number"===J?n=re(e):"integer"===J?n=oe(e):"string"===J&&(n=se(e)),n&&s.push({index:t,error:n})})}else if("file"===y){var K=ie(t);if(!K)return s;s.push(K)}}return s},de=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"",n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};if(/xml/.test(t)){if(!e.xml||!e.xml.name){if(e.xml=e.xml||{},!e.$$ref)return e.type||e.items||e.properties||e.additionalProperties?'\n\x3c!-- XML example cannot be generated; root element name is undefined --\x3e':null;var r=e.$$ref.match(/\S*\/(\S+)$/);e.xml.name=r[1]}return Object(k.memoizedCreateXMLExample)(e,n)}var i=Object(k.memoizedSampleFromSchema)(e,n);return"object"===f()(i)?o()(i,null,2):i},me=function(){var e={},t=A.a.location.search;if(!t)return{};if(""!=t){var n=t.substr(1).split("&");for(var r in n)n.hasOwnProperty(r)&&(r=n[r].split("="),e[decodeURIComponent(r[0])]=r[1]&&decodeURIComponent(r[1])||"")}return e},ve=function(t){return(t instanceof e?t:new e(t.toString(),"utf-8")).toString("base64")},ge={operationsSorter:{alpha:function(e,t){return e.get("path").localeCompare(t.get("path"))},method:function(e,t){return e.get("method").localeCompare(t.get("method"))}},tagsSorter:{alpha:function(e,t){return e.localeCompare(t)}}},ye=function(e){var t=[];for(var n in e){var r=e[n];void 0!==r&&""!==r&&t.push([n,"=",encodeURIComponent(r).replace(/%20/g,"+")].join(""))}return t.join("&")},be=function(e,t,n){return!!E()(n,function(n){return C()(e[n],t[n])})};function _e(e){return"string"!=typeof e||""===e?"":Object(m.sanitizeUrl)(e)}function we(e){if(!d.a.OrderedMap.isOrderedMap(e))return null;if(!e.size)return null;var t=e.find(function(e,t){return t.startsWith("2")&&u()(e.get("content")||{}).length>0}),n=e.get("default")||d.a.OrderedMap(),r=(n.get("content")||d.a.OrderedMap()).keySeq().toJS().length?n:null;return t||r}var xe=function(e){return"string"==typeof e||e instanceof String?e.trim().replace(/\s/g,"%20"):""},Ee=function(e){return j()(xe(e).replace(/%20/g,"_"))},Se=function(e){return e.filter(function(e,t){return/^x-/.test(t)})},Ce=function(e){return e.filter(function(e,t){return/^pattern|maxLength|minLength|maximum|minimum/.test(t)})};function ke(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:function(){return!0};if("object"!==f()(e)||l()(e)||null===e||!t)return e;var r=a()({},e);return u()(r).forEach(function(e){e===t&&n(r[e],e)?delete r[e]:r[e]=ke(r[e],t,n)}),r}function Oe(e){if("string"==typeof e)return e;if(e&&e.toJS&&(e=e.toJS()),"object"===f()(e)&&null!==e)try{return o()(e,null,2)}catch(t){return String(e)}return null==e?"":e.toString()}function Ae(e){return"number"==typeof e?e.toString():e}function Te(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=t.returnAll,r=void 0!==n&&n,o=t.allowHashes,i=void 0===o||o;if(!d.a.Map.isMap(e))throw new Error("paramToIdentifier: received a non-Im.Map parameter as input");var a=e.get("name"),s=e.get("in"),u=[];return e&&e.hashCode&&s&&a&&i&&u.push("".concat(s,".").concat(a,".hash-").concat(e.hashCode())),s&&a&&u.push("".concat(s,".").concat(a)),u.push(a),r?u:u[0]||""}function je(e,t){return Te(e,{returnAll:!0}).map(function(e){return t[e]}).filter(function(e){return void 0!==e})[0]}function Pe(){return Me(M()(32).toString("base64"))}function Ie(e){return Me(R()("sha256").update(e).digest("base64"))}function Me(e){return e.replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,"")}}).call(this,n(64).Buffer)},function(e,t){e.exports=function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}},function(e,t,n){var r=n(54);function o(e,t){for(var n=0;n1?t-1:0),o=1;o2?n-2:0),i=2;i>",i={listOf:function(e){return c(e,"List",r.List.isList)},mapOf:function(e,t){return l(e,t,"Map",r.Map.isMap)},orderedMapOf:function(e,t){return l(e,t,"OrderedMap",r.OrderedMap.isOrderedMap)},setOf:function(e){return c(e,"Set",r.Set.isSet)},orderedSetOf:function(e){return c(e,"OrderedSet",r.OrderedSet.isOrderedSet)},stackOf:function(e){return c(e,"Stack",r.Stack.isStack)},iterableOf:function(e){return c(e,"Iterable",r.Iterable.isIterable)},recordOf:function(e){return s(function(t,n,o,i,s){for(var u=arguments.length,c=Array(u>5?u-5:0),l=5;l6?u-6:0),l=6;l5?c-5:0),p=5;p5?i-5:0),s=5;s key("+l[p]+")"].concat(a));if(h instanceof Error)return h}})).apply(void 0,i);var u})}function p(e){var t=void 0===arguments[1]?"Iterable":arguments[1],n=void 0===arguments[2]?r.Iterable.isIterable:arguments[2];return s(function(r,o,i,s,u){for(var c=arguments.length,l=Array(c>5?c-5:0),p=5;p4)}function u(e){var t=e.get("swagger");return"string"==typeof t&&t.startsWith("2.0")}function c(e){return function(t,n){return function(r){return n&&n.specSelectors&&n.specSelectors.specJson?s(n.specSelectors.specJson())?a.a.createElement(e,o()({},r,n,{Ori:t})):a.a.createElement(t,r):(console.warn("OAS3 wrapper: couldn't get spec"),null)}}}},function(e,t,n){"use strict"; +/* +object-assign +(c) Sindre Sorhus +@license MIT +*/var r=Object.getOwnPropertySymbols,o=Object.prototype.hasOwnProperty,i=Object.prototype.propertyIsEnumerable;function a(e){if(null==e)throw new TypeError("Object.assign cannot be called with null or undefined");return Object(e)}e.exports=function(){try{if(!Object.assign)return!1;var e=new String("abc");if(e[5]="de","5"===Object.getOwnPropertyNames(e)[0])return!1;for(var t={},n=0;n<10;n++)t["_"+String.fromCharCode(n)]=n;if("0123456789"!==Object.getOwnPropertyNames(t).map(function(e){return t[e]}).join(""))return!1;var r={};return"abcdefghijklmnopqrst".split("").forEach(function(e){r[e]=e}),"abcdefghijklmnopqrst"===Object.keys(Object.assign({},r)).join("")}catch(e){return!1}}()?Object.assign:function(e,t){for(var n,s,u=a(e),c=1;c0){var o=n.map(function(e){return console.error(e),e.line=e.fullPath?g(y,e.fullPath):null,e.path=e.fullPath?e.fullPath.join("."):null,e.level="error",e.type="thrown",e.source="resolver",A()(e,"message",{enumerable:!0,value:e.message}),e});i.newThrownErrBatch(o)}return r.updateResolved(t)})}},_e=[],we=V()(k()(S.a.mark(function e(){var t,n,r,o,i,a,s,u,c,l,p,f,h,d,m,v,g;return S.a.wrap(function(e){for(;;)switch(e.prev=e.next){case 0:if(t=_e.system){e.next=4;break}return console.error("debResolveSubtrees: don't have a system to operate on, aborting."),e.abrupt("return");case 4:if(n=t.errActions,r=t.errSelectors,o=t.fn,i=o.resolveSubtree,a=o.AST,s=void 0===a?{}:a,u=t.specSelectors,c=t.specActions,i){e.next=8;break}return console.error("Error: Swagger-Client did not provide a `resolveSubtree` method, doing nothing."),e.abrupt("return");case 8:return l=s.getLineNumberForPath?s.getLineNumberForPath:function(){},p=u.specStr(),f=t.getConfigs(),h=f.modelPropertyMacro,d=f.parameterMacro,m=f.requestInterceptor,v=f.responseInterceptor,e.prev=11,e.next=14,_e.reduce(function(){var e=k()(S.a.mark(function e(t,o){var a,s,c,f,g,y,b;return S.a.wrap(function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,t;case 2:return a=e.sent,s=a.resultMap,c=a.specWithCurrentSubtrees,e.next=7,i(c,o,{baseDoc:u.url(),modelPropertyMacro:h,parameterMacro:d,requestInterceptor:m,responseInterceptor:v});case 7:return f=e.sent,g=f.errors,y=f.spec,r.allErrors().size&&n.clearBy(function(e){return"thrown"!==e.get("type")||"resolver"!==e.get("source")||!e.get("fullPath").every(function(e,t){return e===o[t]||void 0===o[t]})}),j()(g)&&g.length>0&&(b=g.map(function(e){return e.line=e.fullPath?l(p,e.fullPath):null,e.path=e.fullPath?e.fullPath.join("."):null,e.level="error",e.type="thrown",e.source="resolver",A()(e,"message",{enumerable:!0,value:e.message}),e}),n.newThrownErrBatch(b)),W()(s,o,y),W()(c,o,y),e.abrupt("return",{resultMap:s,specWithCurrentSubtrees:c});case 15:case"end":return e.stop()}},e)}));return function(t,n){return e.apply(this,arguments)}}(),x.a.resolve({resultMap:(u.specResolvedSubtree([])||Object(R.Map)()).toJS(),specWithCurrentSubtrees:u.specJson().toJS()}));case 14:g=e.sent,delete _e.system,_e=[],e.next=22;break;case 19:e.prev=19,e.t0=e.catch(11),console.error(e.t0);case 22:c.updateResolvedSubtree([],g.resultMap);case 23:case"end":return e.stop()}},e,null,[[11,19]])})),35),xe=function(e){return function(t){_e.map(function(e){return e.join("@@")}).indexOf(e.join("@@"))>-1||(_e.push(e),_e.system=t,we())}};function Ee(e,t,n,r,o){return{type:X,payload:{path:e,value:r,paramName:t,paramIn:n,isXml:o}}}function Se(e,t,n,r){return{type:X,payload:{path:e,param:t,value:n,isXml:r}}}var Ce=function(e,t){return{type:le,payload:{path:e,value:t}}},ke=function(){return{type:le,payload:{path:[],value:Object(R.Map)()}}},Oe=function(e,t){return{type:ee,payload:{pathMethod:e,isOAS3:t}}},Ae=function(e,t,n,r){return{type:Q,payload:{pathMethod:e,paramName:t,paramIn:n,includeEmptyValue:r}}};function Te(e){return{type:se,payload:{pathMethod:e}}}function je(e,t){return{type:ue,payload:{path:e,value:t,key:"consumes_value"}}}function Pe(e,t){return{type:ue,payload:{path:e,value:t,key:"produces_value"}}}var Ie=function(e,t,n){return{payload:{path:e,method:t,res:n},type:te}},Me=function(e,t,n){return{payload:{path:e,method:t,req:n},type:ne}},Ne=function(e,t,n){return{payload:{path:e,method:t,req:n},type:re}},Re=function(e){return{payload:e,type:oe}},De=function(e){return function(t){var n=t.fn,r=t.specActions,o=t.specSelectors,i=t.getConfigs,a=t.oas3Selectors,s=e.pathName,u=e.method,c=e.operation,l=i(),p=l.requestInterceptor,f=l.responseInterceptor,h=c.toJS();if(c&&c.get("parameters")&&c.get("parameters").filter(function(e){return e&&!0===e.get("allowEmptyValue")}).forEach(function(t){if(o.parameterInclusionSettingFor([s,u],t.get("name"),t.get("in"))){e.parameters=e.parameters||{};var n=Object(J.C)(t,e.parameters);(!n||n&&0===n.size)&&(e.parameters[t.get("name")]="")}}),e.contextUrl=L()(o.url()).toString(),h&&h.operationId?e.operationId=h.operationId:h&&s&&u&&(e.operationId=n.opId(h,s,u)),o.isOAS3()){var d="".concat(s,":").concat(u);e.server=a.selectedServer(d)||a.selectedServer();var m=a.serverVariables({server:e.server,namespace:d}).toJS(),g=a.serverVariables({server:e.server}).toJS();e.serverVariables=_()(m).length?m:g,e.requestContentType=a.requestContentType(s,u),e.responseContentType=a.responseContentType(s,u)||"*/*";var b=a.requestBodyValue(s,u);Object(J.t)(b)?e.requestBody=JSON.parse(b):b&&b.toJS?e.requestBody=b.toJS():e.requestBody=b}var w=y()({},e);w=n.buildRequest(w),r.setRequest(e.pathName,e.method,w);e.requestInterceptor=function(t){var n=p.apply(this,[t]),o=y()({},n);return r.setMutatedRequest(e.pathName,e.method,o),n},e.responseInterceptor=f;var x=v()();return n.execute(e).then(function(t){t.duration=v()()-x,r.setResponse(e.pathName,e.method,t)}).catch(function(t){console.error(t),r.setResponse(e.pathName,e.method,{error:!0,err:q()(t)})})}},Le=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=e.path,n=e.method,r=d()(e,["path","method"]);return function(e){var o=e.fn.fetch,i=e.specSelectors,a=e.specActions,s=i.specJsonWithResolvedSubtrees().toJS(),u=i.operationScheme(t,n),c=i.contentTypeValues([t,n]).toJS(),l=c.requestContentType,p=c.responseContentType,f=/xml/i.test(l),h=i.parameterValues([t,n],f).toJS();return a.executeRequest(Y({},r,{fetch:o,spec:s,pathName:t,method:n,parameters:h,requestContentType:l,scheme:u,responseContentType:p}))}};function Ue(e,t){return{type:ie,payload:{path:e,method:t}}}function qe(e,t){return{type:ae,payload:{path:e,method:t}}}function Fe(e,t,n){return{type:pe,payload:{scheme:e,path:t,method:n}}}},function(e,t,n){var r=n(32),o=n(22),i=n(63),a=n(77),s=n(75),u=function(e,t,n){var c,l,p,f=e&u.F,h=e&u.G,d=e&u.S,m=e&u.P,v=e&u.B,g=e&u.W,y=h?o:o[t]||(o[t]={}),b=y.prototype,_=h?r:d?r[t]:(r[t]||{}).prototype;for(c in h&&(n=t),n)(l=!f&&_&&void 0!==_[c])&&s(y,c)||(p=l?_[c]:n[c],y[c]=h&&"function"!=typeof _[c]?n[c]:v&&l?i(p,r):g&&_[c]==p?function(e){var t=function(t,n,r){if(this instanceof e){switch(arguments.length){case 0:return new e;case 1:return new e(t);case 2:return new e(t,n)}return new e(t,n,r)}return e.apply(this,arguments)};return t.prototype=e.prototype,t}(p):m&&"function"==typeof p?i(Function.call,p):p,m&&((y.virtual||(y.virtual={}))[c]=p,e&u.R&&b&&!b[c]&&a(b,c,p)))};u.F=1,u.G=2,u.S=4,u.P=8,u.B=16,u.W=32,u.U=64,u.R=128,e.exports=u},function(e,t,n){"use strict";var r=n(138),o=["kind","resolve","construct","instanceOf","predicate","represent","defaultStyle","styleAliases"],i=["scalar","sequence","mapping"];e.exports=function(e,t){var n,a;if(t=t||{},Object.keys(t).forEach(function(t){if(-1===o.indexOf(t))throw new r('Unknown option "'+t+'" is met in definition of "'+e+'" YAML type.')}),this.tag=e,this.kind=t.kind||null,this.resolve=t.resolve||function(){return!0},this.construct=t.construct||function(e){return e},this.instanceOf=t.instanceOf||null,this.predicate=t.predicate||null,this.represent=t.represent||null,this.defaultStyle=t.defaultStyle||null,this.styleAliases=(n=t.styleAliases||null,a={},null!==n&&Object.keys(n).forEach(function(e){n[e].forEach(function(t){a[String(t)]=e})}),a),-1===i.indexOf(this.kind))throw new r('Unknown kind "'+this.kind+'" is specified for "'+e+'" YAML type.')}},function(e,t){var n=e.exports="undefined"!=typeof window&&window.Math==Math?window:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")();"number"==typeof __g&&(__g=n)},function(e,t,n){var r=n(197)("wks"),o=n(199),i=n(41).Symbol,a="function"==typeof i;(e.exports=function(e){return r[e]||(r[e]=a&&i[e]||(a?i:o)("Symbol."+e))}).store=r},function(e,t,n){var r=n(214)("wks"),o=n(159),i=n(32).Symbol,a="function"==typeof i;(e.exports=function(e){return r[e]||(r[e]=a&&i[e]||(a?i:o)("Symbol."+e))}).store=r},function(e,t,n){var r=n(41),o=n(72),i=n(81),a=n(97),s=n(153),u=function(e,t,n){var c,l,p,f,h=e&u.F,d=e&u.G,m=e&u.S,v=e&u.P,g=e&u.B,y=d?r:m?r[t]||(r[t]={}):(r[t]||{}).prototype,b=d?o:o[t]||(o[t]={}),_=b.prototype||(b.prototype={});for(c in d&&(n=t),n)p=((l=!h&&y&&void 0!==y[c])?y:n)[c],f=g&&l?s(p,r):v&&"function"==typeof p?s(Function.call,p):p,y&&a(y,c,p,e&u.U),b[c]!=p&&i(b,c,f),v&&_[c]!=p&&(_[c]=p)};r.core=o,u.F=1,u.G=2,u.S=4,u.P=8,u.B=16,u.W=32,u.U=64,u.R=128,e.exports=u},function(e,t){var n;n=function(){return this}();try{n=n||new Function("return this")()}catch(e){"object"==typeof window&&(n=window)}e.exports=n},function(e,t){var n=Array.isArray;e.exports=n},function(e,t,n){"use strict";var r=!("undefined"==typeof window||!window.document||!window.document.createElement),o={canUseDOM:r,canUseWorkers:"undefined"!=typeof Worker,canUseEventListeners:r&&!(!window.addEventListener&&!window.attachEvent),canUseViewport:r&&!!window.screen,isInWorker:!r};e.exports=o},function(e,t,n){"use strict";var r=Object.prototype.hasOwnProperty;function o(e,t){return!!e&&r.call(e,t)}var i=/\\([\\!"#$%&'()*+,.\/:;<=>?@[\]^_`{|}~-])/g;function a(e){return!(e>=55296&&e<=57343)&&(!(e>=64976&&e<=65007)&&(65535!=(65535&e)&&65534!=(65535&e)&&(!(e>=0&&e<=8)&&(11!==e&&(!(e>=14&&e<=31)&&(!(e>=127&&e<=159)&&!(e>1114111)))))))}function s(e){if(e>65535){var t=55296+((e-=65536)>>10),n=56320+(1023&e);return String.fromCharCode(t,n)}return String.fromCharCode(e)}var u=/&([a-z#][a-z0-9]{1,31});/gi,c=/^#((?:x[a-f0-9]{1,8}|[0-9]{1,8}))/i,l=n(463);function p(e,t){var n=0;return o(l,t)?l[t]:35===t.charCodeAt(0)&&c.test(t)&&a(n="x"===t[1].toLowerCase()?parseInt(t.slice(2),16):parseInt(t.slice(1),10))?s(n):e}var f=/[&<>"]/,h=/[&<>"]/g,d={"&":"&","<":"<",">":">",'"':"""};function m(e){return d[e]}t.assign=function(e){return[].slice.call(arguments,1).forEach(function(t){if(t){if("object"!=typeof t)throw new TypeError(t+"must be object");Object.keys(t).forEach(function(n){e[n]=t[n]})}}),e},t.isString=function(e){return"[object String]"===function(e){return Object.prototype.toString.call(e)}(e)},t.has=o,t.unescapeMd=function(e){return e.indexOf("\\")<0?e:e.replace(i,"$1")},t.isValidEntityCode=a,t.fromCodePoint=s,t.replaceEntities=function(e){return e.indexOf("&")<0?e:e.replace(u,p)},t.escapeHtml=function(e){return f.test(e)?e.replace(h,m):e}},function(e,t,n){var r=n(55),o=n(771);e.exports=function(e,t){if(null==e)return{};var n,i,a=o(e,t);if(r){var s=r(e);for(i=0;i=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(a[n]=e[n])}return a}},function(e,t){var n=e.exports="undefined"!=typeof window&&window.Math==Math?window:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")();"number"==typeof __g&&(__g=n)},function(e,t,n){var r=n(35),o=n(99),i=n(73),a=/"/g,s=function(e,t,n,r){var o=String(i(e)),s="<"+t;return""!==n&&(s+=" "+n+'="'+String(r).replace(a,""")+'"'),s+">"+o+""};e.exports=function(e,t){var n={};n[e]=t(s),r(r.P+r.F*o(function(){var t=""[e]('"');return t!==t.toLowerCase()||t.split('"').length>3}),"String",n)}},function(e,t){e.exports=function(e){return"object"==typeof e?null!==e:"function"==typeof e}},function(e,t,n){"use strict";n.r(t),n.d(t,"NEW_THROWN_ERR",function(){return i}),n.d(t,"NEW_THROWN_ERR_BATCH",function(){return a}),n.d(t,"NEW_SPEC_ERR",function(){return s}),n.d(t,"NEW_SPEC_ERR_BATCH",function(){return u}),n.d(t,"NEW_AUTH_ERR",function(){return c}),n.d(t,"CLEAR",function(){return l}),n.d(t,"CLEAR_BY",function(){return p}),n.d(t,"newThrownErr",function(){return f}),n.d(t,"newThrownErrBatch",function(){return h}),n.d(t,"newSpecErr",function(){return d}),n.d(t,"newSpecErrBatch",function(){return m}),n.d(t,"newAuthErr",function(){return v}),n.d(t,"clear",function(){return g}),n.d(t,"clearBy",function(){return y});var r=n(119),o=n.n(r),i="err_new_thrown_err",a="err_new_thrown_err_batch",s="err_new_spec_err",u="err_new_spec_err_batch",c="err_new_auth_err",l="err_clear",p="err_clear_by";function f(e){return{type:i,payload:o()(e)}}function h(e){return{type:a,payload:e}}function d(e){return{type:s,payload:e}}function m(e){return{type:u,payload:e}}function v(e){return{type:c,payload:e}}function g(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return{type:l,payload:e}}function y(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:function(){return!0};return{type:p,payload:e}}},function(e,t,n){var r=n(98);e.exports=function(e){if(!r(e))throw TypeError(e+" is not an object!");return e}},function(e,t,n){var r=n(43);e.exports=function(e){if(!r(e))throw TypeError(e+" is not an object!");return e}},function(e,t){"function"==typeof Object.create?e.exports=function(e,t){e.super_=t,e.prototype=Object.create(t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}})}:e.exports=function(e,t){e.super_=t;var n=function(){};n.prototype=t.prototype,e.prototype=new n,e.prototype.constructor=e}},function(e,t,n){var r=n(64),o=r.Buffer;function i(e,t){for(var n in e)t[n]=e[n]}function a(e,t,n){return o(e,t,n)}o.from&&o.alloc&&o.allocUnsafe&&o.allocUnsafeSlow?e.exports=r:(i(r,t),t.Buffer=a),i(o,a),a.from=function(e,t,n){if("number"==typeof e)throw new TypeError("Argument must not be a number");return o(e,t,n)},a.alloc=function(e,t,n){if("number"!=typeof e)throw new TypeError("Argument must be a number");var r=o(e);return void 0!==t?"string"==typeof n?r.fill(t,n):r.fill(t):r.fill(0),r},a.allocUnsafe=function(e){if("number"!=typeof e)throw new TypeError("Argument must be a number");return o(e)},a.allocUnsafeSlow=function(e){if("number"!=typeof e)throw new TypeError("Argument must be a number");return r.SlowBuffer(e)}},function(e,t,n){var r=n(46),o=n(349),i=n(218),a=Object.defineProperty;t.f=n(50)?Object.defineProperty:function(e,t,n){if(r(e),t=i(t,!0),r(n),o)try{return a(e,t,n)}catch(e){}if("get"in n||"set"in n)throw TypeError("Accessors not supported!");return"value"in n&&(e[t]=n.value),e}},function(e,t,n){e.exports=!n(82)(function(){return 7!=Object.defineProperty({},"a",{get:function(){return 7}}).a})},function(e,t,n){var r=n(366),o="object"==typeof self&&self&&self.Object===Object&&self,i=r||o||Function("return this")();e.exports=i},function(e,t){e.exports=function(e){var t=typeof e;return null!=e&&("object"==t||"function"==t)}},function(e,t,n){"use strict";e.exports={debugTool:null}},function(e,t,n){e.exports=n(573)},function(e,t,n){e.exports=n(770)},function(e,t,n){e.exports=function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=45)}([function(e,t){e.exports=n(17)},function(e,t){e.exports=n(14)},function(e,t){e.exports=n(26)},function(e,t){e.exports=n(16)},function(e,t){e.exports=n(123)},function(e,t){e.exports=n(60)},function(e,t){e.exports=n(61)},function(e,t){e.exports=n(55)},function(e,t){e.exports=n(2)},function(e,t){e.exports=n(54)},function(e,t){e.exports=n(94)},function(e,t){e.exports=n(28)},function(e,t){e.exports=n(930)},function(e,t){e.exports=n(12)},function(e,t){e.exports=n(192)},function(e,t){e.exports=n(936)},function(e,t){e.exports=n(93)},function(e,t){e.exports=n(193)},function(e,t){e.exports=n(939)},function(e,t){e.exports=n(943)},function(e,t){e.exports=n(944)},function(e,t){e.exports=n(92)},function(e,t){e.exports=n(13)},function(e,t){e.exports=n(146)},function(e,t){e.exports=n(4)},function(e,t){e.exports=n(5)},function(e,t){e.exports=n(946)},function(e,t){e.exports=n(421)},function(e,t){e.exports=n(949)},function(e,t){e.exports=n(52)},function(e,t){e.exports=n(64)},function(e,t){e.exports=n(283)},function(e,t){e.exports=n(272)},function(e,t){e.exports=n(950)},function(e,t){e.exports=n(145)},function(e,t){e.exports=n(951)},function(e,t){e.exports=n(959)},function(e,t){e.exports=n(960)},function(e,t){e.exports=n(961)},function(e,t){e.exports=n(40)},function(e,t){e.exports=n(264)},function(e,t){e.exports=n(37)},function(e,t){e.exports=n(964)},function(e,t){e.exports=n(965)},function(e,t){e.exports=n(966)},function(e,t,n){e.exports=n(50)},function(e,t){e.exports=n(967)},function(e,t){e.exports=n(968)},function(e,t){e.exports=n(969)},function(e,t){e.exports=n(970)},function(e,t,n){"use strict";n.r(t);var r={};n.r(r),n.d(r,"path",function(){return mn}),n.d(r,"query",function(){return vn}),n.d(r,"header",function(){return yn}),n.d(r,"cookie",function(){return bn});var o=n(9),i=n.n(o),a=n(10),s=n.n(a),u=n(5),c=n.n(u),l=n(6),p=n.n(l),f=n(7),h=n.n(f),d=n(0),m=n.n(d),v=n(8),g=n.n(v),y=(n(46),n(15)),b=n.n(y),_=n(20),w=n.n(_),x=n(12),E=n.n(x),S=n(4),C=n.n(S),k=n(22),O=n.n(k),A=n(11),T=n.n(A),j=n(2),P=n.n(j),I=n(1),M=n.n(I),N=n(17),R=n.n(N),D=(n(47),n(26)),L=n.n(D),U=n(23),q=n.n(U),F=n(31),B=n.n(F),z={serializeRes:J,mergeInQueryOrForm:Z};function V(e){return H.apply(this,arguments)}function H(){return(H=R()(C.a.mark(function e(t){var n,r,o,i,a,s=arguments;return C.a.wrap(function(e){for(;;)switch(e.prev=e.next){case 0:if(n=s.length>1&&void 0!==s[1]?s[1]:{},"object"===P()(t)&&(t=(n=t).url),n.headers=n.headers||{},z.mergeInQueryOrForm(n),n.headers&&m()(n.headers).forEach(function(e){var t=n.headers[e];"string"==typeof t&&(n.headers[e]=t.replace(/\n+/g," "))}),!n.requestInterceptor){e.next=12;break}return e.next=8,n.requestInterceptor(n);case 8:if(e.t0=e.sent,e.t0){e.next=11;break}e.t0=n;case 11:n=e.t0;case 12:return r=n.headers["content-type"]||n.headers["Content-Type"],/multipart\/form-data/i.test(r)&&(delete n.headers["content-type"],delete n.headers["Content-Type"]),e.prev=14,e.next=17,(n.userFetch||fetch)(n.url,n);case 17:return o=e.sent,e.next=20,z.serializeRes(o,t,n);case 20:if(o=e.sent,!n.responseInterceptor){e.next=28;break}return e.next=24,n.responseInterceptor(o);case 24:if(e.t1=e.sent,e.t1){e.next=27;break}e.t1=o;case 27:o=e.t1;case 28:e.next=38;break;case 30:if(e.prev=30,e.t2=e.catch(14),o){e.next=34;break}throw e.t2;case 34:throw(i=new Error(o.statusText)).statusCode=i.status=o.status,i.responseError=e.t2,i;case 38:if(o.ok){e.next=43;break}throw(a=new Error(o.statusText)).statusCode=a.status=o.status,a.response=o,a;case 43:return e.abrupt("return",o);case 44:case"end":return e.stop()}},e,null,[[14,30]])}))).apply(this,arguments)}var W=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"";return/(json|xml|yaml|text)\b/.test(e)};function J(e,t){var n=(arguments.length>2&&void 0!==arguments[2]?arguments[2]:{}).loadSpec,r=void 0!==n&&n,o={ok:e.ok,url:e.url||t,status:e.status,statusText:e.statusText,headers:K(e.headers)},i=o.headers["content-type"],a=r||W(i);return(a?e.text:e.blob||e.buffer).call(e).then(function(e){if(o.text=e,o.data=e,a)try{var t=function(e,t){return t&&(0===t.indexOf("application/json")||t.indexOf("+json")>0)?JSON.parse(e):q.a.safeLoad(e)}(e,i);o.body=t,o.obj=t}catch(e){o.parseError=e}return o})}function K(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t={};return"function"==typeof e.forEach?(e.forEach(function(e,n){void 0!==t[n]?(t[n]=M()(t[n])?t[n]:[t[n]],t[n].push(e)):t[n]=e}),t):t}function Y(e,t){return t||"undefined"==typeof navigator||(t=navigator),t&&"ReactNative"===t.product?!(!e||"object"!==P()(e)||"string"!=typeof e.uri):"undefined"!=typeof File?e instanceof File:null!==e&&"object"===P()(e)&&"function"==typeof e.pipe}function $(e,t){var n=e.collectionFormat,r=e.allowEmptyValue,o="object"===P()(e)?e.value:e;if(void 0===o&&r)return"";if(Y(o)||"boolean"==typeof o)return o;var i=encodeURIComponent;return t&&(i=B()(o)?function(e){return e}:function(e){return T()(e)}),"object"!==P()(o)||M()(o)?M()(o)?M()(o)&&!n?o.map(i).join(","):"multi"===n?o.map(i):o.map(i).join({csv:",",ssv:"%20",tsv:"%09",pipes:"|"}[n]):i(o):""}function G(e){var t=m()(e).reduce(function(t,n){var r,o=e[n],i=!!o.skipEncoding,a=i?n:encodeURIComponent(n),s=(r=o)&&"object"===P()(r)&&!M()(o);return t[a]=$(s?o:{value:o},i),t},{});return L.a.stringify(t,{encode:!1,indices:!1})||""}function Z(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=e.url,r=void 0===t?"":t,o=e.query,i=e.form;if(i){var a=m()(i).some(function(e){return Y(i[e].value)}),s=e.headers["content-type"]||e.headers["Content-Type"];if(a||/multipart\/form-data/i.test(s)){var u=n(48);e.body=new u,m()(i).forEach(function(t){e.body.append(t,$(i[t],!0))})}else e.body=G(i);delete e.form}if(o){var c=r.split("?"),l=O()(c,2),p=l[0],f=l[1],h="";if(f){var d=L.a.parse(f);m()(o).forEach(function(e){return delete d[e]}),h=L.a.stringify(d,{encode:!0})}var v=function(){for(var e=arguments.length,t=new Array(e),n=0;n0){var o=t(e,n[n.length-1],n);o&&(r=r.concat(o))}if(M()(e)){var i=e.map(function(e,r){return Ce(e,t,n.concat(r))});i&&(r=r.concat(i))}else if(Te(e)){var a=m()(e).map(function(r){return Ce(e[r],t,n.concat(r))});a&&(r=r.concat(a))}return r=Oe(r)}function ke(e){return M()(e)?e:[e]}function Oe(e){var t;return(t=[]).concat.apply(t,he()(e.map(function(e){return M()(e)?Oe(e):e})))}function Ae(e){return e.filter(function(e){return void 0!==e})}function Te(e){return e&&"object"===P()(e)}function je(e){return e&&"function"==typeof e}function Pe(e){if(Ne(e)){var t=e.op;return"add"===t||"remove"===t||"replace"===t}return!1}function Ie(e){return Pe(e)||Ne(e)&&"mutation"===e.type}function Me(e){return Ie(e)&&("add"===e.op||"replace"===e.op||"merge"===e.op||"mergeDeep"===e.op)}function Ne(e){return e&&"object"===P()(e)}function Re(e,t){try{return me.a.getValueByPointer(e,t)}catch(e){return console.error(e),{}}}var De=n(35),Le=n.n(De),Ue=n(36),qe=n(28),Fe=n.n(qe);function Be(e,t){function n(){Error.captureStackTrace?Error.captureStackTrace(this,this.constructor):this.stack=(new Error).stack;for(var e=arguments.length,n=new Array(e),r=0;r-1&&-1===We.indexOf(n)||Je.indexOf(r)>-1||Ke.some(function(e){return r.indexOf(e)>-1})}function $e(e,t){var n=e.split("#"),r=O()(n,2),o=r[0],i=r[1],a=E.a.resolve(o||"",t||"");return i?"".concat(a,"#").concat(i):a}var Ge="application/json, application/yaml",Ze=new RegExp("^([a-z]+://|//)","i"),Xe=Be("JSONRefError",function(e,t,n){this.originalError=n,ie()(this,t||{})}),Qe={},et=new Le.a,tt=[function(e){return"paths"===e[0]&&"responses"===e[3]&&"content"===e[5]&&"example"===e[7]},function(e){return"paths"===e[0]&&"requestBody"===e[3]&&"content"===e[4]&&"example"===e[6]}],nt={key:"$ref",plugin:function(e,t,n,r){var o=r.getInstance(),i=n.slice(0,-1);if(!Ye(i)&&(a=i,!tt.some(function(e){return e(a)}))){var a,s=r.getContext(n).baseDoc;if("string"!=typeof e)return new Xe("$ref: must be a string (JSON-Ref)",{$ref:e,baseDoc:s,fullPath:n});var u,c,l,p=st(e),f=p[0],h=p[1]||"";try{u=s||f?it(f,s):null}catch(t){return at(t,{pointer:h,$ref:e,basePath:u,fullPath:n})}if(function(e,t,n,r){var o=et.get(r);o||(o={},et.set(r,o));var i=function(e){if(0===e.length)return"";return"/".concat(e.map(ht).join("/"))}(n),a="".concat(t||"","#").concat(e),s=i.replace(/allOf\/\d+\/?/g,""),u=r.contextTree.get([]).baseDoc;if(t==u&&mt(s,e))return!0;var c="";if(n.some(function(e){return c="".concat(c,"/").concat(ht(e)),o[c]&&o[c].some(function(e){return mt(e,a)||mt(a,e)})}))return!0;o[s]=(o[s]||[]).concat(a)}(h,u,i,r)&&!o.useCircularStructures){var d=$e(e,u);return e===d?null:_e.replace(n,d)}if(null==u?(l=pt(h),void 0===(c=r.get(l))&&(c=new Xe("Could not resolve reference: ".concat(e),{pointer:h,$ref:e,baseDoc:s,fullPath:n}))):c=null!=(c=ut(u,h)).__value?c.__value:c.catch(function(t){throw at(t,{pointer:h,$ref:e,baseDoc:s,fullPath:n})}),c instanceof Error)return[_e.remove(n),c];var v=$e(e,u),g=_e.replace(i,c,{$$ref:v});if(u&&u!==s)return[g,_e.context(i,{baseDoc:u})];try{if(!function(e,t){var n=[e];return t.path.reduce(function(e,t){return n.push(e[t]),e[t]},e),function e(t){return _e.isObject(t)&&(n.indexOf(t)>=0||m()(t).some(function(n){return e(t[n])}))}(t.value)}(r.state,g)||o.useCircularStructures)return g}catch(e){return null}}}},rt=ie()(nt,{docCache:Qe,absoluteify:it,clearCache:function(e){void 0!==e?delete Qe[e]:m()(Qe).forEach(function(e){delete Qe[e]})},JSONRefError:Xe,wrapError:at,getDoc:ct,split:st,extractFromDoc:ut,fetchJSON:function(e){return Object(Ue.fetch)(e,{headers:{Accept:Ge},loadSpec:!0}).then(function(e){return e.text()}).then(function(e){return q.a.safeLoad(e)})},extract:lt,jsonPointerToArray:pt,unescapeJsonPointerToken:ft}),ot=rt;function it(e,t){if(!Ze.test(e)){if(!t)throw new Xe("Tried to resolve a relative URL, without having a basePath. path: '".concat(e,"' basePath: '").concat(t,"'"));return E.a.resolve(t,e)}return e}function at(e,t){var n;return n=e&&e.response&&e.response.body?"".concat(e.response.body.code," ").concat(e.response.body.message):e.message,new Xe("Could not resolve reference: ".concat(n),t,e)}function st(e){return(e+"").split("#")}function ut(e,t){var n=Qe[e];if(n&&!_e.isPromise(n))try{var r=lt(t,n);return ie()(Q.a.resolve(r),{__value:r})}catch(e){return Q.a.reject(e)}return ct(e).then(function(e){return lt(t,e)})}function ct(e){var t=Qe[e];return t?_e.isPromise(t)?t:Q.a.resolve(t):(Qe[e]=rt.fetchJSON(e).then(function(t){return Qe[e]=t,t}),Qe[e])}function lt(e,t){var n=pt(e);if(n.length<1)return t;var r=_e.getIn(t,n);if(void 0===r)throw new Xe("Could not resolve pointer: ".concat(e," does not exist in document"),{pointer:e});return r}function pt(e){if("string"!=typeof e)throw new TypeError("Expected a string, got a ".concat(P()(e)));return"/"===e[0]&&(e=e.substr(1)),""===e?[]:e.split("/").map(ft)}function ft(e){return"string"!=typeof e?e:Fe.a.unescape(e.replace(/~1/g,"/").replace(/~0/g,"~"))}function ht(e){return Fe.a.escape(e.replace(/~/g,"~0").replace(/\//g,"~1"))}var dt=function(e){return!e||"/"===e||"#"===e};function mt(e,t){if(dt(t))return!0;var n=e.charAt(t.length),r=t.slice(-1);return 0===e.indexOf(t)&&(!n||"/"===n||"#"===n)&&"#"!==r}var vt={key:"allOf",plugin:function(e,t,n,r,o){if(!o.meta||!o.meta.$$ref){var i=n.slice(0,-1);if(!Ye(i)){if(!M()(e)){var a=new TypeError("allOf must be an array");return a.fullPath=n,a}var s=!1,u=o.value;i.forEach(function(e){u&&(u=u[e])}),delete(u=ie()({},u)).allOf;var c=[];return c.push(r.replace(i,{})),e.forEach(function(e,t){if(!r.isObject(e)){if(s)return null;s=!0;var o=new TypeError("Elements in allOf must be objects");return o.fullPath=n,c.push(o)}c.push(r.mergeDeep(i,e));var a=function(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},r=n.specmap,o=n.getBaseUrlForNodePath,i=void 0===o?function(e){return r.getContext([].concat(he()(t),he()(e))).baseDoc}:o,a=n.targetKeys,s=void 0===a?["$ref","$$ref"]:a,u=[];return Ve()(e).forEach(function(){if(s.indexOf(this.key)>-1){var e=this.path,n=t.concat(this.path),o=$e(this.node,i(e));u.push(r.replace(n,o))}}),u}(e,n.slice(0,-1),{getBaseUrlForNodePath:function(e){return r.getContext([].concat(he()(n),[t],he()(e))).baseDoc},specmap:r});c.push.apply(c,he()(a))}),c.push(r.mergeDeep(i,u)),u.$$ref||c.push(r.remove([].concat(i,"$$ref"))),c}}}},gt={key:"parameters",plugin:function(e,t,n,r,o){if(M()(e)&&e.length){var i=ie()([],e),a=n.slice(0,-1),s=ie()({},_e.getIn(r.spec,a));return e.forEach(function(e,t){try{i[t].default=r.parameterMacro(s,e)}catch(e){var o=new Error(e);return o.fullPath=n,o}}),_e.replace(n,i)}return _e.replace(n,e)}},yt={key:"properties",plugin:function(e,t,n,r){var o=ie()({},e);for(var i in e)try{o[i].default=r.modelPropertyMacro(o[i])}catch(e){var a=new Error(e);return a.fullPath=n,a}return _e.replace(n,o)}};function bt(e,t){var n=m()(e);if(h.a){var r=h()(e);t&&(r=r.filter(function(t){return p()(e,t).enumerable})),n.push.apply(n,r)}return n}var _t=function(){function e(t){se()(this,e),this.root=wt(t||{})}return ce()(e,[{key:"set",value:function(e,t){var n=this.getParent(e,!0);if(n){var r=e[e.length-1],o=n.children;o[r]?xt(o[r],t,n):o[r]=wt(t,n)}else xt(this.root,t,null)}},{key:"get",value:function(e){if((e=e||[]).length<1)return this.root.value;for(var t,n,r=this.root,o=0;o1?n-1:0),o=1;o1?n-1:0),o=1;o0})}},{key:"nextPromisedPatch",value:function(){if(this.promisedPatches.length>0)return Q.a.race(this.promisedPatches.map(function(e){return e.value}))}},{key:"getPluginHistory",value:function(e){var t=this.getPluginName(e);return this.pluginHistory[t]||[]}},{key:"getPluginRunCount",value:function(e){return this.getPluginHistory(e).length}},{key:"getPluginHistoryTip",value:function(e){var t=this.getPluginHistory(e);return t&&t[t.length-1]||{}}},{key:"getPluginMutationIndex",value:function(e){var t=this.getPluginHistoryTip(e).mutationIndex;return"number"!=typeof t?-1:t}},{key:"getPluginName",value:function(e){return e.pluginName}},{key:"updatePluginHistory",value:function(e,t){var n=this.getPluginName(e);(this.pluginHistory[n]=this.pluginHistory[n]||[]).push(t)}},{key:"updatePatches",value:function(e,t){var n=this;_e.normalizeArray(e).forEach(function(e){if(e instanceof Error)n.errors.push(e);else try{if(!_e.isObject(e))return void n.debug("updatePatches","Got a non-object patch",e);if(n.showDebug&&n.allPatches.push(e),_e.isPromise(e.value))return n.promisedPatches.push(e),void n.promisedPatchThen(e);if(_e.isContextPatch(e))return void n.setContext(e.path,e.value);if(_e.isMutation(e))return void n.updateMutations(e)}catch(e){console.error(e),n.errors.push(e)}})}},{key:"updateMutations",value:function(e){"object"===P()(e.value)&&!M()(e.value)&&this.allowMetaPatches&&(e.value=ie()({},e.value));var t=_e.applyPatch(this.state,e,{allowMetaPatches:this.allowMetaPatches});t&&(this.mutations.push(e),this.state=t)}},{key:"removePromisedPatch",value:function(e){var t=this.promisedPatches.indexOf(e);t<0?this.debug("Tried to remove a promisedPatch that isn't there!"):this.promisedPatches.splice(t,1)}},{key:"promisedPatchThen",value:function(e){var t=this;return e.value=e.value.then(function(n){var r=ie()({},e,{value:n});t.removePromisedPatch(e),t.updatePatches(r)}).catch(function(n){t.removePromisedPatch(e),t.updatePatches(n)})}},{key:"getMutations",value:function(e,t){return e=e||0,"number"!=typeof t&&(t=this.mutations.length),this.mutations.slice(e,t)}},{key:"getCurrentMutations",value:function(){return this.getMutationsForPlugin(this.getCurrentPlugin())}},{key:"getMutationsForPlugin",value:function(e){var t=this.getPluginMutationIndex(e);return this.getMutations(t+1)}},{key:"getCurrentPlugin",value:function(){return this.currentPlugin}},{key:"getPatchesOfType",value:function(e,t){return e.filter(t)}},{key:"getLib",value:function(){return this.libMethods}},{key:"_get",value:function(e){return _e.getIn(this.state,e)}},{key:"_getContext",value:function(e){return this.contextTree.get(e)}},{key:"setContext",value:function(e,t){return this.contextTree.set(e,t)}},{key:"_hasRun",value:function(e){return this.getPluginRunCount(this.getCurrentPlugin())>(e||0)}},{key:"_clone",value:function(e){return JSON.parse(T()(e))}},{key:"dispatch",value:function(){var e=this,t=this,n=this.nextPlugin();if(!n){var r=this.nextPromisedPatch();if(r)return r.then(function(){return e.dispatch()}).catch(function(){return e.dispatch()});var o={spec:this.state,errors:this.errors};return this.showDebug&&(o.patches=this.allPatches),Q.a.resolve(o)}if(t.pluginCount=t.pluginCount||{},t.pluginCount[n]=(t.pluginCount[n]||0)+1,t.pluginCount[n]>100)return Q.a.resolve({spec:t.state,errors:t.errors.concat(new Error("We've reached a hard limit of ".concat(100," plugin runs")))});if(n!==this.currentPlugin&&this.promisedPatches.length){var i=this.promisedPatches.map(function(e){return e.value});return Q.a.all(i.map(function(e){return e.then(Function,Function)})).then(function(){return e.dispatch()})}return function(){t.currentPlugin=n;var e=t.getCurrentMutations(),r=t.mutations.length-1;try{if(n.isGenerator){var o=!0,i=!1,s=void 0;try{for(var u,c=te()(n(e,t.getLib()));!(o=(u=c.next()).done);o=!0){a(u.value)}}catch(e){i=!0,s=e}finally{try{o||null==c.return||c.return()}finally{if(i)throw s}}}else{a(n(e,t.getLib()))}}catch(e){console.error(e),a([ie()(re()(e),{plugin:n})])}finally{t.updatePluginHistory(n,{mutationIndex:r})}return t.dispatch()}();function a(e){e&&(e=_e.fullyNormalizeArray(e),t.updatePatches(e,n))}}}]),e}();var St={refs:ot,allOf:vt,parameters:gt,properties:yt},Ct=n(29),kt=n.n(Ct),Ot=function(e){return String.prototype.toLowerCase.call(e)},At=function(e){return e.replace(/[^\w]/gi,"_")};function Tt(e){var t=e.openapi;return!!t&&w()(t,"3")}function jt(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"",r=(arguments.length>3&&void 0!==arguments[3]?arguments[3]:{}).v2OperationIdCompatibilityMode;return e&&"object"===P()(e)?(e.operationId||"").replace(/\s/g,"").length?At(e.operationId):function(e,t){if((arguments.length>2&&void 0!==arguments[2]?arguments[2]:{}).v2OperationIdCompatibilityMode){var n="".concat(t.toLowerCase(),"_").concat(e).replace(/[\s!@#$%^&*()_+=[{\]};:<>|.\/?,\\'""-]/g,"_");return(n=n||"".concat(e.substring(1),"_").concat(t)).replace(/((_){2,})/g,"_").replace(/^(_)*/g,"").replace(/([_])*$/g,"")}return"".concat(Ot(t)).concat(At(e))}(t,n,{v2OperationIdCompatibilityMode:r}):null}function Pt(e,t){return"".concat(Ot(t),"-").concat(e)}function It(e,t){return e&&e.paths?function(e,t){return Mt(e,t,!0)||null}(e,function(e){var n=e.pathName,r=e.method,o=e.operation;if(!o||"object"!==P()(o))return!1;var i=o.operationId;return[jt(o,n,r),Pt(n,r),i].some(function(e){return e&&e===t})}):null}function Mt(e,t,n){if(!e||"object"!==P()(e)||!e.paths||"object"!==P()(e.paths))return null;var r=e.paths;for(var o in r)for(var i in r[o])if("PARAMETERS"!==i.toUpperCase()){var a=r[o][i];if(a&&"object"===P()(a)){var s={spec:e,pathName:o,method:i.toUpperCase(),operation:a},u=t(s);if(n&&u)return s}}}function Nt(e){var t=e.spec,n=t.paths,r={};if(!n||t.$$normalized)return e;for(var o in n){var i=n[o];if(kt()(i)){var a=i.parameters,s=function(e){var n=i[e];if(!kt()(n))return"continue";var s=jt(n,o,e);if(s){r[s]?r[s].push(n):r[s]=[n];var u=r[s];if(u.length>1)u.forEach(function(e,t){e.__originalOperationId=e.__originalOperationId||e.operationId,e.operationId="".concat(s).concat(t+1)});else if(void 0!==n.operationId){var c=u[0];c.__originalOperationId=c.__originalOperationId||n.operationId,c.operationId=s}}if("parameters"!==e){var l=[],p={};for(var f in t)"produces"!==f&&"consumes"!==f&&"security"!==f||(p[f]=t[f],l.push(p));if(a&&(p.parameters=a,l.push(p)),l.length)for(var h=0,d=l;h1&&void 0!==arguments[1]?arguments[1]:{},n=t.requestInterceptor,r=t.responseInterceptor,o=e.withCredentials?"include":"same-origin";return function(t){return e({url:t,loadSpec:!0,requestInterceptor:n,responseInterceptor:r,headers:{Accept:Ge},credentials:o}).then(function(e){return e.body})}}function Dt(e){var t=e.fetch,n=e.spec,r=e.url,o=e.mode,i=e.allowMetaPatches,a=void 0===i||i,s=e.pathDiscriminator,u=e.modelPropertyMacro,c=e.parameterMacro,l=e.requestInterceptor,p=e.responseInterceptor,f=e.skipNormalization,h=e.useCircularStructures,d=e.http,m=e.baseDoc;return m=m||r,d=t||d||V,n?v(n):Rt(d,{requestInterceptor:l,responseInterceptor:p})(m).then(v);function v(e){m&&(St.refs.docCache[m]=e),St.refs.fetchJSON=Rt(d,{requestInterceptor:l,responseInterceptor:p});var t,n=[St.refs];return"function"==typeof c&&n.push(St.parameters),"function"==typeof u&&n.push(St.properties),"strict"!==o&&n.push(St.allOf),(t={spec:e,context:{baseDoc:m},plugins:n,allowMetaPatches:a,pathDiscriminator:s,parameterMacro:c,modelPropertyMacro:u,useCircularStructures:h},new Et(t).dispatch()).then(f?function(){var e=R()(C.a.mark(function e(t){return C.a.wrap(function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",t);case 1:case"end":return e.stop()}},e)}));return function(t){return e.apply(this,arguments)}}():Nt)}}var Lt=n(16),Ut=n.n(Lt);function qt(e,t){var n=m()(e);if(h.a){var r=h()(e);t&&(r=r.filter(function(t){return p()(e,t).enumerable})),n.push.apply(n,r)}return n}function Ft(e){for(var t=1;t2&&void 0!==m[2]?m[2]:{},o=r.returnEntireTree,i=r.baseDoc,a=r.requestInterceptor,s=r.responseInterceptor,u=r.parameterMacro,c=r.modelPropertyMacro,l=r.useCircularStructures,p={pathDiscriminator:n,baseDoc:i,requestInterceptor:a,responseInterceptor:s,parameterMacro:u,modelPropertyMacro:c,useCircularStructures:l},f=Nt({spec:t}),h=f.spec,e.next=6,Dt(Ft({},p,{spec:h,allowMetaPatches:!0,skipNormalization:!0}));case 6:return d=e.sent,!o&&M()(n)&&n.length&&(d.spec=Ut()(d.spec,n)||null),e.abrupt("return",d);case 9:case"end":return e.stop()}},e)}))).apply(this,arguments)}var zt=n(38),Vt=n.n(zt);function Ht(e,t){var n=m()(e);if(h.a){var r=h()(e);t&&(r=r.filter(function(t){return p()(e,t).enumerable})),n.push.apply(n,r)}return n}function Wt(e){for(var t=1;t0&&void 0!==arguments[0]?arguments[0]:{};return function(t){var n=t.pathName,r=t.method,o=t.operationId;return function(t){var i=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return e.execute(Wt({spec:e.spec},Vt()(e,"requestInterceptor","responseInterceptor","userFetch"),{pathName:n,method:r,parameters:t,operationId:o},i))}}}};var $t=n(39),Gt=n.n($t),Zt=n(40),Xt=n.n(Zt),Qt=n(41),en=n.n(Qt),tn=n(19),nn=n.n(tn),rn=n(42),on=n.n(rn),an={body:function(e){var t=e.req,n=e.value;t.body=n},header:function(e){var t=e.req,n=e.parameter,r=e.value;t.headers=t.headers||{},void 0!==r&&(t.headers[n.name]=r)},query:function(e){var t=e.req,n=e.value,r=e.parameter;t.query=t.query||{},!1===n&&"boolean"===r.type&&(n="false");0===n&&["number","integer"].indexOf(r.type)>-1&&(n="0");if(n)t.query[r.name]={collectionFormat:r.collectionFormat,value:n};else if(r.allowEmptyValue&&void 0!==n){var o=r.name;t.query[o]=t.query[o]||{},t.query[o].allowEmptyValue=!0}},path:function(e){var t=e.req,n=e.value,r=e.parameter;t.url=t.url.split("{".concat(r.name,"}")).join(encodeURIComponent(n))},formData:function(e){var t=e.req,n=e.value,r=e.parameter;(n||r.allowEmptyValue)&&(t.form=t.form||{},t.form[r.name]={value:n,allowEmptyValue:r.allowEmptyValue,collectionFormat:r.collectionFormat})}};n(49);var sn=n(43),un=n.n(sn),cn=n(44),ln=function(e){return":/?#[]@!$&'()*+,;=".indexOf(e)>-1},pn=function(e){return/^[a-z0-9\-._~]+$/i.test(e)};function fn(e){var t=(arguments.length>1&&void 0!==arguments[1]?arguments[1]:{}).escape,n=arguments.length>2?arguments[2]:void 0;return"number"==typeof e&&(e=e.toString()),"string"==typeof e&&e.length&&t?n?JSON.parse(e):Object(cn.stringToCharArray)(e).map(function(e){return pn(e)?e:ln(e)&&"unsafe"===t?e:(un()(e)||[]).map(function(e){return"0".concat(e.toString(16).toUpperCase()).slice(-2)}).map(function(e){return"%".concat(e)}).join("")}).join(""):e}function hn(e){var t=e.value;return M()(t)?function(e){var t=e.key,n=e.value,r=e.style,o=e.explode,i=e.escape,a=function(e){return fn(e,{escape:i})};if("simple"===r)return n.map(function(e){return a(e)}).join(",");if("label"===r)return".".concat(n.map(function(e){return a(e)}).join("."));if("matrix"===r)return n.map(function(e){return a(e)}).reduce(function(e,n){return!e||o?"".concat(e||"",";").concat(t,"=").concat(n):"".concat(e,",").concat(n)},"");if("form"===r){var s=o?"&".concat(t,"="):",";return n.map(function(e){return a(e)}).join(s)}if("spaceDelimited"===r){var u=o?"".concat(t,"="):"";return n.map(function(e){return a(e)}).join(" ".concat(u))}if("pipeDelimited"===r){var c=o?"".concat(t,"="):"";return n.map(function(e){return a(e)}).join("|".concat(c))}}(e):"object"===P()(t)?function(e){var t=e.key,n=e.value,r=e.style,o=e.explode,i=e.escape,a=function(e){return fn(e,{escape:i})},s=m()(n);if("simple"===r)return s.reduce(function(e,t){var r=a(n[t]),i=o?"=":",",s=e?"".concat(e,","):"";return"".concat(s).concat(t).concat(i).concat(r)},"");if("label"===r)return s.reduce(function(e,t){var r=a(n[t]),i=o?"=":".",s=e?"".concat(e,"."):".";return"".concat(s).concat(t).concat(i).concat(r)},"");if("matrix"===r&&o)return s.reduce(function(e,t){var r=a(n[t]),o=e?"".concat(e,";"):";";return"".concat(o).concat(t,"=").concat(r)},"");if("matrix"===r)return s.reduce(function(e,r){var o=a(n[r]),i=e?"".concat(e,","):";".concat(t,"=");return"".concat(i).concat(r,",").concat(o)},"");if("form"===r)return s.reduce(function(e,t){var r=a(n[t]),i=e?"".concat(e).concat(o?"&":","):"",s=o?"=":",";return"".concat(i).concat(t).concat(s).concat(r)},"")}(e):function(e){var t=e.key,n=e.value,r=e.style,o=e.escape,i=function(e){return fn(e,{escape:o})};if("simple"===r)return i(n);if("label"===r)return".".concat(i(n));if("matrix"===r)return";".concat(t,"=").concat(i(n));if("form"===r)return i(n);if("deepObject"===r)return i(n)}(e)}function dn(e,t){return t.includes("application/json")?"string"==typeof e?e:T()(e):e.toString()}function mn(e){var t=e.req,n=e.value,r=e.parameter,o=r.name,i=r.style,a=r.explode,s=r.content;if(s){var u=m()(s)[0];t.url=t.url.split("{".concat(o,"}")).join(fn(dn(n,u),{escape:!0}))}else{var c=hn({key:r.name,value:n,style:i||"simple",explode:a||!1,escape:!0});t.url=t.url.split("{".concat(o,"}")).join(c)}}function vn(e){var t=e.req,n=e.value,r=e.parameter;if(t.query=t.query||{},r.content){var o=m()(r.content)[0];t.query[r.name]=dn(n,o)}else if(!1===n&&(n="false"),0===n&&(n="0"),n){var i=P()(n);if("deepObject"===r.style)m()(n).forEach(function(e){var o=n[e];t.query["".concat(r.name,"[").concat(e,"]")]={value:hn({key:e,value:o,style:"deepObject",escape:r.allowReserved?"unsafe":"reserved"}),skipEncoding:!0}});else if("object"!==i||M()(n)||"form"!==r.style&&r.style||!r.explode&&void 0!==r.explode)t.query[r.name]={value:hn({key:r.name,value:n,style:r.style||"form",explode:void 0===r.explode||r.explode,escape:r.allowReserved?"unsafe":"reserved"}),skipEncoding:!0};else{m()(n).forEach(function(e){var o=n[e];t.query[e]={value:hn({key:e,value:o,style:r.style||"form",escape:r.allowReserved?"unsafe":"reserved"}),skipEncoding:!0}})}}else if(r.allowEmptyValue&&void 0!==n){var a=r.name;t.query[a]=t.query[a]||{},t.query[a].allowEmptyValue=!0}}var gn=["accept","authorization","content-type"];function yn(e){var t=e.req,n=e.parameter,r=e.value;if(t.headers=t.headers||{},!(gn.indexOf(n.name.toLowerCase())>-1))if(n.content){var o=m()(n.content)[0];t.headers[n.name]=dn(r,o)}else void 0!==r&&(t.headers[n.name]=hn({key:n.name,value:r,style:n.style||"simple",explode:void 0!==n.explode&&n.explode,escape:!1}))}function bn(e){var t=e.req,n=e.parameter,r=e.value;t.headers=t.headers||{};var o=P()(r);if(n.content){var i=m()(n.content)[0];t.headers.Cookie="".concat(n.name,"=").concat(dn(r,i))}else if("undefined"!==o){var a="object"===o&&!M()(r)&&n.explode?"":"".concat(n.name,"=");t.headers.Cookie=a+hn({key:n.name,value:r,escape:!1,style:n.style||"form",explode:void 0!==n.explode&&n.explode})}}var _n=n(30),wn=function(e,t){var n=e.operation,r=e.requestBody,o=e.securities,i=e.spec,a=e.attachContentTypeForEmptyPayload,s=e.requestContentType;t=function(e){var t=e.request,n=e.securities,r=void 0===n?{}:n,o=e.operation,i=void 0===o?{}:o,a=e.spec,s=b()({},t),u=r.authorized,c=void 0===u?{}:u,l=i.security||a.security||[],p=c&&!!m()(c).length,f=Ut()(a,["components","securitySchemes"])||{};if(s.headers=s.headers||{},s.query=s.query||{},!m()(r).length||!p||!l||M()(i.security)&&!i.security.length)return t;return l.forEach(function(e,t){for(var n in e){var r=c[n],o=f[n];if(r){var i=r.value||r,a=o.type;if(r)if("apiKey"===a)"query"===o.in&&(s.query[o.name]=i),"header"===o.in&&(s.headers[o.name]=i),"cookie"===o.in&&(s.cookies[o.name]=i);else if("http"===a){if("basic"===o.scheme){var u=i.username,l=i.password,p=nn()("".concat(u,":").concat(l));s.headers.Authorization="Basic ".concat(p)}"bearer"===o.scheme&&(s.headers.Authorization="Bearer ".concat(i))}else if("oauth2"===a){var h=r.token||{},d=h.access_token,m=h.token_type;m&&"bearer"!==m.toLowerCase()||(m="Bearer"),s.headers.Authorization="".concat(m," ").concat(d)}}}}),s}({request:t,securities:o,operation:n,spec:i});var u=n.requestBody||{},c=m()(u.content||{}),l=s&&c.indexOf(s)>-1;if(r||a){if(s&&l)t.headers["Content-Type"]=s;else if(!s){var p=c[0];p&&(t.headers["Content-Type"]=p,s=p)}}else s&&l&&(t.headers["Content-Type"]=s);return r&&(s?c.indexOf(s)>-1&&("application/x-www-form-urlencoded"===s||0===s.indexOf("multipart/")?"object"===P()(r)?(t.form={},m()(r).forEach(function(e){var n,o,i=r[e];"undefined"!=typeof File&&(o=i instanceof File),"undefined"!=typeof Blob&&(o=o||i instanceof Blob),void 0!==_n.Buffer&&(o=o||_n.Buffer.isBuffer(i)),n="object"!==P()(i)||o?i:M()(i)?i.toString():T()(i),t.form[e]={value:n}})):t.form=r:t.body=r):t.body=r),t};var xn=function(e,t){var n=e.spec,r=e.operation,o=e.securities,i=e.requestContentType,a=e.attachContentTypeForEmptyPayload;if((t=function(e){var t=e.request,n=e.securities,r=void 0===n?{}:n,o=e.operation,i=void 0===o?{}:o,a=e.spec,s=b()({},t),u=r.authorized,c=void 0===u?{}:u,l=r.specSecurity,p=void 0===l?[]:l,f=i.security||p,h=c&&!!m()(c).length,d=a.securityDefinitions;if(s.headers=s.headers||{},s.query=s.query||{},!m()(r).length||!h||!f||M()(i.security)&&!i.security.length)return t;return f.forEach(function(e,t){for(var n in e){var r=c[n];if(r){var o=r.token,i=r.value||r,a=d[n],u=a.type,l=a["x-tokenName"]||"access_token",p=o&&o[l],f=o&&o.token_type;if(r)if("apiKey"===u){var h="query"===a.in?"query":"headers";s[h]=s[h]||{},s[h][a.name]=i}else"basic"===u?i.header?s.headers.authorization=i.header:(i.base64=nn()("".concat(i.username,":").concat(i.password)),s.headers.authorization="Basic ".concat(i.base64)):"oauth2"===u&&p&&(f=f&&"bearer"!==f.toLowerCase()?f:"Bearer",s.headers.authorization="".concat(f," ").concat(p))}}}),s}({request:t,securities:o,operation:r,spec:n})).body||t.form||a)i?t.headers["Content-Type"]=i:M()(r.consumes)?t.headers["Content-Type"]=r.consumes[0]:M()(n.consumes)?t.headers["Content-Type"]=n.consumes[0]:r.parameters&&r.parameters.filter(function(e){return"file"===e.type}).length?t.headers["Content-Type"]="multipart/form-data":r.parameters&&r.parameters.filter(function(e){return"formData"===e.in}).length&&(t.headers["Content-Type"]="application/x-www-form-urlencoded");else if(i){var s=r.parameters&&r.parameters.filter(function(e){return"body"===e.in}).length>0,u=r.parameters&&r.parameters.filter(function(e){return"formData"===e.in}).length>0;(s||u)&&(t.headers["Content-Type"]=i)}return t};function En(e,t){var n=m()(e);if(h.a){var r=h()(e);t&&(r=r.filter(function(t){return p()(e,t).enumerable})),n.push.apply(n,r)}return n}function Sn(e){for(var t=1;t-1&&(c=o,l=u[p.indexOf(o)])}return!c&&u&&u.length&&(c=u[0].url,l=u[0]),c.indexOf("{")>-1&&function(e){for(var t,n=[],r=/{([^}]+)}/g;t=r.exec(e);)n.push(t[1]);return n}(c).forEach(function(e){if(l.variables&&l.variables[e]){var t=l.variables[e],n=s[e]||t.default,r=new RegExp("{".concat(e,"}"),"g");c=c.replace(r,n)}}),function(){var e,t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"",n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"",r=E.a.parse(t),o=E.a.parse(n),i=Pn(r.protocol)||Pn(o.protocol)||"",a=r.host||o.host,s=r.pathname||"";return"/"===(e=i&&a?"".concat(i,"://").concat(a+s):s)[e.length-1]?e.slice(0,-1):e}(c,i)}(b):function(e){var t,n=e.spec,r=e.scheme,o=e.contextUrl,i=void 0===o?"":o,a=E.a.parse(i),s=M()(n.schemes)?n.schemes[0]:null,u=r||s||Pn(a.protocol)||"http",c=n.host||a.host||"",l=n.basePath||"";return"/"===(t=u&&c?"".concat(u,"://").concat(c+l):l)[t.length-1]?t.slice(0,-1):t}(b),!n)return delete g.cookies,g;g.url+=S,g.method="".concat(x).toUpperCase(),h=h||{};var C=t.paths[S]||{};o&&(g.headers.accept=o);var k=An([].concat(Cn(w.parameters)).concat(Cn(C.parameters)));k.forEach(function(e){var n,r=d[e.in];if("body"===e.in&&e.schema&&e.schema.properties&&(n=h),void 0===(n=e&&e.name&&h[e.name])?n=e&&e.name&&h["".concat(e.in,".").concat(e.name)]:On(e.name,k).length>1&&console.warn("Parameter '".concat(e.name,"' is ambiguous because the defined spec has more than one parameter with the name: '").concat(e.name,"' and the passed-in parameter values did not define an 'in' value.")),null!==n){if(void 0!==e.default&&void 0===n&&(n=e.default),void 0===n&&e.required&&!e.allowEmptyValue)throw new Error("Required parameter ".concat(e.name," is not provided"));if(v&&e.schema&&"object"===e.schema.type&&"string"==typeof n)try{n=JSON.parse(n)}catch(e){throw new Error("Could not parse object parameter value string as JSON")}r&&r({req:g,parameter:e,value:n,operation:w,spec:t})}});var O=Sn({},e,{operation:w});if((g=v?wn(O,g):xn(O,g)).cookies&&m()(g.cookies).length){var A=m()(g.cookies).reduce(function(e,t){var n=g.cookies[t];return e+(e?"&":"")+on.a.serialize(t,n)},"");g.headers.Cookie=A}return g.cookies&&delete g.cookies,Z(g),g}var Pn=function(e){return e?e.replace(/\W/g,""):null};function In(e,t){var n=m()(e);if(h.a){var r=h()(e);t&&(r=r.filter(function(t){return p()(e,t).enumerable})),n.push.apply(n,r)}return n}function Mn(e){var t=this,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};if("string"==typeof e?n.url=e:n=e,!(this instanceof Mn))return new Mn(n);b()(this,n);var r=this.resolve().then(function(){return t.disableInterfaces||b()(t,Mn.makeApisTagOperation(t)),t});return r.client=this,r}Mn.http=V,Mn.makeHttp=function(e,t,n){return n=n||function(e){return e},t=t||function(e){return e},function(r){return"string"==typeof r&&(r={url:r}),z.mergeInQueryOrForm(r),r=t(r),n(e(r))}}.bind(null,Mn.http),Mn.resolve=Dt,Mn.resolveSubtree=function(e,t){return Bt.apply(this,arguments)},Mn.execute=function(e){var t=e.http,n=e.fetch,r=e.spec,o=e.operationId,i=e.pathName,a=e.method,s=e.parameters,u=e.securities,c=Gt()(e,["http","fetch","spec","operationId","pathName","method","parameters","securities"]),l=t||n||V;i&&a&&!o&&(o=Pt(i,a));var p=Tn.buildRequest(Sn({spec:r,operationId:o,parameters:s,securities:u,http:l},c));return p.body&&(Xt()(p.body)||en()(p.body))&&(p.body=T()(p.body)),l(p)},Mn.serializeRes=J,Mn.serializeHeaders=K,Mn.clearCache=function(){St.refs.clearCache()},Mn.makeApisTagOperation=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=Yt.makeExecute(e);return{apis:Yt.mapTagOperations({v2OperationIdCompatibilityMode:e.v2OperationIdCompatibilityMode,spec:e.spec,cb:t})}},Mn.buildRequest=jn,Mn.helpers={opId:jt},Mn.prototype={http:V,execute:function(e){return this.applyDefaults(),Mn.execute(function(e){for(var t=1;t + * @license MIT + */ +var r=n(569),o=n(570),i=n(355);function a(){return u.TYPED_ARRAY_SUPPORT?2147483647:1073741823}function s(e,t){if(a()=a())throw new RangeError("Attempt to allocate Buffer larger than maximum size: 0x"+a().toString(16)+" bytes");return 0|e}function d(e,t){if(u.isBuffer(e))return e.length;if("undefined"!=typeof ArrayBuffer&&"function"==typeof ArrayBuffer.isView&&(ArrayBuffer.isView(e)||e instanceof ArrayBuffer))return e.byteLength;"string"!=typeof e&&(e=""+e);var n=e.length;if(0===n)return 0;for(var r=!1;;)switch(t){case"ascii":case"latin1":case"binary":return n;case"utf8":case"utf-8":case void 0:return B(e).length;case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return 2*n;case"hex":return n>>>1;case"base64":return z(e).length;default:if(r)return B(e).length;t=(""+t).toLowerCase(),r=!0}}function m(e,t,n){var r=!1;if((void 0===t||t<0)&&(t=0),t>this.length)return"";if((void 0===n||n>this.length)&&(n=this.length),n<=0)return"";if((n>>>=0)<=(t>>>=0))return"";for(e||(e="utf8");;)switch(e){case"hex":return j(this,t,n);case"utf8":case"utf-8":return k(this,t,n);case"ascii":return A(this,t,n);case"latin1":case"binary":return T(this,t,n);case"base64":return C(this,t,n);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return P(this,t,n);default:if(r)throw new TypeError("Unknown encoding: "+e);e=(e+"").toLowerCase(),r=!0}}function v(e,t,n){var r=e[t];e[t]=e[n],e[n]=r}function g(e,t,n,r,o){if(0===e.length)return-1;if("string"==typeof n?(r=n,n=0):n>2147483647?n=2147483647:n<-2147483648&&(n=-2147483648),n=+n,isNaN(n)&&(n=o?0:e.length-1),n<0&&(n=e.length+n),n>=e.length){if(o)return-1;n=e.length-1}else if(n<0){if(!o)return-1;n=0}if("string"==typeof t&&(t=u.from(t,r)),u.isBuffer(t))return 0===t.length?-1:y(e,t,n,r,o);if("number"==typeof t)return t&=255,u.TYPED_ARRAY_SUPPORT&&"function"==typeof Uint8Array.prototype.indexOf?o?Uint8Array.prototype.indexOf.call(e,t,n):Uint8Array.prototype.lastIndexOf.call(e,t,n):y(e,[t],n,r,o);throw new TypeError("val must be string, number or Buffer")}function y(e,t,n,r,o){var i,a=1,s=e.length,u=t.length;if(void 0!==r&&("ucs2"===(r=String(r).toLowerCase())||"ucs-2"===r||"utf16le"===r||"utf-16le"===r)){if(e.length<2||t.length<2)return-1;a=2,s/=2,u/=2,n/=2}function c(e,t){return 1===a?e[t]:e.readUInt16BE(t*a)}if(o){var l=-1;for(i=n;is&&(n=s-u),i=n;i>=0;i--){for(var p=!0,f=0;fo&&(r=o):r=o;var i=t.length;if(i%2!=0)throw new TypeError("Invalid hex string");r>i/2&&(r=i/2);for(var a=0;a>8,o=n%256,i.push(o),i.push(r);return i}(t,e.length-n),e,n,r)}function C(e,t,n){return 0===t&&n===e.length?r.fromByteArray(e):r.fromByteArray(e.slice(t,n))}function k(e,t,n){n=Math.min(e.length,n);for(var r=[],o=t;o239?4:c>223?3:c>191?2:1;if(o+p<=n)switch(p){case 1:c<128&&(l=c);break;case 2:128==(192&(i=e[o+1]))&&(u=(31&c)<<6|63&i)>127&&(l=u);break;case 3:i=e[o+1],a=e[o+2],128==(192&i)&&128==(192&a)&&(u=(15&c)<<12|(63&i)<<6|63&a)>2047&&(u<55296||u>57343)&&(l=u);break;case 4:i=e[o+1],a=e[o+2],s=e[o+3],128==(192&i)&&128==(192&a)&&128==(192&s)&&(u=(15&c)<<18|(63&i)<<12|(63&a)<<6|63&s)>65535&&u<1114112&&(l=u)}null===l?(l=65533,p=1):l>65535&&(l-=65536,r.push(l>>>10&1023|55296),l=56320|1023&l),r.push(l),o+=p}return function(e){var t=e.length;if(t<=O)return String.fromCharCode.apply(String,e);var n="",r=0;for(;r0&&(e=this.toString("hex",0,n).match(/.{2}/g).join(" "),this.length>n&&(e+=" ... ")),""},u.prototype.compare=function(e,t,n,r,o){if(!u.isBuffer(e))throw new TypeError("Argument must be a Buffer");if(void 0===t&&(t=0),void 0===n&&(n=e?e.length:0),void 0===r&&(r=0),void 0===o&&(o=this.length),t<0||n>e.length||r<0||o>this.length)throw new RangeError("out of range index");if(r>=o&&t>=n)return 0;if(r>=o)return-1;if(t>=n)return 1;if(this===e)return 0;for(var i=(o>>>=0)-(r>>>=0),a=(n>>>=0)-(t>>>=0),s=Math.min(i,a),c=this.slice(r,o),l=e.slice(t,n),p=0;po)&&(n=o),e.length>0&&(n<0||t<0)||t>this.length)throw new RangeError("Attempt to write outside buffer bounds");r||(r="utf8");for(var i=!1;;)switch(r){case"hex":return b(this,e,t,n);case"utf8":case"utf-8":return _(this,e,t,n);case"ascii":return w(this,e,t,n);case"latin1":case"binary":return x(this,e,t,n);case"base64":return E(this,e,t,n);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return S(this,e,t,n);default:if(i)throw new TypeError("Unknown encoding: "+r);r=(""+r).toLowerCase(),i=!0}},u.prototype.toJSON=function(){return{type:"Buffer",data:Array.prototype.slice.call(this._arr||this,0)}};var O=4096;function A(e,t,n){var r="";n=Math.min(e.length,n);for(var o=t;or)&&(n=r);for(var o="",i=t;in)throw new RangeError("Trying to access beyond buffer length")}function M(e,t,n,r,o,i){if(!u.isBuffer(e))throw new TypeError('"buffer" argument must be a Buffer instance');if(t>o||te.length)throw new RangeError("Index out of range")}function N(e,t,n,r){t<0&&(t=65535+t+1);for(var o=0,i=Math.min(e.length-n,2);o>>8*(r?o:1-o)}function R(e,t,n,r){t<0&&(t=4294967295+t+1);for(var o=0,i=Math.min(e.length-n,4);o>>8*(r?o:3-o)&255}function D(e,t,n,r,o,i){if(n+r>e.length)throw new RangeError("Index out of range");if(n<0)throw new RangeError("Index out of range")}function L(e,t,n,r,i){return i||D(e,0,n,4),o.write(e,t,n,r,23,4),n+4}function U(e,t,n,r,i){return i||D(e,0,n,8),o.write(e,t,n,r,52,8),n+8}u.prototype.slice=function(e,t){var n,r=this.length;if((e=~~e)<0?(e+=r)<0&&(e=0):e>r&&(e=r),(t=void 0===t?r:~~t)<0?(t+=r)<0&&(t=0):t>r&&(t=r),t0&&(o*=256);)r+=this[e+--t]*o;return r},u.prototype.readUInt8=function(e,t){return t||I(e,1,this.length),this[e]},u.prototype.readUInt16LE=function(e,t){return t||I(e,2,this.length),this[e]|this[e+1]<<8},u.prototype.readUInt16BE=function(e,t){return t||I(e,2,this.length),this[e]<<8|this[e+1]},u.prototype.readUInt32LE=function(e,t){return t||I(e,4,this.length),(this[e]|this[e+1]<<8|this[e+2]<<16)+16777216*this[e+3]},u.prototype.readUInt32BE=function(e,t){return t||I(e,4,this.length),16777216*this[e]+(this[e+1]<<16|this[e+2]<<8|this[e+3])},u.prototype.readIntLE=function(e,t,n){e|=0,t|=0,n||I(e,t,this.length);for(var r=this[e],o=1,i=0;++i=(o*=128)&&(r-=Math.pow(2,8*t)),r},u.prototype.readIntBE=function(e,t,n){e|=0,t|=0,n||I(e,t,this.length);for(var r=t,o=1,i=this[e+--r];r>0&&(o*=256);)i+=this[e+--r]*o;return i>=(o*=128)&&(i-=Math.pow(2,8*t)),i},u.prototype.readInt8=function(e,t){return t||I(e,1,this.length),128&this[e]?-1*(255-this[e]+1):this[e]},u.prototype.readInt16LE=function(e,t){t||I(e,2,this.length);var n=this[e]|this[e+1]<<8;return 32768&n?4294901760|n:n},u.prototype.readInt16BE=function(e,t){t||I(e,2,this.length);var n=this[e+1]|this[e]<<8;return 32768&n?4294901760|n:n},u.prototype.readInt32LE=function(e,t){return t||I(e,4,this.length),this[e]|this[e+1]<<8|this[e+2]<<16|this[e+3]<<24},u.prototype.readInt32BE=function(e,t){return t||I(e,4,this.length),this[e]<<24|this[e+1]<<16|this[e+2]<<8|this[e+3]},u.prototype.readFloatLE=function(e,t){return t||I(e,4,this.length),o.read(this,e,!0,23,4)},u.prototype.readFloatBE=function(e,t){return t||I(e,4,this.length),o.read(this,e,!1,23,4)},u.prototype.readDoubleLE=function(e,t){return t||I(e,8,this.length),o.read(this,e,!0,52,8)},u.prototype.readDoubleBE=function(e,t){return t||I(e,8,this.length),o.read(this,e,!1,52,8)},u.prototype.writeUIntLE=function(e,t,n,r){(e=+e,t|=0,n|=0,r)||M(this,e,t,n,Math.pow(2,8*n)-1,0);var o=1,i=0;for(this[t]=255&e;++i=0&&(i*=256);)this[t+o]=e/i&255;return t+n},u.prototype.writeUInt8=function(e,t,n){return e=+e,t|=0,n||M(this,e,t,1,255,0),u.TYPED_ARRAY_SUPPORT||(e=Math.floor(e)),this[t]=255&e,t+1},u.prototype.writeUInt16LE=function(e,t,n){return e=+e,t|=0,n||M(this,e,t,2,65535,0),u.TYPED_ARRAY_SUPPORT?(this[t]=255&e,this[t+1]=e>>>8):N(this,e,t,!0),t+2},u.prototype.writeUInt16BE=function(e,t,n){return e=+e,t|=0,n||M(this,e,t,2,65535,0),u.TYPED_ARRAY_SUPPORT?(this[t]=e>>>8,this[t+1]=255&e):N(this,e,t,!1),t+2},u.prototype.writeUInt32LE=function(e,t,n){return e=+e,t|=0,n||M(this,e,t,4,4294967295,0),u.TYPED_ARRAY_SUPPORT?(this[t+3]=e>>>24,this[t+2]=e>>>16,this[t+1]=e>>>8,this[t]=255&e):R(this,e,t,!0),t+4},u.prototype.writeUInt32BE=function(e,t,n){return e=+e,t|=0,n||M(this,e,t,4,4294967295,0),u.TYPED_ARRAY_SUPPORT?(this[t]=e>>>24,this[t+1]=e>>>16,this[t+2]=e>>>8,this[t+3]=255&e):R(this,e,t,!1),t+4},u.prototype.writeIntLE=function(e,t,n,r){if(e=+e,t|=0,!r){var o=Math.pow(2,8*n-1);M(this,e,t,n,o-1,-o)}var i=0,a=1,s=0;for(this[t]=255&e;++i>0)-s&255;return t+n},u.prototype.writeIntBE=function(e,t,n,r){if(e=+e,t|=0,!r){var o=Math.pow(2,8*n-1);M(this,e,t,n,o-1,-o)}var i=n-1,a=1,s=0;for(this[t+i]=255&e;--i>=0&&(a*=256);)e<0&&0===s&&0!==this[t+i+1]&&(s=1),this[t+i]=(e/a>>0)-s&255;return t+n},u.prototype.writeInt8=function(e,t,n){return e=+e,t|=0,n||M(this,e,t,1,127,-128),u.TYPED_ARRAY_SUPPORT||(e=Math.floor(e)),e<0&&(e=255+e+1),this[t]=255&e,t+1},u.prototype.writeInt16LE=function(e,t,n){return e=+e,t|=0,n||M(this,e,t,2,32767,-32768),u.TYPED_ARRAY_SUPPORT?(this[t]=255&e,this[t+1]=e>>>8):N(this,e,t,!0),t+2},u.prototype.writeInt16BE=function(e,t,n){return e=+e,t|=0,n||M(this,e,t,2,32767,-32768),u.TYPED_ARRAY_SUPPORT?(this[t]=e>>>8,this[t+1]=255&e):N(this,e,t,!1),t+2},u.prototype.writeInt32LE=function(e,t,n){return e=+e,t|=0,n||M(this,e,t,4,2147483647,-2147483648),u.TYPED_ARRAY_SUPPORT?(this[t]=255&e,this[t+1]=e>>>8,this[t+2]=e>>>16,this[t+3]=e>>>24):R(this,e,t,!0),t+4},u.prototype.writeInt32BE=function(e,t,n){return e=+e,t|=0,n||M(this,e,t,4,2147483647,-2147483648),e<0&&(e=4294967295+e+1),u.TYPED_ARRAY_SUPPORT?(this[t]=e>>>24,this[t+1]=e>>>16,this[t+2]=e>>>8,this[t+3]=255&e):R(this,e,t,!1),t+4},u.prototype.writeFloatLE=function(e,t,n){return L(this,e,t,!0,n)},u.prototype.writeFloatBE=function(e,t,n){return L(this,e,t,!1,n)},u.prototype.writeDoubleLE=function(e,t,n){return U(this,e,t,!0,n)},u.prototype.writeDoubleBE=function(e,t,n){return U(this,e,t,!1,n)},u.prototype.copy=function(e,t,n,r){if(n||(n=0),r||0===r||(r=this.length),t>=e.length&&(t=e.length),t||(t=0),r>0&&r=this.length)throw new RangeError("sourceStart out of bounds");if(r<0)throw new RangeError("sourceEnd out of bounds");r>this.length&&(r=this.length),e.length-t=0;--o)e[o+t]=this[o+n];else if(i<1e3||!u.TYPED_ARRAY_SUPPORT)for(o=0;o>>=0,n=void 0===n?this.length:n>>>0,e||(e=0),"number"==typeof e)for(i=t;i55295&&n<57344){if(!o){if(n>56319){(t-=3)>-1&&i.push(239,191,189);continue}if(a+1===r){(t-=3)>-1&&i.push(239,191,189);continue}o=n;continue}if(n<56320){(t-=3)>-1&&i.push(239,191,189),o=n;continue}n=65536+(o-55296<<10|n-56320)}else o&&(t-=3)>-1&&i.push(239,191,189);if(o=null,n<128){if((t-=1)<0)break;i.push(n)}else if(n<2048){if((t-=2)<0)break;i.push(n>>6|192,63&n|128)}else if(n<65536){if((t-=3)<0)break;i.push(n>>12|224,n>>6&63|128,63&n|128)}else{if(!(n<1114112))throw new Error("Invalid code point");if((t-=4)<0)break;i.push(n>>18|240,n>>12&63|128,n>>6&63|128,63&n|128)}}return i}function z(e){return r.toByteArray(function(e){if((e=function(e){return e.trim?e.trim():e.replace(/^\s+|\s+$/g,"")}(e).replace(q,"")).length<2)return"";for(;e.length%4!=0;)e+="=";return e}(e))}function V(e,t,n,r){for(var o=0;o=t.length||o>=e.length);++o)t[o+n]=e[o];return o}}).call(this,n(36))},function(e,t,n){"use strict";e.exports={current:null}},function(e,t){e.exports=function(e){return null!=e&&"object"==typeof e}},function(e,t){var n,r,o=e.exports={};function i(){throw new Error("setTimeout has not been defined")}function a(){throw new Error("clearTimeout has not been defined")}function s(e){if(n===setTimeout)return setTimeout(e,0);if((n===i||!n)&&setTimeout)return n=setTimeout,setTimeout(e,0);try{return n(e,0)}catch(t){try{return n.call(null,e,0)}catch(t){return n.call(this,e,0)}}}!function(){try{n="function"==typeof setTimeout?setTimeout:i}catch(e){n=i}try{r="function"==typeof clearTimeout?clearTimeout:a}catch(e){r=a}}();var u,c=[],l=!1,p=-1;function f(){l&&u&&(l=!1,u.length?c=u.concat(c):p=-1,c.length&&h())}function h(){if(!l){var e=s(f);l=!0;for(var t=c.length;t;){for(u=c,c=[];++p1)for(var n=1;n0&&"/"!==t[0]});function oe(e,t,n){return t=t||[],te.apply(void 0,[e].concat(u()(t))).get("parameters",Object(p.List)()).reduce(function(e,t){var r=n&&"body"===t.get("in")?t.get("value_xml"):t.get("value");return e.set(Object(l.B)(t,{allowHashes:!1}),r)},Object(p.fromJS)({}))}function ie(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"";if(p.List.isList(e))return e.some(function(e){return p.Map.isMap(e)&&e.get("in")===t})}function ae(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"";if(p.List.isList(e))return e.some(function(e){return p.Map.isMap(e)&&e.get("type")===t})}function se(e,t){t=t||[];var n=x(e).getIn(["paths"].concat(u()(t)),Object(p.fromJS)({})),r=e.getIn(["meta","paths"].concat(u()(t)),Object(p.fromJS)({})),o=ue(e,t),i=n.get("parameters")||new p.List,a=r.get("consumes_value")?r.get("consumes_value"):ae(i,"file")?"multipart/form-data":ae(i,"formData")?"application/x-www-form-urlencoded":void 0;return Object(p.fromJS)({requestContentType:a,responseContentType:o})}function ue(e,t){t=t||[];var n=x(e).getIn(["paths"].concat(u()(t)),null);if(null!==n){var r=e.getIn(["meta","paths"].concat(u()(t),["produces_value"]),null),o=n.getIn(["produces",0],null);return r||o||"application/json"}}function ce(e,t){t=t||[];var n=x(e),r=n.getIn(["paths"].concat(u()(t)),null);if(null!==r){var o=t,i=a()(o,1)[0],s=r.get("produces",null),c=n.getIn(["paths",i,"produces"],null),l=n.getIn(["produces"],null);return s||c||l}}function le(e,t){t=t||[];var n=x(e),r=n.getIn(["paths"].concat(u()(t)),null);if(null!==r){var o=t,i=a()(o,1)[0],s=r.get("consumes",null),c=n.getIn(["paths",i,"consumes"],null),l=n.getIn(["consumes"],null);return s||c||l}}var pe=function(e,t,n){var r=e.get("url").match(/^([a-z][a-z0-9+\-.]*):/),i=o()(r)?r[1]:null;return e.getIn(["scheme",t,n])||e.getIn(["scheme","_defaultScheme"])||i||""},fe=function(e,t,n){return["http","https"].indexOf(pe(e,t,n))>-1},he=function(e,t){t=t||[];var n=e.getIn(["meta","paths"].concat(u()(t),["parameters"]),Object(p.fromJS)([])),r=!0;return n.forEach(function(e){var t=e.get("errors");t&&t.count()&&(r=!1)}),r};function de(e){return p.Map.isMap(e)?e:new p.Map}},function(e,t,n){"use strict";n.r(t),n.d(t,"SHOW_AUTH_POPUP",function(){return d}),n.d(t,"AUTHORIZE",function(){return m}),n.d(t,"LOGOUT",function(){return v}),n.d(t,"PRE_AUTHORIZE_OAUTH2",function(){return g}),n.d(t,"AUTHORIZE_OAUTH2",function(){return y}),n.d(t,"VALIDATE",function(){return b}),n.d(t,"CONFIGURE_AUTH",function(){return _}),n.d(t,"showDefinitions",function(){return w}),n.d(t,"authorize",function(){return x}),n.d(t,"logout",function(){return E}),n.d(t,"preAuthorizeImplicit",function(){return S}),n.d(t,"authorizeOauth2",function(){return C}),n.d(t,"authorizePassword",function(){return k}),n.d(t,"authorizeApplication",function(){return O}),n.d(t,"authorizeAccessCodeWithFormParams",function(){return A}),n.d(t,"authorizeAccessCodeWithBasicAuthentication",function(){return T}),n.d(t,"authorizeRequest",function(){return j}),n.d(t,"configureAuth",function(){return P});var r=n(26),o=n.n(r),i=n(16),a=n.n(i),s=n(28),u=n.n(s),c=n(95),l=n.n(c),p=n(18),f=n.n(p),h=n(3),d="show_popup",m="authorize",v="logout",g="pre_authorize_oauth2",y="authorize_oauth2",b="validate",_="configure_auth";function w(e){return{type:d,payload:e}}function x(e){return{type:m,payload:e}}function E(e){return{type:v,payload:e}}var S=function(e){return function(t){var n=t.authActions,r=t.errActions,o=e.auth,i=e.token,a=e.isValid,s=o.schema,c=o.name,l=s.get("flow");delete f.a.swaggerUIRedirectOauth2,"accessCode"===l||a||r.newAuthErr({authId:c,source:"auth",level:"warning",message:"Authorization may be unsafe, passed state was changed in server Passed state wasn't returned from auth server"}),i.error?r.newAuthErr({authId:c,source:"auth",level:"error",message:u()(i)}):n.authorizeOauth2({auth:o,token:i})}};function C(e){return{type:y,payload:e}}var k=function(e){return function(t){var n=t.authActions,r=e.schema,o=e.name,i=e.username,s=e.password,u=e.passwordType,c=e.clientId,l=e.clientSecret,p={grant_type:"password",scope:e.scopes.join(" "),username:i,password:s},f={};switch(u){case"request-body":!function(e,t,n){t&&a()(e,{client_id:t});n&&a()(e,{client_secret:n})}(p,c,l);break;case"basic":f.Authorization="Basic "+Object(h.a)(c+":"+l);break;default:console.warn("Warning: invalid passwordType ".concat(u," was passed, not including client id and secret"))}return n.authorizeRequest({body:Object(h.b)(p),url:r.get("tokenUrl"),name:o,headers:f,query:{},auth:e})}};var O=function(e){return function(t){var n=t.authActions,r=e.schema,o=e.scopes,i=e.name,a=e.clientId,s=e.clientSecret,u={Authorization:"Basic "+Object(h.a)(a+":"+s)},c={grant_type:"client_credentials",scope:o.join(" ")};return n.authorizeRequest({body:Object(h.b)(c),name:i,url:r.get("tokenUrl"),auth:e,headers:u})}},A=function(e){var t=e.auth,n=e.redirectUrl;return function(e){var r=e.authActions,o=t.schema,i=t.name,a=t.clientId,s=t.clientSecret,u=t.codeVerifier,c={grant_type:"authorization_code",code:t.code,client_id:a,client_secret:s,redirect_uri:n,code_verifier:u};return r.authorizeRequest({body:Object(h.b)(c),name:i,url:o.get("tokenUrl"),auth:t})}},T=function(e){var t=e.auth,n=e.redirectUrl;return function(e){var r=e.authActions,o=t.schema,i=t.name,a=t.clientId,s=t.clientSecret,u={Authorization:"Basic "+Object(h.a)(a+":"+s)},c={grant_type:"authorization_code",code:t.code,client_id:a,redirect_uri:n};return r.authorizeRequest({body:Object(h.b)(c),name:i,url:o.get("tokenUrl"),auth:t,headers:u})}},j=function(e){return function(t){var n,r=t.fn,i=t.getConfigs,s=t.authActions,c=t.errActions,p=t.oas3Selectors,f=t.specSelectors,h=t.authSelectors,d=e.body,m=e.query,v=void 0===m?{}:m,g=e.headers,y=void 0===g?{}:g,b=e.name,_=e.url,w=e.auth,x=(h.getConfigs()||{}).additionalQueryStringParams;n=f.isOAS3()?l()(_,p.selectedServer(),!0):l()(_,f.url(),!0),"object"===o()(x)&&(n.query=a()({},n.query,x));var E=n.toString(),S=a()({Accept:"application/json, text/plain, */*","Content-Type":"application/x-www-form-urlencoded","X-Requested-With":"XMLHttpRequest"},y);r.fetch({url:E,method:"post",headers:S,query:v,body:d,requestInterceptor:i().requestInterceptor,responseInterceptor:i().responseInterceptor}).then(function(e){var t=JSON.parse(e.data),n=t&&(t.error||""),r=t&&(t.parseError||"");e.ok?n||r?c.newAuthErr({authId:b,level:"error",source:"auth",message:u()(t)}):s.authorizeOauth2({auth:w,token:t}):c.newAuthErr({authId:b,level:"error",source:"auth",message:e.statusText})}).catch(function(e){var t=new Error(e).message;if(e.response&&e.response.data){var n=e.response.data;try{var r="string"==typeof n?JSON.parse(n):n;r.error&&(t+=", error: ".concat(r.error)),r.error_description&&(t+=", description: ".concat(r.error_description))}catch(e){}}c.newAuthErr({authId:b,level:"error",source:"auth",message:t})})}};function P(e){return{type:_,payload:e}}},function(e,t){var n=e.exports={version:"2.6.5"};"number"==typeof __e&&(__e=n)},function(e,t){e.exports=function(e){if(null==e)throw TypeError("Can't call method on "+e);return e}},function(e,t,n){var r=n(127),o=Math.min;e.exports=function(e){return e>0?o(r(e),9007199254740991):0}},function(e,t){var n={}.hasOwnProperty;e.exports=function(e,t){return n.call(e,t)}},function(e,t,n){var r=n(211),o=n(210);e.exports=function(e){return r(o(e))}},function(e,t,n){var r=n(49),o=n(133);e.exports=n(50)?function(e,t,n){return r.f(e,t,o(1,n))}:function(e,t,n){return e[t]=n,e}},function(e,t,n){"use strict";e.exports=function(e){if("function"!=typeof e)throw new TypeError(e+" is not a function");return e}},function(e,t,n){"use strict";n.r(t),n.d(t,"UPDATE_LAYOUT",function(){return o}),n.d(t,"UPDATE_FILTER",function(){return i}),n.d(t,"UPDATE_MODE",function(){return a}),n.d(t,"SHOW",function(){return s}),n.d(t,"updateLayout",function(){return u}),n.d(t,"updateFilter",function(){return c}),n.d(t,"show",function(){return l}),n.d(t,"changeMode",function(){return p});var r=n(3),o="layout_update_layout",i="layout_update_filter",a="layout_update_mode",s="layout_show";function u(e){return{type:o,payload:e}}function c(e){return{type:i,payload:e}}function l(e){var t=!(arguments.length>1&&void 0!==arguments[1])||arguments[1];return e=Object(r.w)(e),{type:s,payload:{thing:e,shown:t}}}function p(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"";return e=Object(r.w)(e),{type:a,payload:{thing:e,mode:t}}}},function(e,t,n){"use strict";(function(t){ +/*! + * @description Recursive object extending + * @author Viacheslav Lotsmanov + * @license MIT + * + * The MIT License (MIT) + * + * Copyright (c) 2013-2018 Viacheslav Lotsmanov + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +function n(e){return e instanceof t||e instanceof Date||e instanceof RegExp}function r(e){if(e instanceof t){var n=t.alloc?t.alloc(e.length):new t(e.length);return e.copy(n),n}if(e instanceof Date)return new Date(e.getTime());if(e instanceof RegExp)return new RegExp(e);throw new Error("Unexpected situation")}function o(e){var t=[];return e.forEach(function(e,i){"object"==typeof e&&null!==e?Array.isArray(e)?t[i]=o(e):n(e)?t[i]=r(e):t[i]=a({},e):t[i]=e}),t}function i(e,t){return"__proto__"===t?void 0:e[t]}var a=e.exports=function(){if(arguments.length<1||"object"!=typeof arguments[0])return!1;if(arguments.length<2)return arguments[0];var e,t,s=arguments[0],u=Array.prototype.slice.call(arguments,1);return u.forEach(function(u){"object"!=typeof u||null===u||Array.isArray(u)||Object.keys(u).forEach(function(c){return t=i(s,c),(e=i(u,c))===s?void 0:"object"!=typeof e||null===e?void(s[c]=e):Array.isArray(e)?void(s[c]=o(e)):n(e)?void(s[c]=r(e)):"object"!=typeof t||null===t||Array.isArray(t)?void(s[c]=a({},e)):void(s[c]=a(t,e))})}),s}}).call(this,n(64).Buffer)},function(e,t,n){var r=n(151),o=n(336);e.exports=n(126)?function(e,t,n){return r.f(e,t,o(1,n))}:function(e,t,n){return e[t]=n,e}},function(e,t){e.exports=function(e){try{return!!e()}catch(e){return!0}}},function(e,t,n){var r=n(106),o=n(603),i=n(604),a="[object Null]",s="[object Undefined]",u=r?r.toStringTag:void 0;e.exports=function(e){return null==e?void 0===e?s:a:u&&u in Object(e)?o(e):i(e)}},function(e,t,n){var r=n(621),o=n(624);e.exports=function(e,t){var n=o(e,t);return r(n)?n:void 0}},function(e,t,n){var r=n(380),o=n(661),i=n(107);e.exports=function(e){return i(e)?r(e):o(e)}},function(e,t,n){"use strict";var r=n(178),o=Object.keys||function(e){var t=[];for(var n in e)t.push(n);return t};e.exports=p;var i=n(137);i.inherits=n(47);var a=n(390),s=n(240);i.inherits(p,a);for(var u=o(s.prototype),c=0;c=t.length?{value:void 0,done:!0}:(e=r(t,n),this._i+=e.length,{value:e,done:!1})})},function(e,t){e.exports={}},function(e,t,n){n(561);for(var r=n(32),o=n(77),i=n(102),a=n(34)("toStringTag"),s="CSSRuleList,CSSStyleDeclaration,CSSValueList,ClientRectList,DOMRectList,DOMStringList,DOMTokenList,DataTransferItemList,FileList,HTMLAllCollection,HTMLCollection,HTMLFormElement,HTMLSelectElement,MediaList,MimeTypeArray,NamedNodeMap,NodeList,PaintRequestList,Plugin,PluginArray,SVGLengthList,SVGNumberList,SVGPathSegList,SVGPointList,SVGStringList,SVGTransformList,SourceBufferList,StyleSheetList,TextTrackCueList,TextTrackList,TouchList".split(","),u=0;u1){for(var d=Array(h),m=0;m1){for(var g=Array(v),y=0;y=this._finalSize&&(this._update(this._block),this._block.fill(0));var n=8*this._len;if(n<=4294967295)this._block.writeUInt32BE(n,this._blockSize-4);else{var r=(4294967295&n)>>>0,o=(n-r)/4294967296;this._block.writeUInt32BE(o,this._blockSize-8),this._block.writeUInt32BE(r,this._blockSize-4)}this._update(this._block);var i=this._hash();return e?i.toString(e):i},o.prototype._update=function(){throw new Error("_update must be implemented by subclass")},e.exports=o},function(e,t,n){var r=n(63),o=n(406),i=n(407),a=n(46),s=n(158),u=n(225),c={},l={};(t=e.exports=function(e,t,n,p,f){var h,d,m,v,g=f?function(){return e}:u(e),y=r(n,p,t?2:1),b=0;if("function"!=typeof g)throw TypeError(e+" is not iterable!");if(i(g)){for(h=s(e.length);h>b;b++)if((v=t?y(a(d=e[b])[0],d[1]):y(e[b]))===c||v===l)return v}else for(m=g.call(e);!(d=m.next()).done;)if((v=o(m,y,d.value,t))===c||v===l)return v}).BREAK=c,t.RETURN=l},function(e,t,n){"use strict";function r(e){return null==e}e.exports.isNothing=r,e.exports.isObject=function(e){return"object"==typeof e&&null!==e},e.exports.toArray=function(e){return Array.isArray(e)?e:r(e)?[]:[e]},e.exports.repeat=function(e,t){var n,r="";for(n=0;n1&&void 0!==arguments[1]?arguments[1]:{},r=Object(i.A)(t),a=r.type,s=r.example,u=r.properties,c=r.additionalProperties,l=r.items,p=n.includeReadOnly,f=n.includeWriteOnly;if(void 0!==s)return Object(i.e)(s,"$$ref",function(e){return"string"==typeof e&&e.indexOf("#")>-1});if(!a)if(u)a="object";else{if(!l)return;a="array"}if("object"===a){var d=Object(i.A)(u),m={};for(var v in d)d[v]&&d[v].deprecated||d[v]&&d[v].readOnly&&!p||d[v]&&d[v].writeOnly&&!f||(m[v]=e(d[v],n));if(!0===c)m.additionalProp1={};else if(c)for(var g=Object(i.A)(c),y=e(g,n),b=1;b<4;b++)m["additionalProp"+b]=y;return m}return"array"===a?o()(l.anyOf)?l.anyOf.map(function(t){return e(t,n)}):o()(l.oneOf)?l.oneOf.map(function(t){return e(t,n)}):[e(l,n)]:t.enum?t.default?t.default:Object(i.w)(t.enum)[0]:"file"!==a?h(t):void 0},m=function(e){return e.schema&&(e=e.schema),e.properties&&(e.type="object"),e},v=function e(t){var n,r,a=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},s=p()({},Object(i.A)(t)),u=s.type,c=s.properties,l=s.additionalProperties,f=s.items,d=s.example,m=a.includeReadOnly,v=a.includeWriteOnly,g=s.default,y={},b={},_=t.xml,w=_.name,x=_.prefix,E=_.namespace,S=s.enum;if(!u)if(c||l)u="object";else{if(!f)return;u="array"}if(n=(x?x+":":"")+(w=w||"notagname"),E){var C=x?"xmlns:"+x:"xmlns";b[C]=E}if("array"===u&&f){if(f.xml=f.xml||_||{},f.xml.name=f.xml.name||_.name,_.wrapped)return y[n]=[],o()(d)?d.forEach(function(t){f.example=t,y[n].push(e(f,a))}):o()(g)?g.forEach(function(t){f.default=t,y[n].push(e(f,a))}):y[n]=[e(f,a)],b&&y[n].push({_attr:b}),y;var k=[];return o()(d)?(d.forEach(function(t){f.example=t,k.push(e(f,a))}),k):o()(g)?(g.forEach(function(t){f.default=t,k.push(e(f,a))}),k):e(f,a)}if("object"===u){var O=Object(i.A)(c);for(var A in y[n]=[],d=d||{},O)if(O.hasOwnProperty(A)&&(!O[A].readOnly||m)&&(!O[A].writeOnly||v))if(O[A].xml=O[A].xml||{},O[A].xml.attribute){var T=o()(O[A].enum)&&O[A].enum[0],j=O[A].example,P=O[A].default;b[O[A].xml.name||A]=void 0!==j&&j||void 0!==d[A]&&d[A]||void 0!==P&&P||T||h(O[A])}else{O[A].xml.name=O[A].xml.name||A,void 0===O[A].example&&void 0!==d[A]&&(O[A].example=d[A]);var I=e(O[A]);o()(I)?y[n]=y[n].concat(I):y[n].push(I)}return!0===l?y[n].push({additionalProp:"Anything can be here"}):l&&y[n].push({additionalProp:h(l)}),b&&y[n].push({_attr:b}),y}return r=void 0!==d?d:void 0!==g?g:o()(S)?S[0]:h(t),y[n]=b?[{_attr:b},r]:r,y};function g(e,t){var n=v(e,t);if(n)return s()(n,{declaration:!0,indent:"\t"})}var y=c()(g),b=c()(d)},function(e,t,n){"use strict";n.r(t),n.d(t,"UPDATE_CONFIGS",function(){return i}),n.d(t,"TOGGLE_CONFIGS",function(){return a}),n.d(t,"update",function(){return s}),n.d(t,"toggle",function(){return u}),n.d(t,"loaded",function(){return c});var r=n(2),o=n.n(r),i="configs_update",a="configs_toggle";function s(e,t){return{type:i,payload:o()({},e,t)}}function u(e){return{type:a,payload:e}}var c=function(){return function(){}}},function(e,t,n){"use strict";n.d(t,"a",function(){return a});var r=n(1),o=n.n(r),i=o.a.Set.of("type","format","items","default","maximum","exclusiveMaximum","minimum","exclusiveMinimum","maxLength","minLength","pattern","maxItems","minItems","uniqueItems","enum","multipleOf");function a(e){var t=(arguments.length>1&&void 0!==arguments[1]?arguments[1]:{}).isOAS3;if(!o.a.Map.isMap(e))return{schema:o.a.Map(),parameterContentMediaType:null};if(!t)return"body"===e.get("in")?{schema:e.get("schema",o.a.Map()),parameterContentMediaType:null}:{schema:e.filter(function(e,t){return i.includes(t)}),parameterContentMediaType:null};if(e.get("content")){var n=e.get("content",o.a.Map({})).keySeq().first();return{schema:e.getIn(["content",n,"schema"],o.a.Map()),parameterContentMediaType:n}}return{schema:e.get("schema",o.a.Map()),parameterContentMediaType:null}}},function(e,t,n){e.exports=n(781)},function(e,t,n){"use strict";n.r(t);var r=n(469),o="object"==typeof self&&self&&self.Object===Object&&self,i=(r.a||o||Function("return this")()).Symbol,a=Object.prototype,s=a.hasOwnProperty,u=a.toString,c=i?i.toStringTag:void 0;var l=function(e){var t=s.call(e,c),n=e[c];try{e[c]=void 0;var r=!0}catch(e){}var o=u.call(e);return r&&(t?e[c]=n:delete e[c]),o},p=Object.prototype.toString;var f=function(e){return p.call(e)},h="[object Null]",d="[object Undefined]",m=i?i.toStringTag:void 0;var v=function(e){return null==e?void 0===e?d:h:m&&m in Object(e)?l(e):f(e)};var g=function(e,t){return function(n){return e(t(n))}}(Object.getPrototypeOf,Object);var y=function(e){return null!=e&&"object"==typeof e},b="[object Object]",_=Function.prototype,w=Object.prototype,x=_.toString,E=w.hasOwnProperty,S=x.call(Object);var C=function(e){if(!y(e)||v(e)!=b)return!1;var t=g(e);if(null===t)return!0;var n=E.call(t,"constructor")&&t.constructor;return"function"==typeof n&&n instanceof n&&x.call(n)==S},k=n(330),O={INIT:"@@redux/INIT"};function A(e,t,n){var r;if("function"==typeof t&&void 0===n&&(n=t,t=void 0),void 0!==n){if("function"!=typeof n)throw new Error("Expected the enhancer to be a function.");return n(A)(e,t)}if("function"!=typeof e)throw new Error("Expected the reducer to be a function.");var o=e,i=t,a=[],s=a,u=!1;function c(){s===a&&(s=a.slice())}function l(){return i}function p(e){if("function"!=typeof e)throw new Error("Expected listener to be a function.");var t=!0;return c(),s.push(e),function(){if(t){t=!1,c();var n=s.indexOf(e);s.splice(n,1)}}}function f(e){if(!C(e))throw new Error("Actions must be plain objects. Use custom middleware for async actions.");if(void 0===e.type)throw new Error('Actions may not have an undefined "type" property. Have you misspelled a constant?');if(u)throw new Error("Reducers may not dispatch actions.");try{u=!0,i=o(i,e)}finally{u=!1}for(var t=a=s,n=0;n0&&void 0!==arguments[0]?arguments[0]:{},t=arguments[1];if(a)throw a;for(var r=!1,o={},s=0;s0?r:n)(e)}},function(e,t){e.exports={}},function(e,t,n){var r=n(348),o=n(215);e.exports=Object.keys||function(e){return r(e,o)}},function(e,t){var n={}.toString;e.exports=function(e){return n.call(e).slice(8,-1)}},function(e,t){e.exports=!0},function(e,t){e.exports=function(e){if("function"!=typeof e)throw TypeError(e+" is not a function!");return e}},function(e,t){e.exports=function(e,t){return{enumerable:!(1&e),configurable:!(2&e),writable:!(4&e),value:t}}},function(e,t,n){var r=n(49).f,o=n(75),i=n(34)("toStringTag");e.exports=function(e,t,n){e&&!o(e=n?e:e.prototype,i)&&r(e,i,{configurable:!0,value:t})}},function(e,t,n){var r=n(159)("meta"),o=n(43),i=n(75),a=n(49).f,s=0,u=Object.isExtensible||function(){return!0},c=!n(82)(function(){return u(Object.preventExtensions({}))}),l=function(e){a(e,r,{value:{i:"O"+ ++s,w:{}}})},p=e.exports={KEY:r,NEED:!1,fastKey:function(e,t){if(!o(e))return"symbol"==typeof e?e:("string"==typeof e?"S":"P")+e;if(!i(e,r)){if(!u(e))return"F";if(!t)return"E";l(e)}return e[r].i},getWeak:function(e,t){if(!i(e,r)){if(!u(e))return!0;if(!t)return!1;l(e)}return e[r].w},onFreeze:function(e){return c&&p.NEED&&u(e)&&!i(e,r)&&l(e),e}}},function(e,t,n){"use strict";e.exports=function(e){for(var t=arguments.length-1,n="Minified React error #"+e+"; visit http://facebook.github.io/react/docs/error-decoder.html?invariant="+e,r=0;r1&&void 0!==arguments[1]?arguments[1]:[],n={arrayBehaviour:(arguments.length>2&&void 0!==arguments[2]?arguments[2]:{}).arrayBehaviour||"replace"},r=t.map(function(e){return e||{}}),i=e||{},c=0;c1?t-1:0),r=1;r")}),p=function(){var e=/(?:)/,t=e.exec;e.exec=function(){return t.apply(this,arguments)};var n="ab".split(e);return 2===n.length&&"a"===n[0]&&"b"===n[1]}();e.exports=function(e,t,n){var f=s(e),h=!i(function(){var t={};return t[f]=function(){return 7},7!=""[e](t)}),d=h?!i(function(){var t=!1,n=/a/;return n.exec=function(){return t=!0,null},"split"===e&&(n.constructor={},n.constructor[c]=function(){return n}),n[f](""),!t}):void 0;if(!h||!d||"replace"===e&&!l||"split"===e&&!p){var m=/./[f],v=n(a,f,""[e],function(e,t,n,r,o){return t.exec===u?h&&!o?{done:!0,value:m.call(t,n,r)}:{done:!0,value:e.call(n,t,r)}:{done:!1}}),g=v[0],y=v[1];r(String.prototype,e,g),o(RegExp.prototype,f,2==t?function(e,t){return y.call(e,this,t)}:function(e){return y.call(e,this)})}}},function(e,t,n){var r=n(212),o=Math.min;e.exports=function(e){return e>0?o(r(e),9007199254740991):0}},function(e,t){var n=0,r=Math.random();e.exports=function(e){return"Symbol(".concat(void 0===e?"":e,")_",(++n+r).toString(36))}},function(e,t,n){var r=n(46),o=n(350),i=n(215),a=n(213)("IE_PROTO"),s=function(){},u=function(){var e,t=n(217)("iframe"),r=i.length;for(t.style.display="none",n(351).appendChild(t),t.src="javascript:",(e=t.contentWindow.document).open(),e.write(" + + + + + \ No newline at end of file diff --git a/openflexure_microscope/common/flask_labthings/views/extensions.py b/openflexure_microscope/common/flask_labthings/views/extensions.py new file mode 100644 index 00000000..836af0bb --- /dev/null +++ b/openflexure_microscope/common/flask_labthings/views/extensions.py @@ -0,0 +1,28 @@ +""" +Top-level representation of attached and enabled Extensions +""" +from ..view import View +from ..find import registered_extensions +from ..schema import ExtensionSchema +from ..decorators import marshal_with, ThingProperty + + +@ThingProperty +class ExtensionList(View): + """ + List and basic documentation for all enabled Extensions + """ + + @marshal_with(ExtensionSchema(many=True)) + def get(self): + """ + Return the current Extension forms + + Returns an array of present Extension forms (describing Extension user interfaces.) + Please note, this is *not* a list of all enabled Extensions, only those with associated + user interface forms. + + A complete list of enabled Extensions can be found in the microscope state. + + """ + return registered_extensions().values() diff --git a/openflexure_microscope/common/flask_labthings/views/tasks.py b/openflexure_microscope/common/flask_labthings/views/tasks.py new file mode 100644 index 00000000..f583ba17 --- /dev/null +++ b/openflexure_microscope/common/flask_labthings/views/tasks.py @@ -0,0 +1,51 @@ +from flask import abort, url_for + +from ..decorators import marshal_with, ThingProperty, Tag +from ..view import View +from ..schema import TaskSchema + +from openflexure_microscope.common.labthings_core import tasks + + +@ThingProperty +@Tag("tasks") +class TaskList(View): + @marshal_with(TaskSchema(many=True)) + def get(self): + """ + List of all session tasks + """ + return tasks.tasks() + + +@Tag(["properties", "tasks"]) +class TaskView(View): + @marshal_with(TaskSchema()) + def get(self, id): + """ + Show status of a session task + + Includes progress and intermediate data. + """ + try: + task = tasks.dict()[id] + except KeyError: + return abort(404) # 404 Not Found + + return task + + @marshal_with(TaskSchema()) + def delete(self, id): + """ + Terminate a running task. + + If the task is finished, deletes its entry. + """ + try: + task = tasks.dict()[id] + except KeyError: + return abort(404) # 404 Not Found + + task.terminate() + + return task diff --git a/openflexure_microscope/common/labthings_core/__init__.py b/openflexure_microscope/common/labthings_core/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/openflexure_microscope/common/lock.py b/openflexure_microscope/common/labthings_core/lock.py similarity index 100% rename from openflexure_microscope/common/lock.py rename to openflexure_microscope/common/labthings_core/lock.py diff --git a/openflexure_microscope/common/tasks/__init__.py b/openflexure_microscope/common/labthings_core/tasks/__init__.py similarity index 94% rename from openflexure_microscope/common/tasks/__init__.py rename to openflexure_microscope/common/labthings_core/tasks/__init__.py index 6b3e6dcb..282998ca 100644 --- a/openflexure_microscope/common/tasks/__init__.py +++ b/openflexure_microscope/common/labthings_core/tasks/__init__.py @@ -1,6 +1,7 @@ __all__ = [ "taskify", "tasks", + "dict", "states", "current_task", "update_task_progress", @@ -12,6 +13,7 @@ __all__ = [ from .pool import ( tasks, + dict, states, current_task, update_task_progress, diff --git a/openflexure_microscope/common/tasks/pool.py b/openflexure_microscope/common/labthings_core/tasks/pool.py similarity index 79% rename from openflexure_microscope/common/tasks/pool.py rename to openflexure_microscope/common/labthings_core/tasks/pool.py index 258e46c5..6c152fa5 100644 --- a/openflexure_microscope/common/tasks/pool.py +++ b/openflexure_microscope/common/labthings_core/tasks/pool.py @@ -4,6 +4,8 @@ from functools import wraps from .thread import TaskThread +from flask import copy_current_request_context + class TaskMaster: def __init__(self, *args, **kwargs): @@ -11,11 +13,19 @@ class TaskMaster: @property def tasks(self): + """ + Returns: + list: List of TaskThread objects. + """ + return self._tasks + + @property + def dict(self): """ Returns: dict: Dictionary of TaskThread objects. Key is TaskThread ID. """ - return {t.id: t for t in self._tasks} + return {str(t.id): t for t in self._tasks} @property def states(self): @@ -23,10 +33,13 @@ class TaskMaster: Returns: dict: Dictionary of TaskThread.state dictionaries. Key is TaskThread ID. """ - return {t.id: t.state for t in self._tasks} + return {str(t.id): t.state for t in self._tasks} def new(self, f, *args, **kwargs): - task = TaskThread(target=f, args=args, kwargs=kwargs) + # copy_current_request_context allows threads to access flask current_app + task = TaskThread( + target=copy_current_request_context(f), args=args, kwargs=kwargs + ) self._tasks.append(task) return task @@ -45,13 +58,23 @@ class TaskMaster: def tasks(): + """ + List of tasks in default taskmaster + Returns: + list: List of tasks in default taskmaster + """ + global _default_task_master + return _default_task_master.tasks + + +def dict(): """ Dictionary of tasks in default taskmaster Returns: dict: Dictionary of tasks in default taskmaster """ global _default_task_master - return _default_task_master.tasks + return _default_task_master.dict def states(): diff --git a/openflexure_microscope/common/tasks/thread.py b/openflexure_microscope/common/labthings_core/tasks/thread.py similarity index 99% rename from openflexure_microscope/common/tasks/thread.py rename to openflexure_microscope/common/labthings_core/tasks/thread.py index fbe97d85..d3b06c34 100644 --- a/openflexure_microscope/common/tasks/thread.py +++ b/openflexure_microscope/common/labthings_core/tasks/thread.py @@ -26,7 +26,7 @@ class TaskThread(threading.Thread): ) # A UUID for the TaskThread (not the same as the threading.Thread ident) - self._ID = uuid.uuid4().hex # Task ID + self._ID = uuid.uuid4() # Task ID # Make _target, _args, and _kwargs available to the subclass self._target = target diff --git a/openflexure_microscope/common/labthings_core/utilities.py b/openflexure_microscope/common/labthings_core/utilities.py new file mode 100644 index 00000000..20a90796 --- /dev/null +++ b/openflexure_microscope/common/labthings_core/utilities.py @@ -0,0 +1,71 @@ +import collections.abc +import re +import base64 +import operator +from functools import reduce + + +def get_docstring(obj): + ds = obj.__doc__ + if ds: + return ds.strip() + else: + return "" + + +def get_summary(obj): + return get_docstring(obj).partition("\n")[0].strip() + + +def rupdate(d, u): + for k, v in u.items(): + # Merge lists if they're present in both objects + if isinstance(v, list): + if not k in d: + d[k] = [] + if isinstance(d[k], list): + d[k].extend(v) + # Recursively merge dictionaries if the element is a dictionary + elif isinstance(v, collections.abc.Mapping): + if not k in d: + d[k] = {} + d[k] = rupdate(d.get(k, {}), v) + # If not a list or dictionary, overwrite old value with new value + else: + d[k] = v + return d + + +def get_by_path(root, items): + """Access a nested object in root by item sequence.""" + return reduce(operator.getitem, items, root) + + +def set_by_path(root, items, value): + """Set a value in a nested object in root by item sequence.""" + get_by_path(root, items[:-1])[items[-1]] = value + + +def create_from_path(items): + tree_dict = {} + for key in reversed(items): + tree_dict = {key: tree_dict} + return tree_dict + + +def bottom_level_name(obj): + return obj.__name__.split(".")[-1] + + +def camel_to_snake(name): + s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name) + return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower() + + +def camel_to_spine(name): + s1 = re.sub("(.)([A-Z][a-z]+)", r"\1-\2", name) + return re.sub("([a-z0-9])([A-Z])", r"\1-\2", s1).lower() + + +def snake_to_spine(name): + return name.replace("_", "-") diff --git a/openflexure_microscope/config.py b/openflexure_microscope/config.py index 0c6e56a3..d0d730f8 100644 --- a/openflexure_microscope/config.py +++ b/openflexure_microscope/config.py @@ -3,6 +3,7 @@ import os import errno import logging import shutil +from uuid import UUID import numpy as np from fractions import Fraction @@ -23,8 +24,10 @@ class JSONEncoder(json.JSONEncoder): """ def default(self, o, markers=None): + if isinstance(o, UUID): + return str(o) # PiCamera fractions - if isinstance(o, Fraction): + elif isinstance(o, Fraction): return float(o) # Numpy integers elif isinstance(o, np.integer): @@ -54,17 +57,11 @@ class OpenflexureSettingsFile: expand (bool): Expand paths to valid auxillary config files. """ - def __init__(self, config_path: str = None, expand: bool = True): + def __init__(self, config_path: str = None): global DEFAULT_CONFIG, USER_CONFIG_FILE_PATH - self.expandable_keys = { - "camera_settings": None, - "stage_settings": None, - } #: Dictionary of keys that can be passed as a file path string and expanded automatically - # Set arguments self.config_path = config_path or USER_CONFIG_FILE_PATH - self.expand = expand # Initialise basic config file with defaults if it doesn't exist initialise_file(self.config_path, populate=DEFAULT_CONFIG) @@ -76,11 +73,6 @@ class OpenflexureSettingsFile: # Unexpanded config dictionary (used at load/save time) loaded_config = load_json_file(self.config_path) - # If the loaded config is in contracted format - if self.expand: - # Expand self.raw_config into self._config - loaded_config = self.expand_config(loaded_config) - logging.debug("Reading settings from disk") return loaded_config @@ -88,12 +80,8 @@ class OpenflexureSettingsFile: """ Save config to a file on-disk, and splits into auxillary config files if available. """ - # If the loaded config was in contracted format - if self.expand: - # Contract self._config into self.raw_config - save_settings = self.contract_config(config) - else: - save_settings = config + + save_settings = config if backup: if os.path.isfile(self.config_path): @@ -109,64 +97,6 @@ class OpenflexureSettingsFile: return settings - def expand_config(self, config_dict): - """ - Search a config dictionary for paths to valid auxillary config files, - and expand into the config dictionary if available. - - Args: - config_dict (dict): Dictionary of config data to expand - """ - return_config = {} - if not config_dict: - config_dict = {} - # For each value in the raw loaded config - for key, value in config_dict.items(): - # If it's a valid expandable parameter - if key in self.expandable_keys and type(value) is str: - - logging.debug("Expanding {}".format(value)) - - # Store expansion path - self.expandable_keys[key] = config_dict[key] - # Create the expansion file if it doesn't yet exist - initialise_file(value) - # Load the expansion file into _config - return_config[key] = load_json_file(value) or {} - else: - return_config[key] = value - - return return_config - - def contract_config(self, config_dict): - """ - Split a config dictionary into auxillary config files, if available. - - Args: - config_dict (dict): Dictionary of config data to contract/split - """ - return_config = {} - # For each value in the expanded config - for key, value in config_dict.items(): - # If it's a valid expandable parameter - if ( - key in self.expandable_keys - and self.expandable_keys[key] is not None - and type(value) is dict - ): - - logging.debug("Saving to {}".format(self.expandable_keys[key])) - # Create the file if it doesn't exist - initialise_file(self.expandable_keys[key]) - # Save the expanded config dictionary to the file - save_json_file(self.expandable_keys[key], value) - # Replace the expanded dictionary with a file path - return_config[key] = self.expandable_keys[key] - else: - return_config[key] = value - - return return_config - # HANDLE BASIC LOADING AND SAVING OF SETTINGS FILES @@ -255,10 +185,11 @@ DEFAULT_CONFIG_FILE_PATH = os.path.join(HERE, "microscope_settings.default.json" USER_CONFIG_DIR = os.path.join(os.path.expanduser("~"), ".openflexure") USER_CONFIG_FILE_PATH = os.path.join(USER_CONFIG_DIR, "microscope_settings.json") +USER_EXTENSIONS_PATH = os.path.join(USER_CONFIG_DIR, "microscope_extensions") # Load the default config with open(DEFAULT_CONFIG_FILE_PATH, "r") as default_rc: DEFAULT_CONFIG = default_rc.read() # Create the default user settings object -user_settings = OpenflexureSettingsFile(config_path=USER_CONFIG_FILE_PATH, expand=True) +user_settings = OpenflexureSettingsFile(config_path=USER_CONFIG_FILE_PATH) diff --git a/openflexure_microscope/devel/__init__.py b/openflexure_microscope/devel/__init__.py index 95330291..d412d008 100644 --- a/openflexure_microscope/devel/__init__.py +++ b/openflexure_microscope/devel/__init__.py @@ -5,13 +5,10 @@ Here we include some classes used frequently in plugin development, as well as some Flask imports to simplify API route development """ -# Plugin classes -from openflexure_microscope.plugins import MicroscopePlugin -from openflexure_microscope.api.views import MicroscopeViewPlugin from openflexure_microscope.api.utilities import JsonResponse # Task management -from openflexure_microscope.common.tasks import ( +from openflexure_microscope.common.labthings_core.tasks import ( current_task, update_task_progress, update_task_data, diff --git a/openflexure_microscope/microscope.py b/openflexure_microscope/microscope.py index 981e78d6..e57e1882 100644 --- a/openflexure_microscope/microscope.py +++ b/openflexure_microscope/microscope.py @@ -12,11 +12,10 @@ from openflexure_microscope.camera.base import BaseCamera from openflexure_microscope.camera.mock import MockStreamer from openflexure_microscope.utilities import serialise_array_b64 -from openflexure_microscope.plugins import PluginLoader -from openflexure_microscope.task import TaskOrchestrator -from openflexure_microscope.common.lock import CompositeLock from openflexure_microscope.config import user_settings +from openflexure_microscope.common.labthings_core.lock import CompositeLock + class Microscope: """ @@ -31,31 +30,22 @@ class Microscope: stage (:py:class:`openflexure_microscope.stage.base.BaseStage`): Stage object task: (:py:class:`openflexure_microscope.task.TaskOrchestrator`): Threaded ask orchestrator for managing background tasks using microscope hardware - plugins (:py:class:`openflexure_microscope.plugins.PluginLoader`): Mounting point for all microscope plugins """ def __init__(self): # Initial attributes - self.id = uuid.uuid4().hex + self.id = uuid.uuid4() self.name = self.id self.fov = [0, 0] - self.plugin_maps = [] self.camera = None self.stage = None # Initialise with an empty composite lock self.lock = CompositeLock([]) - # Create a task orchestrator - self.task = TaskOrchestrator() - # Apply settings loaded from file self.apply_settings(user_settings.load()) - # Create plugin mount-point and attach plugins from maps - self.plugins = PluginLoader(self) - self.attach_plugins(self.plugin_maps) - def __enter__(self): """Create microscope on context enter.""" return self @@ -120,25 +110,6 @@ class Microscope: logging.info("Reapplying settings to newly attached devices") self.apply_settings(settings_full) - def attach_plugins(self, plugin_maps: list): - """ - Automatically search for plugin maps in config, and attach. - """ - if plugin_maps: - for plugin_map in plugin_maps: - self.plugins.attach(plugin_map) - else: - logging.warning("No plugins specified. Skipping.") - - def reload_plugins(self): - """ - Empty the plugin mount and re-attach from config. - """ - logging.info("Tearing down existing PluginMount...") - self.plugins = PluginLoader(self) - logging.info("Repopulating PluginMount...") - self.attach_plugins(self.plugin_maps) - def has_real_stage(self): if hasattr(self, "stage") and not isinstance(self.stage, MockStage): return True @@ -151,27 +122,6 @@ class Microscope: else: return False - # Create unified state - @property - def state(self): - """Dictionary of the basic microscope state. - - Return: - dict: Dictionary containing position data, - and :py:attr:`openflexure_microscope.camera.base.BaseCamera.status` - """ - # DEPRECATED - logging.warning( - "Microscope.state is deprecated. Use Microscope.status instead. State will be removed in a future version." - ) - state = { - "camera": self.camera.status, - "stage": self.stage.status, - "plugin": self.plugins.state, - "version": pkg_resources.get_distribution("openflexure_microscope").version, - } - return state - # Create unified status @property def status(self): @@ -208,8 +158,6 @@ class Microscope: self.name = config["name"] if "fov" in config: self.fov = config["fov"] - if "plugins" in config: - self.plugin_maps = config["plugins"] def read_settings(self, json_safe=False): """ @@ -222,12 +170,7 @@ class Microscope: don't get removed from the settings file. """ - settings_current = { - "id": self.id, - "name": self.name, - "fov": self.fov, - "plugins": self.plugin_maps, - } + settings_current = {"id": self.id, "name": self.name, "fov": self.fov} # If attached to a camera if self.camera: @@ -256,22 +199,6 @@ class Microscope: self.stage.save_settings() user_settings.save(current_config, backup=True) - @property - def config(self) -> dict: - logging.warning( - "Reading microscope through config property is deprecated.\ - Please use read_settings method instead." - ) - return self.read_settings() - - @config.setter - def config(self, config: dict) -> None: - logging.warning( - "Setting microscope through config property is deprecated.\ - Please use apply_settings method instead." - ) - self.apply_settings(config) - @property def metadata(self): """ @@ -279,7 +206,7 @@ class Microscope: """ system_metadata = { "microscope_settings": self.read_settings(), - "microscope_state": self.state, + "microscope_state": self.status, "microscope_id": self.id, "microscope_name": self.name, } diff --git a/openflexure_microscope/microscope_settings.default.json b/openflexure_microscope/microscope_settings.default.json index c77883e4..bde62f47 100644 --- a/openflexure_microscope/microscope_settings.default.json +++ b/openflexure_microscope/microscope_settings.default.json @@ -3,13 +3,5 @@ 4100, 3146 ], - "jpeg_quality": 75, - "camera_settings": "~/.openflexure/camera_settings.json", - "stage_settings": "~/.openflexure/stage_settings.json", - "plugins": [ - "openflexure_microscope.plugins.default.autofocus:AutofocusPlugin", - "openflexure_microscope.plugins.default.scan:ScanPlugin", - "openflexure_microscope.plugins.default.camera_calibration:AutocalibrationPlugin", - "openflexure_microscope.plugins.default.zip_builder:ZipBuilderPlugin" - ] + "jpeg_quality": 75 } \ No newline at end of file diff --git a/openflexure_microscope/plugins/__init__.py b/openflexure_microscope/plugins/__init__.py index c22207f5..e69de29b 100644 --- a/openflexure_microscope/plugins/__init__.py +++ b/openflexure_microscope/plugins/__init__.py @@ -1,17 +0,0 @@ -__all__ = [ - "module_from_file", - "load_plugin_class", - "load_plugin_module", - "class_from_map", - "PluginLoader", - "MicroscopePlugin", -] - -from .loader import ( - module_from_file, - load_plugin_class, - load_plugin_module, - class_from_map, - PluginLoader, - MicroscopePlugin, -) diff --git a/openflexure_microscope/plugins/default/scan/plugin.py b/openflexure_microscope/plugins/default/scan/plugin.py index ac92fc68..0863ff51 100644 --- a/openflexure_microscope/plugins/default/scan/plugin.py +++ b/openflexure_microscope/plugins/default/scan/plugin.py @@ -134,7 +134,7 @@ class ScanPlugin(MicroscopePlugin): basename = generate_basename() # Generate a stack ID - scan_id = uuid.uuid4().hex + scan_id = uuid.uuid4() # Store initial position initial_position = self.microscope.stage.position @@ -288,7 +288,7 @@ class ScanPlugin(MicroscopePlugin): # Generate a stack ID if not scan_id: - scan_id = uuid.uuid4().hex + scan_id = uuid.uuid4() # Add scan metadata if not "time" in metadata: diff --git a/openflexure_microscope/plugins/default/zip_builder/plugin.py b/openflexure_microscope/plugins/default/zip_builder/plugin.py index d6b55a22..545f6a1f 100644 --- a/openflexure_microscope/plugins/default/zip_builder/plugin.py +++ b/openflexure_microscope/plugins/default/zip_builder/plugin.py @@ -123,7 +123,7 @@ class ZipBuilderPlugin(MicroscopePlugin): # Update task progress update_task_progress(int((index / n_files) * 100)) - session_id = uuid.uuid4().hex + session_id = uuid.uuid4() # self.session_zips[session_id] = fp self.session_zips[session_id] = { "id": session_id, diff --git a/openflexure_microscope/plugins/example/__init__.py b/openflexure_microscope/plugins/example/__init__.py deleted file mode 100644 index ce9672da..00000000 --- a/openflexure_microscope/plugins/example/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -__all__ = ["Plugin", "HelloWorldAPI", "IdentifyAPI"] - -from .plugin import Plugin -from .api import HelloWorldAPI, IdentifyAPI diff --git a/openflexure_microscope/plugins/example/api.py b/openflexure_microscope/plugins/example/api.py deleted file mode 100644 index ac818184..00000000 --- a/openflexure_microscope/plugins/example/api.py +++ /dev/null @@ -1,100 +0,0 @@ -from openflexure_microscope.api.utilities import JsonResponse -from openflexure_microscope.api.views import MicroscopeViewPlugin -from openflexure_microscope.exceptions import TaskDeniedException - -from flask import request, Response, escape, jsonify, abort - - -class IdentifyAPI(MicroscopeViewPlugin): - """ - A simple example API plugin, attached through the main microscope plugin. - """ - - def get(self): - """ - Method to call when an HTTP GET request is made. - """ - data = ( - self.plugin.identify() - ) # Call a method from our plugin, using the MicroscopeViewPlugin.plugin shortcut - return Response(escape(data)) - - -class HelloWorldAPI(MicroscopeViewPlugin): - """ - A method to create, set, and return a new microscope parameter. - """ - - def get(self): - """ - Method to call when an HTTP GET request is made. - """ - - # If the microscope does not already contain our plugin_string attribute - if not hasattr(self.microscope, "plugin_string"): - # Make a string, using the MicroscopeViewPlugin.plugin shortcut - self.microscope.plugin_string = self.plugin.hello_world() - - return Response(self.microscope.plugin_string) - - def post(self): - """ - Method to call when an HTTP POST request is made. - Assumes request will include a JSON payload. - """ - # Get payload JSON - payload = JsonResponse(request) - - # Extract a value from the JSON key 'plugin_string', and convert to a string. If no value is given, default to empty. - new_plugin_string = payload.param("plugin_string", default="", convert=str) - - if new_plugin_string: # If not None or empty - # Set microscope attribute to the specified string - self.microscope.plugin_string = new_plugin_string - - return Response(self.microscope.plugin_string) - - -class LongRunningAPI(MicroscopeViewPlugin): - """ - An example API plugin that uses a long-running plugin method. - """ - - def post(self): - """ - Method to call when an HTTP POST request is made. - """ - # Get payload JSON - payload = JsonResponse(request) - - # Extract a values from the JSON payload. - time_to_run = payload.param("time", default=10, convert=int) - - # Attach the long-running method as a microscope task - try: - task = self.microscope.task.start(self.plugin.long_running, time_to_run) - return jsonify(task.state), 201 - - except TaskDeniedException: - return abort(409) - - -class SomeExceptionAPI(MicroscopeViewPlugin): - """ - An example API plugin that uses a long-running but broken plugin method. - """ - - def post(self): - """ - Method to call when an HTTP POST request is made. - """ - # Get payload JSON - payload = JsonResponse(request) - - # Attach the long-running method as a microscope task - try: - task = self.microscope.task.start(self.plugin.some_exception) - return jsonify(task.state), 201 - - except TaskDeniedException: - return abort(409) diff --git a/openflexure_microscope/plugins/example/plugin.py b/openflexure_microscope/plugins/example/plugin.py deleted file mode 100644 index c61cb16e..00000000 --- a/openflexure_microscope/plugins/example/plugin.py +++ /dev/null @@ -1,63 +0,0 @@ -import random -import time -from openflexure_microscope.plugins import MicroscopePlugin - -from .api import IdentifyAPI, HelloWorldAPI, LongRunningAPI, SomeExceptionAPI - - -class Plugin(MicroscopePlugin): - """ - A set of default plugins - """ - - api_views = { - "/identify": IdentifyAPI, - "/hello": HelloWorldAPI, - "/long_running": LongRunningAPI, - "/some_exception": SomeExceptionAPI, - } - - def identify(self): - """ - Demonstrate access to Microscope.camera, and Microscope.stage - """ - - response = "My parent camera is {}, and my parent stage is {}.".format( - self.microscope.camera, self.microscope.stage - ) - print(response) - return response - - def hello_world(self): - """ - Demonstrate passive method - """ - - return "Hello world!" - - def some_exception(self): - """ - Demonstrate some broken plugin method that would be long running - """ - with self.microscope.lock: - time.sleep(3) - result = 15.0 / 0 - - return result - - def long_running(self, t_run): - """ - Demonstrate a long-running method that requires microscope hardware - """ - print("Starting a long-running task...") - n_array = [] - - print("Acquiring composite microscope lock...") - with self.microscope.camera.lock, self.microscope.stage.lock: - for _ in range(t_run): - n_array.append(random.random()) - time.sleep(1) - - print("Long-running task finished! Releasing locks.") - - return n_array diff --git a/openflexure_microscope/plugins/loader.py b/openflexure_microscope/plugins/loader.py deleted file mode 100644 index 35f26c73..00000000 --- a/openflexure_microscope/plugins/loader.py +++ /dev/null @@ -1,349 +0,0 @@ -import importlib -import copy -import os -import collections -import inspect -import logging - -from openflexure_microscope.utilities import camel_to_snake, camel_to_spine -from openflexure_microscope.api.views import MicroscopeViewPlugin - - -class ConColors: - HEADER = "\033[95m" - OKBLUE = "\033[94m" - OKGREEN = "\033[92m" - WARNING = "\033[93m" - FAIL = "\033[91m" - ENDC = "\033[0m" - BOLD = "\033[1m" - UNDERLINE = "\033[4m" - - -def module_from_file(plugin_path): - # Expand environment variables in path string - plugin_path = os.path.expandvars(plugin_path) - # Expand user directory in path string - plugin_path = os.path.expanduser(plugin_path) - - # Check if the path is to a file - if not os.path.isfile(plugin_path): - logging.warning( - ConColors.FAIL - + "No valid plugin found at {}.".format(plugin_path) - + ConColors.ENDC - ) - return None, None, None - - else: - # Get name of plugin from the file - plugin_name = os.path.splitext(os.path.basename(plugin_path))[0] - - plugin_spec = importlib.util.spec_from_file_location(plugin_name, plugin_path) - plugin_module = importlib.util.module_from_spec(plugin_spec) - - return plugin_spec, plugin_module, plugin_name - - -def check_module(module_path): - """ - Used to check if a module exists, without ever importing it. - - This checks each nested level separately to avoid raising exceptions. - For example, checking "mymodule.submodule.subsubmodule" will first - check if 'mymodule' exists, then if it does, it will check - 'mymodule.submodule', and so on. - """ - module_split = module_path.split(".") - spec_path = "" - - for module_level in module_split: - spec_path += ".{}".format(module_level) - spec_path = spec_path.strip(".") - - logging.debug("Checking {}".format(spec_path)) - - # Try to find a module loader for the plugin path - plugin_spec = importlib.util.find_spec(spec_path) - - # If plugin spec doesn't exist, return False early - if plugin_spec is None: - return False - - # If all checks pass, return True - return True - - -def name_from_module(plugin_path): - path_array = plugin_path.split(".") - - # Truncate namespace of default plugins - if path_array[0:2] == ["openflexure_microscope", "plugins"]: - path_array = path_array[2:] - - return "/".join(path_array) - - -def load_plugin_module(plugin_path): - # If the loader was found (i.e. plugin probably exists) - if check_module(plugin_path): - try: - plugin_module = importlib.import_module(plugin_path) - plugin_name = name_from_module(plugin_path) - except Exception as e: - logging.warning("Error loading plugin.") - logging.error(e) - return None, None - - # If no loader was found, try finding a file from path - else: - plugin_spec, plugin_module, plugin_name = module_from_file(plugin_path) - # If a valid plugin was found - if plugin_spec and plugin_module: - # Execute the module, so we have access to it - plugin_spec.loader.exec_module(plugin_module) - - return plugin_module, plugin_name - - -def load_plugin_class(plugin_path, plugin_class_name): - plugin_module, plugin_name = load_plugin_module(plugin_path) - if plugin_module: - # Now try to extract the class - try: - plugin_class = getattr(plugin_module, plugin_class_name) - except AttributeError: - logging.warning( - ConColors.FAIL - + "Class {} does not exist in plugin {}. Skipping.".format( - plugin_class_name, plugin_path - ) - + ConColors.ENDC - ) - return None, None - else: - return plugin_class, plugin_name - else: - return None, None - - -def class_from_map(plugin_map): - plugin_arr = plugin_map.split(":") - - if not len(plugin_arr) == 2: - logging.warning( - ConColors.WARNING - + "Malformed plugin map {}. Skipping.".format(plugin_map) - + ConColors.ENDC - ) - return None, None - else: - return load_plugin_class(*plugin_arr) - - -class PluginLoader(object): - """ - A mount-point for all loaded plugins. Attaches to a Microscope object. - - Args: - parent (:py:class:`openflexure_microscope.microscope.Microscope`): The parent Microscope object to attach to. - """ - - def __init__(self, parent): - self.parent = parent - self._plugins = [] # List of plugin objects - self._legacy_plugins = {} # DEPRECATED: Dictionary of plugins with old names - self.forms = [] # List of plugin forms - logging.info("Creating plugin mount") - - @property - def state(self): - # DEPRECATED - logging.warning( - "PluginMount.state is deprecated. Use PluginMount.active instead. State will be removed in a future version." - ) - return list(self._legacy_plugins.keys()) - - @property - def active(self): - return self._plugins - - def attach(self, plugin_map): - """ - Attach a MicroscopePlugin instance to the plugin mount. - - Args: - plugin_map (str): A plugin map describing the file or module to load a MicroscopePlugin child from. Maps should be in the format 'module.to.load:ClassName' or '/path/to/file:ClassName'. - """ - plugin_class, plugin_name = class_from_map(plugin_map) - - if plugin_class is not None: - - logging.debug(f"Loading plugin class {plugin_class}, {plugin_name}") - - plugin_name_python_safe = plugin_name.replace("/", "_") - - if plugin_class and plugin_name: - plugin_object = plugin_class() - - if hasattr( - self, plugin_name - ): # If a plugin with the same name is already attached. - logging.warning( - ConColors.WARNING - + "A plugin named {} has already been loaded. Skipping {}.".format( - plugin_name, plugin_map - ) - + ConColors.ENDC - ) - - elif isinstance( - plugin_object, BasePlugin - ): # If plugin_object is an instance of MicroscopePlugin - # Attach plugin_object to the plugin mount - setattr(self, plugin_name_python_safe, plugin_object) - # Store the plugin object, and it's properties - self._plugins.append(plugin_object) - # DEPRECATED: Store the plugin with it's old name - self._legacy_plugins[plugin_name] = plugin_object - - # Grant plugin access to the hardware - if isinstance(plugin_object, MicroscopePlugin): - plugin_object.microscope = self.parent - - logging.info( - ConColors.OKGREEN - + "Plugin {} loaded as {}.".format( - plugin_map, plugin_object._name - ) - + ConColors.ENDC - ) - - else: - logging.warning(f"Error loading plugin {plugin_map}. Moving on.") - - -class BasePlugin: - """ - Parent class for all plugins. - - Handles binding route views and forms. - """ - - def __init__(self): - self._views = ( - {} - ) # Key: Full, Python-safe ID. Val: Original rule, and view class - self._rules = {} # Key: Original rule. Val: View class - self._gui = None - - # If old api_views dictionary is found - if hasattr(self, "api_views"): - # Convert to new format - self._convert_old_api_views() - # If old api_form dictionary is found - if hasattr(self, "api_form"): - # Convert to new format - self._convert_old_api_form() - - @property - def views(self): - return self._views - - def add_view(self, rule, view_class, **kwargs): - # Remove all leading slashes from view route - cleaned_rule = rule - while cleaned_rule[0] == "/": - cleaned_rule = cleaned_rule[1:] - - # Expand the rule to include plugin name - full_rule = "/{}/{}".format(self._name_uri_safe, cleaned_rule) - - view_id = cleaned_rule.replace("/", "_").replace("<", "").replace(">", "") - - # Create a Python-safe route ID - logging.debug(view_id) - - # Store route information in a dictionary - d = {"rule": full_rule, "view": view_class, "kwargs": kwargs} - - # Add view to private views dictionary - self._views[view_id] = d - # Store the rule expansion information - self._rules[rule] = self._views[view_id] - - @property - def gui(self): - print(self._gui) - # Handle missing/no GUI - if not self._gui: - return None - # Handle GUI as a dictionary-returning callable function - elif isinstance(self._gui, collections.Callable): - api_gui = self._gui() - # Handle GUI as a static dictionary - elif isinstance(self._gui, dict): - api_gui = copy.deepcopy(self._gui) - # Handle borked GUI - else: - raise ValueError( - "GUI must be a dictionary, or a dictionary-returning function with no arguments." - ) - - api_gui["id"] = self._name - - if "forms" in api_gui and isinstance(api_gui["forms"], list): - for form in api_gui["forms"]: - if "route" in form and form["route"] in self._rules.keys(): - form["route"] = self._rules[form["route"]]["rule"] - else: - logging.warn( - "No valid expandable route found for {}".format(form["route"]) - ) - return api_gui - - def set_gui(self, form_dictionary: dict): - self._gui = form_dictionary - - def _convert_old_api_views(self): - for view_route, view_class in self.api_views.items(): - self.add_view(view_route, view_class) - - def _convert_old_api_form(self): - self.set_gui(self.api_form) - - @property - def _name(self): - return self.__class__.__name__ - - @property - def _name_python_safe(self): - return camel_to_snake(self._name) - - @property - def _name_uri_safe(self): - return camel_to_spine(self._name) - - def _full_name(self): - module = self.__class__.__module__ - if module is None or module == str.__class__.__module__: - return self.__class__.__name__ # Avoid reporting __builtin__ - else: - return module + "." + self.__class__.__name__ - - -class MicroscopePlugin(BasePlugin): - """ - Parent class for all microscope plugins. - - Initially only defines an empty object for microscope. All plugins - must be an instance of this class to successfully attach to PluginMount. - """ - - api_views = {} # Initially empty dictionary of API views associated with the plugin - - def __init__(self): - BasePlugin.__init__(self) - self.microscope = ( - None - ) #: :py:class:`openflexure_microscope.microscope.Microscope`: Microscope object diff --git a/openflexure_microscope/stage/base.py b/openflexure_microscope/stage/base.py index 45607a77..edb40158 100644 --- a/openflexure_microscope/stage/base.py +++ b/openflexure_microscope/stage/base.py @@ -1,6 +1,6 @@ import numpy as np from abc import ABCMeta, abstractmethod -from openflexure_microscope.common.lock import StrictLock +from openflexure_microscope.common.labthings_core.lock import StrictLock class BaseStage(metaclass=ABCMeta): @@ -49,6 +49,14 @@ class BaseStage(metaclass=ABCMeta): """The current position, as a list""" pass + @property + def position_map(self): + return { + "x": self.position[0], + "y": self.position[1], + "z": self.position[2], + } + @property @abstractmethod def backlash(self): diff --git a/openflexure_microscope/stage/mock.py b/openflexure_microscope/stage/mock.py index 92765ec3..fe2d2ac6 100644 --- a/openflexure_microscope/stage/mock.py +++ b/openflexure_microscope/stage/mock.py @@ -20,13 +20,10 @@ class MockStage(BaseStage): def status(self): """The general status dictionary of the board.""" status = { - "position": { - "x": self.position[0], - "y": self.position[1], - "z": self.position[2], - }, + "position": self.position_map, "board": None, - "version": "0", + "firmware": None, + "version": None } return status diff --git a/openflexure_microscope/stage/sanga.py b/openflexure_microscope/stage/sanga.py index 27b192a5..2c4fa6d9 100644 --- a/openflexure_microscope/stage/sanga.py +++ b/openflexure_microscope/stage/sanga.py @@ -34,11 +34,7 @@ class SangaStage(BaseStage): def status(self): """The general status dictionary of the board.""" status = { - "position": { - "x": self.position[0], - "y": self.position[1], - "z": self.position[2], - }, + "position": self.position_map, "board": self.board.board, "firmware": self.board.firmware, } diff --git a/openflexure_microscope/stage/sangaboard/extensible_serial_instrument.py b/openflexure_microscope/stage/sangaboard/extensible_serial_instrument.py index 91c53a7b..90534d6c 100644 --- a/openflexure_microscope/stage/sangaboard/extensible_serial_instrument.py +++ b/openflexure_microscope/stage/sangaboard/extensible_serial_instrument.py @@ -213,9 +213,9 @@ class ExtensibleSerialInstrument(object): return self.read_multiline(termination_line) else: logging.debug("Reading response...") - line = ( - self.readline(timeout).strip() - ) # question: should we strip the final newline? + line = self.readline( + timeout + ).strip() # question: should we strip the final newline? logging.debug(f"Read finished. Got {line}") return line diff --git a/openflexure_microscope/task.py b/openflexure_microscope/task.py deleted file mode 100644 index c195e3fc..00000000 --- a/openflexure_microscope/task.py +++ /dev/null @@ -1,76 +0,0 @@ -import logging -from openflexure_microscope.common.tasks import ( - tasks, - states, - taskify, - cleanup_tasks, - remove_task, -) - -# DEPRECATED -class TaskOrchestrator: - """ - DEPRECATED: Class responsible for spawning threaded tasks, and storing their returns. - A microscope should contain exactly one instance of `TaskOrchestrator`. - - Attributes: - tasks (list): List of `Task` objects - - """ - - @property - def tasks(self): - logging.warning( - "TaskOrchestrator class is deprecated. Please use common.tasks.tasks().values() instead." - ) - return tasks().values() - - @property - def state(self): - """ - Returns a list of dictionary representations of all tasks in the session. - """ - logging.warning( - "TaskOrchestrator class is deprecated. Please use common.tasks.tasks() instead." - ) - return states() - - def task_from_id(self, task_id: str): - """ - Returns a particular task object with the specified `task_id`. - """ - return tasks()[task_id] - - def start(self, function, *args, **kwargs): - """ - Attach and start a new long-running task, if allowed by `start_condition()`. - Returns an instance of :py:class:`openflexure_microscope.task.Task`, which can be used to check the state - or eventual return of the task. - - Args: - function (function): The target function to run in the background - args, kwargs: Arguments that will be passed to `function` at runtime. - """ - logging.warning( - "TaskOrchestrator class is deprecated. Please use common.tasks.taskify() instead." - ) - return taskify(function)(*args, **kwargs) - - def clean(self): - """ - Remove all inactive tasks from the task array. - This will remove tasks that either finished or never started. - """ - logging.warning( - "TaskOrchestrator class is deprecated. Please use common.tasks.cleanup_tasks() instead." - ) - cleanup_tasks() - - def delete(self, task_id: str): - """ - Delete a given task by ID, only if task is not currently running. - """ - logging.warning( - "TaskOrchestrator class is deprecated. Please use common.tasks.remove_task() instead." - ) - remove_task(task_id) diff --git a/openflexure_microscope/utilities.py b/openflexure_microscope/utilities.py index 20a543ad..bd88664e 100644 --- a/openflexure_microscope/utilities.py +++ b/openflexure_microscope/utilities.py @@ -2,6 +2,7 @@ import re import copy import operator import base64 +from uuid import UUID import numpy as np from collections import abc from functools import reduce @@ -12,62 +13,13 @@ def deserialise_array_b64(b64_string, dtype, shape): flat_arr = np.fromstring(base64.b64decode(b64_string), dtype) return flat_arr.reshape(shape) + def serialise_array_b64(npy_arr): - b64_string = base64.b64encode(npy_arr).decode('ascii') + b64_string = base64.b64encode(npy_arr).decode("ascii") dtype = str(npy_arr.dtype) shape = npy_arr.shape return b64_string, dtype, shape -def get_by_path(root, items): - """Access a nested object in root by item sequence.""" - return reduce(operator.getitem, items, root) - - -def set_by_path(root, items, value): - """Set a value in a nested object in root by item sequence.""" - get_by_path(root, items[:-1])[items[-1]] = value - - -def create_from_path(items): - tree_dict = {} - for key in reversed(items): - tree_dict = {key: tree_dict} - return tree_dict - - -def bottom_level_name(obj): - return obj.__name__.split(".")[-1] - - -def description_from_view(view_class): - methods = [] - for method_key in ["get", "post", "put", "delete"]: - if hasattr(view_class, method_key): - methods.append(method_key.upper()) - brief_description = get_docstring(view_class).partition("\n")[0].strip() - - d = {"methods": methods, "description": brief_description} - - return d - - -def get_docstring(obj): - ds = obj.__doc__ - if ds: - return ds.strip() - else: - return "" - - -def camel_to_snake(name): - s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name) - return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower() - - -def camel_to_spine(name): - s1 = re.sub("(.)([A-Z][a-z]+)", r"\1-\2", name) - return re.sub("([a-z0-9])([A-Z])", r"\1-\2", s1).lower() - @contextmanager def set_properties(obj, **kwargs): @@ -127,11 +79,20 @@ def filter_dict(dictionary: dict, keys: list): return out -def entry_by_id(entry_id: str, object_list: list): +def entry_by_uuid(entry_id: str, object_list: list): """Return an object from a list, if .id matches id argument.""" found = None + if type(entry_id) == str: + converter = str + elif type(entry_id) == int: + converter = int + elif isinstance(entry_id, UUID): + converter = int + else: + raise TypeError("Argument entry_id must be a string, integer, or UUID object.") for o in object_list: - if o.id == entry_id: + # Convert to strings (in case of UUID objects, for example) + if converter(o.id) == converter(entry_id): found = o return found diff --git a/poetry.lock b/poetry.lock index c54fe7f2..2efd26f1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -39,6 +39,12 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" version = "19.3.0" +[package.extras] +azure-pipelines = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "pytest-azurepipelines"] +dev = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "pre-commit"] +docs = ["sphinx", "zope.interface"] +tests = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] + [[package]] category = "dev" description = "Internationalization utilities" @@ -64,6 +70,9 @@ attrs = ">=17.4.0" click = ">=6.5" toml = ">=0.9.4" +[package.extras] +d = ["aiohttp (>=3.3.2)"] + [[package]] category = "dev" description = "Python package for providing Mozilla's CA Bundle." @@ -119,6 +128,11 @@ Werkzeug = ">=0.15" click = ">=5.1" itsdangerous = ">=0.24" +[package.extras] +dev = ["pytest", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinxcontrib-log-cabinet", "sphinx-issues"] +docs = ["sphinx", "pallets-sphinx-themes", "sphinxcontrib-log-cabinet", "sphinx-issues"] +dotenv = ["python-dotenv"] + [[package]] category = "main" description = "A Flask extension adding a decorator for CORS support" @@ -155,6 +169,12 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" version = "4.3.21" +[package.extras] +pipfile = ["pipreqs", "requirementslib"] +pyproject = ["toml"] +requirements = ["pipreqs", "pip-api"] +xdg_home = ["appdirs (>=1.4.0)"] + [[package]] category = "main" description = "Various helpers to pass data to untrusted environments and back." @@ -174,6 +194,9 @@ version = "2.10.3" [package.dependencies] MarkupSafe = ">=0.23" +[package.extras] +i18n = ["Babel (>=0.8)"] + [[package]] category = "dev" description = "A fast and thorough lazy object proxy." @@ -190,6 +213,20 @@ optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" version = "1.1.1" +[[package]] +category = "main" +description = "A lightweight library for converting complex datatypes to and from native Python datatypes." +name = "marshmallow" +optional = false +python-versions = ">=3.5" +version = "3.3.0" + +[package.extras] +dev = ["pytest", "pytz", "simplejson", "mypy (0.750)", "flake8 (3.7.9)", "flake8-bugbear (19.8.0)", "pre-commit (>=1.20,<2.0)", "tox"] +docs = ["sphinx (2.2.2)", "sphinx-issues (1.2.0)", "alabaster (0.7.12)", "sphinx-version-warning (1.1.2)"] +lint = ["mypy (0.750)", "flake8 (3.7.9)", "flake8-bugbear (19.8.0)", "pre-commit (>=1.20,<2.0)"] +tests = ["pytest", "pytz", "simplejson"] + [[package]] category = "dev" description = "McCabe checker, plugin for flake8" @@ -230,6 +267,7 @@ version = "1.13.1b0" reference = "b39c2b6e42f5f7f57bb46eafcb5c9e2bbdb5d0cb" type = "git" url = "https://github.com/rwb27/picamera.git" + [[package]] category = "main" description = "Python Imaging Library (Fork)" @@ -298,6 +336,10 @@ chardet = ">=3.0.2,<3.1.0" idna = ">=2.5,<2.9" urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26" +[package.extras] +security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)"] +socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"] + [[package]] category = "dev" description = "a python refactoring library..." @@ -368,6 +410,10 @@ sphinxcontrib-jsmath = "*" sphinxcontrib-qthelp = "*" sphinxcontrib-serializinghtml = "*" +[package.extras] +docs = ["sphinxcontrib-websupport"] +test = ["pytest", "pytest-cov", "html5lib", "flake8 (>=3.5.0)", "flake8-import-order", "mypy (>=0.750)", "docutils-stubs"] + [[package]] category = "dev" description = "" @@ -376,6 +422,9 @@ optional = false python-versions = "*" version = "1.0.1" +[package.extras] +test = ["pytest", "flake8", "mypy"] + [[package]] category = "dev" description = "" @@ -384,6 +433,9 @@ optional = false python-versions = "*" version = "1.0.1" +[package.extras] +test = ["pytest", "flake8", "mypy"] + [[package]] category = "dev" description = "" @@ -392,6 +444,9 @@ optional = false python-versions = "*" version = "1.0.2" +[package.extras] +test = ["pytest", "flake8", "mypy", "html5lib"] + [[package]] category = "dev" description = "Sphinx domain for documenting HTTP APIs" @@ -412,6 +467,9 @@ optional = false python-versions = ">=3.5" version = "1.0.1" +[package.extras] +test = ["pytest", "flake8", "mypy"] + [[package]] category = "dev" description = "" @@ -420,6 +478,9 @@ optional = false python-versions = "*" version = "1.0.2" +[package.extras] +test = ["pytest", "flake8", "mypy"] + [[package]] category = "dev" description = "" @@ -428,6 +489,9 @@ optional = false python-versions = "*" version = "1.1.3" +[package.extras] +test = ["pytest", "flake8", "mypy"] + [[package]] category = "dev" description = "Python Library for Tom's Obvious, Minimal Language" @@ -453,6 +517,29 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4" version = "1.25.7" +[package.extras] +brotli = ["brotlipy (>=0.6.0)"] +secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] +socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"] + +[[package]] +category = "main" +description = "Declarative parsing and validation of HTTP request objects, with built-in support for popular web frameworks, including Flask, Django, Bottle, Tornado, Pyramid, webapp2, Falcon, and aiohttp." +name = "webargs" +optional = false +python-versions = "*" +version = "5.5.2" + +[package.dependencies] +marshmallow = ">=2.15.2" + +[package.extras] +dev = ["pytest", "mock", "webtest (2.0.33)", "Flask (>=0.12.2)", "Django (>=1.11.16)", "bottle (>=0.12.13)", "tornado (>=4.5.2)", "pyramid (>=1.9.1)", "webapp2 (>=3.0.0b1)", "falcon (>=1.4.0,<2.0)", "flake8 (3.7.8)", "pre-commit (>=1.17,<2.0)", "tox", "webtest-aiohttp (2.0.0)", "pytest-aiohttp (>=0.3.0)", "aiohttp (>=3.0.0)", "mypy (0.730)", "flake8-bugbear (19.8.0)"] +docs = ["Sphinx (2.2.0)", "sphinx-issues (1.2.0)", "sphinx-typlog-theme (0.7.3)", "Flask (>=0.12.2)", "Django (>=1.11.16)", "bottle (>=0.12.13)", "tornado (>=4.5.2)", "pyramid (>=1.9.1)", "webapp2 (>=3.0.0b1)", "falcon (>=1.4.0,<2.0)", "aiohttp (>=3.0.0)"] +frameworks = ["Flask (>=0.12.2)", "Django (>=1.11.16)", "bottle (>=0.12.13)", "tornado (>=4.5.2)", "pyramid (>=1.9.1)", "webapp2 (>=3.0.0b1)", "falcon (>=1.4.0,<2.0)", "aiohttp (>=3.0.0)"] +lint = ["flake8 (3.7.8)", "pre-commit (>=1.17,<2.0)", "mypy (0.730)", "flake8-bugbear (19.8.0)"] +tests = ["pytest", "mock", "webtest (2.0.33)", "Flask (>=0.12.2)", "Django (>=1.11.16)", "bottle (>=0.12.13)", "tornado (>=4.5.2)", "pyramid (>=1.9.1)", "webapp2 (>=3.0.0b1)", "falcon (>=1.4.0,<2.0)", "webtest-aiohttp (2.0.0)", "pytest-aiohttp (>=0.3.0)", "aiohttp (>=3.0.0)"] + [[package]] category = "main" description = "The comprehensive WSGI web application library." @@ -461,6 +548,11 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" version = "0.16.0" +[package.extras] +dev = ["pytest", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinx-issues"] +termcolor = ["termcolor"] +watchdog = ["watchdog"] + [[package]] category = "dev" description = "Module for decorators, wrappers and monkey patching." @@ -473,56 +565,364 @@ version = "1.11.2" rpi = ["picamera", "RPi.GPIO"] [metadata] -content-hash = "ea3fcd1909a76b04efa8ebefdcb77f0b449642820a94429fbbbf20bb4f9a64ab" +content-hash = "a1af645f32e0c3c01c8183ede8b6d5d649ddf8c8250702faa216e164e9e36335" python-versions = "^3.6" -[metadata.hashes] -alabaster = ["446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359", "a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"] -appdirs = ["9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", "d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"] -astroid = ["71ea07f44df9568a75d0f354c49143a4575d90645e9fead6dfb52c26a85ed13a", "840947ebfa8b58f318d42301cf8c0a20fd794a33b61cc4638e28e9e61ba32f42"] -attrs = ["08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", "f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"] -babel = ["af92e6106cb7c55286b25b38ad7695f8b4efb36a90ba483d7f7a6628c46158ab", "e86135ae101e31e2c8ec20a4e0c5220f4eed12487d5cf3f78be7e98d3a57fc28"] -black = ["817243426042db1d36617910df579a54f1afd659adb96fc5032fcf4b36209739", "e030a9a28f542debc08acceb273f228ac422798e5215ba2a791a6ddeaaca22a5"] -certifi = ["017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3", "25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f"] -chardet = ["84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", "fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"] -click = ["2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", "5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"] -colorama = ["7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff", "e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"] -docutils = ["6c4f696463b79f1fb8ba0c594b63840ebd41f059e92b31957c46b74a4599b6d0", "9e4d7ecfc600058e07ba661411a2b7de2fd0fafa17d1a7f7361cd47b1175c827", "a2aeea129088da402665e92e0b25b04b073c04b2dce4ab65caaa38b7ce2e1a99"] -flask = ["13f9f196f330c7c2c5d7a5cf91af894110ca0215ac051b5844701f2bfd934d52", "45eb5a6fd193d6cf7e0cf5d8a5b31f83d5faae0293695626f539a823e93b13f6"] -flask-cors = ["72170423eb4612f0847318afff8c247b38bd516b7737adfc10d1c2cdbb382d16", "f4d97201660e6bbcff2d89d082b5b6d31abee04b1b3003ee073a6fd25ad1d69a"] -idna = ["c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", "ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"] -imagesize = ["3f349de3eb99145973fefb7dbe38554414e5c30abd0c8e4b970a7c9d09f3a1d8", "f3832918bc3c66617f92e35f5d70729187676313caa60c187eb0f28b8fe5e3b5"] -isort = ["54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", "6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"] -itsdangerous = ["321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19", "b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"] -jinja2 = ["74320bb91f31270f9551d46522e33af46a80c3d619f4a4bf42b3164d30b5911f", "9fe95f19286cfefaa917656583d020be14e7859c6b0252588391e47db34527de"] -lazy-object-proxy = ["0c4b206227a8097f05c4dbdd323c50edf81f15db3b8dc064d08c62d37e1a504d", "194d092e6f246b906e8f70884e620e459fc54db3259e60cf69a4d66c3fda3449", "1be7e4c9f96948003609aa6c974ae59830a6baecc5376c25c92d7d697e684c08", "4677f594e474c91da97f489fea5b7daa17b5517190899cf213697e48d3902f5a", "48dab84ebd4831077b150572aec802f303117c8cc5c871e182447281ebf3ac50", "5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd", "59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239", "8d859b89baf8ef7f8bc6b00aa20316483d67f0b1cbf422f5b4dc56701c8f2ffb", "9254f4358b9b541e3441b007a0ea0764b9d056afdeafc1a5569eee1cc6c1b9ea", "9651375199045a358eb6741df3e02a651e0330be090b3bc79f6d0de31a80ec3e", "97bb5884f6f1cdce0099f86b907aa41c970c3c672ac8b9c8352789e103cf3156", "9b15f3f4c0f35727d3a0fba4b770b3c4ebbb1fa907dbcc046a1d2799f3edd142", "a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442", "a6ae12d08c0bf9909ce12385803a543bfe99b95fe01e752536a60af2b7797c62", "ca0a928a3ddbc5725be2dd1cf895ec0a254798915fb3a36af0964a0a4149e3db", "cb2c7c57005a6804ab66f106ceb8482da55f5314b7fcb06551db1edae4ad1531", "d74bb8693bf9cf75ac3b47a54d716bbb1a92648d5f781fc799347cfc95952383", "d945239a5639b3ff35b70a88c5f2f491913eb94871780ebfabb2568bd58afc5a", "eba7011090323c1dadf18b3b689845fd96a61ba0a1dfbd7f24b921398affc357", "efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4", "f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0"] -markupsafe = ["00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", "09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", "09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", "1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", "24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", "29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", "43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", "46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", "500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", "535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", "62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", "6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", "717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", "79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", "7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", "88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", "8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", "98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", "9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", "9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", "ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", "b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", "b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", "b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", "ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", "c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", "cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", "e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"] -mccabe = ["ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", "dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"] -numpy = ["0a7a1dd123aecc9f0076934288ceed7fd9a81ba3919f11a855a7887cbe82a02f", "0c0763787133dfeec19904c22c7e358b231c87ba3206b211652f8cbe1241deb6", "3d52298d0be333583739f1aec9026f3b09fdfe3ddf7c7028cb16d9d2af1cca7e", "43bb4b70585f1c2d153e45323a886839f98af8bfa810f7014b20be714c37c447", "475963c5b9e116c38ad7347e154e5651d05a2286d86455671f5b1eebba5feb76", "64874913367f18eb3013b16123c9fed113962e75d809fca5b78ebfbb73ed93ba", "683828e50c339fc9e68720396f2de14253992c495fdddef77a1e17de55f1decc", "6ca4000c4a6f95a78c33c7dadbb9495c10880be9c89316aa536eac359ab820ae", "75fd817b7061f6378e4659dd792c84c0b60533e867f83e0d1e52d5d8e53df88c", "7d81d784bdbed30137aca242ab307f3e65c8d93f4c7b7d8f322110b2e90177f9", "8d0af8d3664f142414fd5b15cabfd3b6cc3ef242a3c7a7493257025be5a6955f", "9679831005fb16c6df3dd35d17aa31dc0d4d7573d84f0b44cc481490a65c7725", "a8f67ebfae9f575d85fa859b54d3bdecaeece74e3274b0b5c5f804d7ca789fe1", "acbf5c52db4adb366c064d0b7c7899e3e778d89db585feadd23b06b587d64761", "ada4805ed51f5bcaa3a06d3dd94939351869c095e30a2b54264f5a5004b52170", "c7354e8f0eca5c110b7e978034cd86ed98a7a5ffcf69ca97535445a595e07b8e", "e2e9d8c87120ba2c591f60e32736b82b67f72c37ba88a4c23c81b5b8fa49c018", "e467c57121fe1b78a8f68dd9255fbb3bb3f4f7547c6b9e109f31d14569f490c3", "ede47b98de79565fcd7f2decb475e2dcc85ee4097743e551fe26cfc7eb3ff143", "f58913e9227400f1395c7b800503ebfdb0772f1c33ff8cb4d6451c06cabdf316", "fe39f5fd4103ec4ca3cb8600b19216cd1ff316b4990f4c0b6057ad982c0a34d5"] -packaging = ["28b924174df7a2fa32c1953825ff29c61e2f5e082343165438812f00d3a7fc47", "d9551545c6d761f3def1677baf08ab2a3ca17c56879e70fecba2fc4dde4ed108"] +[metadata.files] +alabaster = [ + {file = "alabaster-0.7.12-py2.py3-none-any.whl", hash = "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359"}, + {file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"}, +] +appdirs = [ + {file = "appdirs-1.4.3-py2.py3-none-any.whl", hash = "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"}, + {file = "appdirs-1.4.3.tar.gz", hash = "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92"}, +] +astroid = [ + {file = "astroid-2.3.3-py3-none-any.whl", hash = "sha256:840947ebfa8b58f318d42301cf8c0a20fd794a33b61cc4638e28e9e61ba32f42"}, + {file = "astroid-2.3.3.tar.gz", hash = "sha256:71ea07f44df9568a75d0f354c49143a4575d90645e9fead6dfb52c26a85ed13a"}, +] +attrs = [ + {file = "attrs-19.3.0-py2.py3-none-any.whl", hash = "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c"}, + {file = "attrs-19.3.0.tar.gz", hash = "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"}, +] +babel = [ + {file = "Babel-2.7.0-py2.py3-none-any.whl", hash = "sha256:af92e6106cb7c55286b25b38ad7695f8b4efb36a90ba483d7f7a6628c46158ab"}, + {file = "Babel-2.7.0.tar.gz", hash = "sha256:e86135ae101e31e2c8ec20a4e0c5220f4eed12487d5cf3f78be7e98d3a57fc28"}, +] +black = [ + {file = "black-18.9b0-py36-none-any.whl", hash = "sha256:817243426042db1d36617910df579a54f1afd659adb96fc5032fcf4b36209739"}, + {file = "black-18.9b0.tar.gz", hash = "sha256:e030a9a28f542debc08acceb273f228ac422798e5215ba2a791a6ddeaaca22a5"}, +] +certifi = [ + {file = "certifi-2019.11.28-py2.py3-none-any.whl", hash = "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3"}, + {file = "certifi-2019.11.28.tar.gz", hash = "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f"}, +] +chardet = [ + {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, + {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"}, +] +click = [ + {file = "Click-7.0-py2.py3-none-any.whl", hash = "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13"}, + {file = "Click-7.0.tar.gz", hash = "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"}, +] +colorama = [ + {file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"}, + {file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"}, +] +docutils = [ + {file = "docutils-0.15.2-py2-none-any.whl", hash = "sha256:9e4d7ecfc600058e07ba661411a2b7de2fd0fafa17d1a7f7361cd47b1175c827"}, + {file = "docutils-0.15.2-py3-none-any.whl", hash = "sha256:6c4f696463b79f1fb8ba0c594b63840ebd41f059e92b31957c46b74a4599b6d0"}, + {file = "docutils-0.15.2.tar.gz", hash = "sha256:a2aeea129088da402665e92e0b25b04b073c04b2dce4ab65caaa38b7ce2e1a99"}, +] +flask = [ + {file = "Flask-1.1.1-py2.py3-none-any.whl", hash = "sha256:45eb5a6fd193d6cf7e0cf5d8a5b31f83d5faae0293695626f539a823e93b13f6"}, + {file = "Flask-1.1.1.tar.gz", hash = "sha256:13f9f196f330c7c2c5d7a5cf91af894110ca0215ac051b5844701f2bfd934d52"}, +] +flask-cors = [ + {file = "Flask-Cors-3.0.8.tar.gz", hash = "sha256:72170423eb4612f0847318afff8c247b38bd516b7737adfc10d1c2cdbb382d16"}, + {file = "Flask_Cors-3.0.8-py2.py3-none-any.whl", hash = "sha256:f4d97201660e6bbcff2d89d082b5b6d31abee04b1b3003ee073a6fd25ad1d69a"}, +] +idna = [ + {file = "idna-2.8-py2.py3-none-any.whl", hash = "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"}, + {file = "idna-2.8.tar.gz", hash = "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407"}, +] +imagesize = [ + {file = "imagesize-1.1.0-py2.py3-none-any.whl", hash = "sha256:3f349de3eb99145973fefb7dbe38554414e5c30abd0c8e4b970a7c9d09f3a1d8"}, + {file = "imagesize-1.1.0.tar.gz", hash = "sha256:f3832918bc3c66617f92e35f5d70729187676313caa60c187eb0f28b8fe5e3b5"}, +] +isort = [ + {file = "isort-4.3.21-py2.py3-none-any.whl", hash = "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"}, + {file = "isort-4.3.21.tar.gz", hash = "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1"}, +] +itsdangerous = [ + {file = "itsdangerous-1.1.0-py2.py3-none-any.whl", hash = "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"}, + {file = "itsdangerous-1.1.0.tar.gz", hash = "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19"}, +] +jinja2 = [ + {file = "Jinja2-2.10.3-py2.py3-none-any.whl", hash = "sha256:74320bb91f31270f9551d46522e33af46a80c3d619f4a4bf42b3164d30b5911f"}, + {file = "Jinja2-2.10.3.tar.gz", hash = "sha256:9fe95f19286cfefaa917656583d020be14e7859c6b0252588391e47db34527de"}, +] +lazy-object-proxy = [ + {file = "lazy-object-proxy-1.4.3.tar.gz", hash = "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0"}, + {file = "lazy_object_proxy-1.4.3-cp27-cp27m-macosx_10_13_x86_64.whl", hash = "sha256:a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442"}, + {file = "lazy_object_proxy-1.4.3-cp27-cp27m-win32.whl", hash = "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4"}, + {file = "lazy_object_proxy-1.4.3-cp27-cp27m-win_amd64.whl", hash = "sha256:4677f594e474c91da97f489fea5b7daa17b5517190899cf213697e48d3902f5a"}, + {file = "lazy_object_proxy-1.4.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:0c4b206227a8097f05c4dbdd323c50edf81f15db3b8dc064d08c62d37e1a504d"}, + {file = "lazy_object_proxy-1.4.3-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:d945239a5639b3ff35b70a88c5f2f491913eb94871780ebfabb2568bd58afc5a"}, + {file = "lazy_object_proxy-1.4.3-cp34-cp34m-win32.whl", hash = "sha256:9651375199045a358eb6741df3e02a651e0330be090b3bc79f6d0de31a80ec3e"}, + {file = "lazy_object_proxy-1.4.3-cp34-cp34m-win_amd64.whl", hash = "sha256:eba7011090323c1dadf18b3b689845fd96a61ba0a1dfbd7f24b921398affc357"}, + {file = "lazy_object_proxy-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:48dab84ebd4831077b150572aec802f303117c8cc5c871e182447281ebf3ac50"}, + {file = "lazy_object_proxy-1.4.3-cp35-cp35m-win32.whl", hash = "sha256:ca0a928a3ddbc5725be2dd1cf895ec0a254798915fb3a36af0964a0a4149e3db"}, + {file = "lazy_object_proxy-1.4.3-cp35-cp35m-win_amd64.whl", hash = "sha256:194d092e6f246b906e8f70884e620e459fc54db3259e60cf69a4d66c3fda3449"}, + {file = "lazy_object_proxy-1.4.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:97bb5884f6f1cdce0099f86b907aa41c970c3c672ac8b9c8352789e103cf3156"}, + {file = "lazy_object_proxy-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:cb2c7c57005a6804ab66f106ceb8482da55f5314b7fcb06551db1edae4ad1531"}, + {file = "lazy_object_proxy-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:8d859b89baf8ef7f8bc6b00aa20316483d67f0b1cbf422f5b4dc56701c8f2ffb"}, + {file = "lazy_object_proxy-1.4.3-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:1be7e4c9f96948003609aa6c974ae59830a6baecc5376c25c92d7d697e684c08"}, + {file = "lazy_object_proxy-1.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d74bb8693bf9cf75ac3b47a54d716bbb1a92648d5f781fc799347cfc95952383"}, + {file = "lazy_object_proxy-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:9b15f3f4c0f35727d3a0fba4b770b3c4ebbb1fa907dbcc046a1d2799f3edd142"}, + {file = "lazy_object_proxy-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9254f4358b9b541e3441b007a0ea0764b9d056afdeafc1a5569eee1cc6c1b9ea"}, + {file = "lazy_object_proxy-1.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:a6ae12d08c0bf9909ce12385803a543bfe99b95fe01e752536a60af2b7797c62"}, + {file = "lazy_object_proxy-1.4.3-cp38-cp38-win32.whl", hash = "sha256:5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd"}, + {file = "lazy_object_proxy-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239"}, +] +markupsafe = [ + {file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-win32.whl", hash = "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-win_amd64.whl", hash = "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e"}, + {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f"}, + {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-win32.whl", hash = "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-win_amd64.whl", hash = "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"}, + {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, +] +marshmallow = [ + {file = "marshmallow-3.3.0-py2.py3-none-any.whl", hash = "sha256:3e53dd9e9358977a3929e45cdbe4a671f9eff53a7d6a23f33ed3eab8c1890d8f"}, + {file = "marshmallow-3.3.0.tar.gz", hash = "sha256:0ba81b6da4ae69eb229b74b3c741ff13fe04fb899824377b1aff5aaa1a9fd46e"}, +] +mccabe = [ + {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, + {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, +] +numpy = [ + {file = "numpy-1.17.4-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:ede47b98de79565fcd7f2decb475e2dcc85ee4097743e551fe26cfc7eb3ff143"}, + {file = "numpy-1.17.4-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:43bb4b70585f1c2d153e45323a886839f98af8bfa810f7014b20be714c37c447"}, + {file = "numpy-1.17.4-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c7354e8f0eca5c110b7e978034cd86ed98a7a5ffcf69ca97535445a595e07b8e"}, + {file = "numpy-1.17.4-cp35-cp35m-win32.whl", hash = "sha256:64874913367f18eb3013b16123c9fed113962e75d809fca5b78ebfbb73ed93ba"}, + {file = "numpy-1.17.4-cp35-cp35m-win_amd64.whl", hash = "sha256:6ca4000c4a6f95a78c33c7dadbb9495c10880be9c89316aa536eac359ab820ae"}, + {file = "numpy-1.17.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:75fd817b7061f6378e4659dd792c84c0b60533e867f83e0d1e52d5d8e53df88c"}, + {file = "numpy-1.17.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:7d81d784bdbed30137aca242ab307f3e65c8d93f4c7b7d8f322110b2e90177f9"}, + {file = "numpy-1.17.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fe39f5fd4103ec4ca3cb8600b19216cd1ff316b4990f4c0b6057ad982c0a34d5"}, + {file = "numpy-1.17.4-cp36-cp36m-win32.whl", hash = "sha256:e467c57121fe1b78a8f68dd9255fbb3bb3f4f7547c6b9e109f31d14569f490c3"}, + {file = "numpy-1.17.4-cp36-cp36m-win_amd64.whl", hash = "sha256:8d0af8d3664f142414fd5b15cabfd3b6cc3ef242a3c7a7493257025be5a6955f"}, + {file = "numpy-1.17.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:9679831005fb16c6df3dd35d17aa31dc0d4d7573d84f0b44cc481490a65c7725"}, + {file = "numpy-1.17.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:acbf5c52db4adb366c064d0b7c7899e3e778d89db585feadd23b06b587d64761"}, + {file = "numpy-1.17.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:3d52298d0be333583739f1aec9026f3b09fdfe3ddf7c7028cb16d9d2af1cca7e"}, + {file = "numpy-1.17.4-cp37-cp37m-win32.whl", hash = "sha256:475963c5b9e116c38ad7347e154e5651d05a2286d86455671f5b1eebba5feb76"}, + {file = "numpy-1.17.4-cp37-cp37m-win_amd64.whl", hash = "sha256:0c0763787133dfeec19904c22c7e358b231c87ba3206b211652f8cbe1241deb6"}, + {file = "numpy-1.17.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:683828e50c339fc9e68720396f2de14253992c495fdddef77a1e17de55f1decc"}, + {file = "numpy-1.17.4-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e2e9d8c87120ba2c591f60e32736b82b67f72c37ba88a4c23c81b5b8fa49c018"}, + {file = "numpy-1.17.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:a8f67ebfae9f575d85fa859b54d3bdecaeece74e3274b0b5c5f804d7ca789fe1"}, + {file = "numpy-1.17.4-cp38-cp38-win32.whl", hash = "sha256:0a7a1dd123aecc9f0076934288ceed7fd9a81ba3919f11a855a7887cbe82a02f"}, + {file = "numpy-1.17.4-cp38-cp38-win_amd64.whl", hash = "sha256:ada4805ed51f5bcaa3a06d3dd94939351869c095e30a2b54264f5a5004b52170"}, + {file = "numpy-1.17.4.zip", hash = "sha256:f58913e9227400f1395c7b800503ebfdb0772f1c33ff8cb4d6451c06cabdf316"}, +] +packaging = [ + {file = "packaging-19.2-py2.py3-none-any.whl", hash = "sha256:d9551545c6d761f3def1677baf08ab2a3ca17c56879e70fecba2fc4dde4ed108"}, + {file = "packaging-19.2.tar.gz", hash = "sha256:28b924174df7a2fa32c1953825ff29c61e2f5e082343165438812f00d3a7fc47"}, +] picamera = [] -pillow = ["01a501be4ae05fd714d269cb9c9f145518e58e73faa3f140ddb67fae0c2607b1", "051de330a06c99d6f84bcf582960487835bcae3fc99365185dc2d4f65a390c0e", "07c35919f983c2c593498edcc126ad3a94154184899297cc9d27a6587672cbaa", "0ae5289948c5e0a16574750021bd8be921c27d4e3527800dc9c2c1d2abc81bf7", "0b1efce03619cdbf8bcc61cfae81fcda59249a469f31c6735ea59badd4a6f58a", "0cf0208500df8d0c3cad6383cd98a2d038b0678fd4f777a8f7e442c5faeee81d", "163136e09bd1d6c6c6026b0a662976e86c58b932b964f255ff384ecc8c3cefa3", "18e912a6ccddf28defa196bd2021fe33600cbe5da1aa2f2e2c6df15f720b73d1", "24ec3dea52339a610d34401d2d53d0fb3c7fd08e34b20c95d2ad3973193591f1", "267f8e4c0a1d7e36e97c6a604f5b03ef58e2b81c1becb4fccecddcb37e063cc7", "3273a28734175feebbe4d0a4cde04d4ed20f620b9b506d26f44379d3c72304e1", "39fbd5d62167197318a0371b2a9c699ce261b6800bb493eadde2ba30d868fe8c", "4132c78200372045bb348fcad8d52518c8f5cfc077b1089949381ee4a61f1c6d", "4baab2d2da57b0d9d544a2ce0f461374dd90ccbcf723fe46689aff906d43a964", "4c678e23006798fc8b6f4cef2eaad267d53ff4c1779bd1af8725cc11b72a63f3", "4d4bc2e6bb6861103ea4655d6b6f67af8e5336e7216e20fff3e18ffa95d7a055", "505738076350a337c1740a31646e1de09a164c62c07db3b996abdc0f9d2e50cf", "5233664eadfa342c639b9b9977190d64ad7aca4edc51a966394d7e08e7f38a9f", "52e2e56fc3706d8791761a157115dc8391319720ad60cc32992350fda74b6be2", "5337ac3280312aa065ed0a8ec1e4b6142e9f15c31baed36b5cd964745853243f", "5ccd97e0f01f42b7e35907272f0f8ad2c3660a482d799a0c564c7d50e83604d4", "5d95cb9f6cced2628f3e4de7e795e98b2659dfcc7176ab4a01a8b48c2c2f488f", "634209852cc06c0c1243cc74f8fdc8f7444d866221de51125f7b696d775ec5ca", "75d1f20bd8072eff92c5f457c266a61619a02d03ece56544195c56d41a1a0522", "7eda4c737637af74bac4b23aa82ea6fbb19002552be85f0b89bc27e3a762d239", "801ddaa69659b36abf4694fed5aa9f61d1ecf2daaa6c92541bbbbb775d97b9fe", "825aa6d222ce2c2b90d34a0ea31914e141a85edefc07e17342f1d2fdf121c07c", "87fe838f9dac0597f05f2605c0700b1926f9390c95df6af45d83141e0c514bd9", "9c215442ff8249d41ff58700e91ef61d74f47dfd431a50253e1a1ca9436b0697", "a3d90022f2202bbb14da991f26ca7a30b7e4c62bf0f8bf9825603b22d7e87494", "a631fd36a9823638fe700d9225f9698fb59d049c942d322d4c09544dc2115356", "a6523a23a205be0fe664b6b8747a5c86d55da960d9586db039eec9f5c269c0e6", "a756ecf9f4b9b3ed49a680a649af45a8767ad038de39e6c030919c2f443eb000", "ac036b6a6bac7010c58e643d78c234c2f7dc8bb7e591bd8bc3555cf4b1527c28", "b117287a5bdc81f1bac891187275ec7e829e961b8032c9e5ff38b70fd036c78f", "ba04f57d1715ca5ff74bb7f8a818bf929a204b3b3c2c2826d1e1cc3b1c13398c", "ba6ef2bd62671c7fb9cdb3277414e87a5cd38b86721039ada1464f7452ad30b2", "c8939dba1a37960a502b1a030a4465c46dd2c2bca7adf05fa3af6bea594e720e", "cd878195166723f30865e05d87cbaf9421614501a4bd48792c5ed28f90fd36ca", "cee815cc62d136e96cf76771b9d3eb58e0777ec18ea50de5cfcede8a7c429aa8", "d1722b7aa4b40cf93ac3c80d3edd48bf93b9208241d166a14ad8e7a20ee1d4f3", "d7c1c06246b05529f9984435fc4fa5a545ea26606e7f450bdbe00c153f5aeaad", "db418635ea20528f247203bf131b40636f77c8209a045b89fa3badb89e1fcea0", "e1555d4fda1db8005de72acf2ded1af660febad09b4708430091159e8ae1963e", "e9c8066249c040efdda84793a2a669076f92a301ceabe69202446abb4c5c5ef9", "e9f13711780c981d6eadd6042af40e172548c54b06266a1aabda7de192db0838", "f0e3288b92ca5dbb1649bd00e80ef652a72b657dc94989fa9c348253d179054b", "f227d7e574d050ff3996049e086e1f18c7bd2d067ef24131e50a1d3fe5831fbc", "f62b1aeb5c2ced8babd4fbba9c74cbef9de309f5ed106184b12d9778a3971f15", "f71ff657e63a9b24cac254bb8c9bd3c89c7a1b5e00ee4b3997ca1c18100dac28", "fc9a12aad714af36cf3ad0275a96a733526571e52710319855628f476dcb144e"] -pygments = ["2a3fe295e54a20164a9df49c75fa58526d3be48e14aceba6d6b1e8ac0bfd6f1b", "98c8aa5a9f778fcd1026a17361ddaf7330d1b7c62ae97c3bb0ae73e0b9b6b0fe"] -pylint = ["3db5468ad013380e987410a8d6956226963aed94ecb5f9d3a28acca6d9ac36cd", "886e6afc935ea2590b462664b161ca9a5e40168ea99e5300935f6591ad467df4"] -pyparsing = ["20f995ecd72f2a1f4bf6b072b63b22e2eb457836601e76d6e5dfcd75436acc1f", "4ca62001be367f01bd3e92ecbb79070272a9d4964dce6a48a82ff0b8bc7e683a"] -pytz = ["1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d", "b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be"] -pyyaml = ["3d7da3009c0f3e783b2c873687652d83b1bbfd5c88e9813fb7e5b03c0dd3108b", "3ef3092145e9b70e3ddd2c7ad59bdd0252a94dfe3949721633e41344de00a6bf", "40c71b8e076d0550b2e6380bada1f1cd1017b882f7e16f09a65be98e017f211a", "558dd60b890ba8fd982e05941927a3911dc409a63dcb8b634feaa0cda69330d3", "a7c28b45d9f99102fa092bb213aa12e0aaf9a6a1f5e395d36166639c1f96c3a1", "aa7dd4a6a427aed7df6fb7f08a580d68d9b118d90310374716ae90b710280af1", "bc558586e6045763782014934bfaf39d48b8ae85a2713117d16c39864085c613", "d46d7982b62e0729ad0175a9bc7e10a566fc07b224d2c79fafb5e032727eaa04", "d5eef459e30b09f5a098b9cea68bebfeb268697f78d647bd255a085371ac7f3f", "e01d3203230e1786cd91ccfdc8f8454c8069c91bee3962ad93b87a4b2860f537", "e170a9e6fcfd19021dd29845af83bb79236068bf5fd4df3327c1be18182b2531"] -requests = ["11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", "9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"] -rope = ["6b728fdc3e98a83446c27a91fc5d56808a004f8beab7a31ab1d7224cecc7d969", "c5c5a6a87f7b1a2095fb311135e2a3d1f194f5ecb96900fdd0a9100881f48aaf", "f0dcf719b63200d492b85535ebe5ea9b29e0d0b8aebeb87fe03fc1a65924fdaf"] -"rpi.gpio" = ["a4210ad63bfe844e43995286de0d3950dfacfa0f3799bb9392770ac54a7d2e47"] -scipy = ["03b1e0775edbe6a4c64effb05fff2ce1429b76d29d754aa5ee2d848b60033351", "09d008237baabf52a5d4f5a6fcf9b3c03408f3f61a69c404472a16861a73917e", "10325f0ffac2400b1ec09537b7e403419dcd25d9fee602a44e8a32119af9079e", "1db9f964ed9c52dc5bd6127f0dd90ac89791daa690a5665cc01eae185912e1ba", "409846be9d6bdcbd78b9e5afe2f64b2da5a923dd7c1cd0615ce589489533fdbb", "4907040f62b91c2e170359c3d36c000af783f0fa1516a83d6c1517cde0af5340", "6c0543f2fdd38dee631fb023c0f31c284a532d205590b393d72009c14847f5b1", "826b9f5fbb7f908a13aa1efd4b7321e36992f5868d5d8311c7b40cf9b11ca0e7", "a7695a378c2ce402405ea37b12c7a338a8755e081869bd6b95858893ceb617ae", "a84c31e8409b420c3ca57fd30c7589378d6fdc8d155d866a7f8e6e80dec6fd06", "adadeeae5500de0da2b9e8dd478520d0a9945b577b2198f2462555e68f58e7ef", "b283a76a83fe463c9587a2c88003f800e08c3929dfbeba833b78260f9c209785", "c19a7389ab3cd712058a8c3c9ffd8d27a57f3d84b9c91a931f542682bb3d269d", "c3bb4bd2aca82fb498247deeac12265921fe231502a6bc6edea3ee7fe6c40a7a", "c5ea60ece0c0c1c849025bfc541b60a6751b491b6f11dd9ef37ab5b8c9041921", "db61a640ca20f237317d27bc658c1fc54c7581ff7f6502d112922dc285bdabee"] -six = ["1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd", "30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66"] -snowballstemmer = ["209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0", "df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52"] -sphinx = ["3b16e48e791a322d584489ab28d8800652123d1fbfdd173e2965a31d40bf22d7", "559c1a8ed1365a982f77650720b41114414139a635692a23c2990824d0a84cf2"] -sphinxcontrib-applehelp = ["edaa0ab2b2bc74403149cb0209d6775c96de797dfd5b5e2a71981309efab3897", "fb8dee85af95e5c30c91f10e7eb3c8967308518e0f7488a2828ef7bc191d0d5d"] -sphinxcontrib-devhelp = ["6c64b077937330a9128a4da74586e8c2130262f014689b4b89e2d08ee7294a34", "9512ecb00a2b0821a146736b39f7aeb90759834b07e81e8cc23a9c70bacb9981"] -sphinxcontrib-htmlhelp = ["4670f99f8951bd78cd4ad2ab962f798f5618b17675c35c5ac3b2132a14ea8422", "d4fd39a65a625c9df86d7fa8a2d9f3cd8299a3a4b15db63b50aac9e161d8eff7"] -sphinxcontrib-httpdomain = ["1fb5375007d70bf180cdd1c79e741082be7aa2d37ba99efe561e1c2e3f38191e", "ac40b4fba58c76b073b03931c7b8ead611066a6aebccafb34dc19694f4eb6335"] -sphinxcontrib-jsmath = ["2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", "a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"] -sphinxcontrib-qthelp = ["513049b93031beb1f57d4daea74068a4feb77aa5630f856fcff2e50de14e9a20", "79465ce11ae5694ff165becda529a600c754f4bc459778778c7017374d4d406f"] -sphinxcontrib-serializinghtml = ["c0efb33f8052c04fd7a26c0a07f1678e8512e0faec19f4aa8f2473a8b81d5227", "db6615af393650bf1151a6cd39120c29abaf93cc60db8c48eb2dddbfdc3a9768"] -toml = ["229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", "235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e", "f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"] -typed-ast = ["1170afa46a3799e18b4c977777ce137bb53c7485379d9706af8a59f2ea1aa161", "18511a0b3e7922276346bcb47e2ef9f38fb90fd31cb9223eed42c85d1312344e", "262c247a82d005e43b5b7f69aff746370538e176131c32dda9cb0f324d27141e", "2b907eb046d049bcd9892e3076c7a6456c93a25bebfe554e931620c90e6a25b0", "354c16e5babd09f5cb0ee000d54cfa38401d8b8891eefa878ac772f827181a3c", "48e5b1e71f25cfdef98b013263a88d7145879fbb2d5185f2a0c79fa7ebbeae47", "4e0b70c6fc4d010f8107726af5fd37921b666f5b31d9331f0bd24ad9a088e631", "630968c5cdee51a11c05a30453f8cd65e0cc1d2ad0d9192819df9978984529f4", "66480f95b8167c9c5c5c87f32cf437d585937970f3fc24386f313a4c97b44e34", "71211d26ffd12d63a83e079ff258ac9d56a1376a25bc80b1cdcdf601b855b90b", "7954560051331d003b4e2b3eb822d9dd2e376fa4f6d98fee32f452f52dd6ebb2", "838997f4310012cf2e1ad3803bce2f3402e9ffb71ded61b5ee22617b3a7f6b6e", "95bd11af7eafc16e829af2d3df510cecfd4387f6453355188342c3e79a2ec87a", "bc6c7d3fa1325a0c6613512a093bc2a2a15aeec350451cbdf9e1d4bffe3e3233", "cc34a6f5b426748a507dd5d1de4c1978f2eb5626d51326e43280941206c209e1", "d755f03c1e4a51e9b24d899561fec4ccaf51f210d52abdf8c07ee2849b212a36", "d7c45933b1bdfaf9f36c579671fec15d25b06c8398f113dab64c18ed1adda01d", "d896919306dd0aa22d0132f62a1b78d11aaf4c9fc5b3410d3c666b818191630a", "fdc1c9bbf79510b76408840e009ed65958feba92a88833cdceecff93ae8fff66", "ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12"] -urllib3 = ["a8a318824cc77d1fd4b2bec2ded92646630d7fe8619497b142c84a9e6f5a7293", "f3c5fd51747d450d4dcf6f923c81f78f811aab8205fda64b0aba34a4e48b0745"] -werkzeug = ["7280924747b5733b246fe23972186c6b348f9ae29724135a6dfc1e53cea433e7", "e5f4a1f98b52b18a93da705a7458e55afb26f32bff83ff5d19189f92462d65c4"] -wrapt = ["565a021fd19419476b9362b05eeaa094178de64f8361e44468f9e9d7843901e1"] +pillow = [ + {file = "Pillow-5.4.1-cp27-cp27m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:18e912a6ccddf28defa196bd2021fe33600cbe5da1aa2f2e2c6df15f720b73d1"}, + {file = "Pillow-5.4.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:267f8e4c0a1d7e36e97c6a604f5b03ef58e2b81c1becb4fccecddcb37e063cc7"}, + {file = "Pillow-5.4.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:051de330a06c99d6f84bcf582960487835bcae3fc99365185dc2d4f65a390c0e"}, + {file = "Pillow-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:825aa6d222ce2c2b90d34a0ea31914e141a85edefc07e17342f1d2fdf121c07c"}, + {file = "Pillow-5.4.1-cp27-cp27m-win_amd64.whl", hash = "sha256:5d95cb9f6cced2628f3e4de7e795e98b2659dfcc7176ab4a01a8b48c2c2f488f"}, + {file = "Pillow-5.4.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:ba04f57d1715ca5ff74bb7f8a818bf929a204b3b3c2c2826d1e1cc3b1c13398c"}, + {file = "Pillow-5.4.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:f227d7e574d050ff3996049e086e1f18c7bd2d067ef24131e50a1d3fe5831fbc"}, + {file = "Pillow-5.4.1-cp34-cp34m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:3273a28734175feebbe4d0a4cde04d4ed20f620b9b506d26f44379d3c72304e1"}, + {file = "Pillow-5.4.1-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:cee815cc62d136e96cf76771b9d3eb58e0777ec18ea50de5cfcede8a7c429aa8"}, + {file = "Pillow-5.4.1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:4d4bc2e6bb6861103ea4655d6b6f67af8e5336e7216e20fff3e18ffa95d7a055"}, + {file = "Pillow-5.4.1-cp34-cp34m-win32.whl", hash = "sha256:a6523a23a205be0fe664b6b8747a5c86d55da960d9586db039eec9f5c269c0e6"}, + {file = "Pillow-5.4.1-cp34-cp34m-win_amd64.whl", hash = "sha256:505738076350a337c1740a31646e1de09a164c62c07db3b996abdc0f9d2e50cf"}, + {file = "Pillow-5.4.1-cp35-cp35m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:7eda4c737637af74bac4b23aa82ea6fbb19002552be85f0b89bc27e3a762d239"}, + {file = "Pillow-5.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:163136e09bd1d6c6c6026b0a662976e86c58b932b964f255ff384ecc8c3cefa3"}, + {file = "Pillow-5.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9c215442ff8249d41ff58700e91ef61d74f47dfd431a50253e1a1ca9436b0697"}, + {file = "Pillow-5.4.1-cp35-cp35m-win32.whl", hash = "sha256:0ae5289948c5e0a16574750021bd8be921c27d4e3527800dc9c2c1d2abc81bf7"}, + {file = "Pillow-5.4.1-cp35-cp35m-win_amd64.whl", hash = "sha256:801ddaa69659b36abf4694fed5aa9f61d1ecf2daaa6c92541bbbbb775d97b9fe"}, + {file = "Pillow-5.4.1-cp36-cp36m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:cd878195166723f30865e05d87cbaf9421614501a4bd48792c5ed28f90fd36ca"}, + {file = "Pillow-5.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:fc9a12aad714af36cf3ad0275a96a733526571e52710319855628f476dcb144e"}, + {file = "Pillow-5.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d7c1c06246b05529f9984435fc4fa5a545ea26606e7f450bdbe00c153f5aeaad"}, + {file = "Pillow-5.4.1-cp36-cp36m-win32.whl", hash = "sha256:0b1efce03619cdbf8bcc61cfae81fcda59249a469f31c6735ea59badd4a6f58a"}, + {file = "Pillow-5.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:a631fd36a9823638fe700d9225f9698fb59d049c942d322d4c09544dc2115356"}, + {file = "Pillow-5.4.1-cp37-cp37m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:24ec3dea52339a610d34401d2d53d0fb3c7fd08e34b20c95d2ad3973193591f1"}, + {file = "Pillow-5.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:e9c8066249c040efdda84793a2a669076f92a301ceabe69202446abb4c5c5ef9"}, + {file = "Pillow-5.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:4c678e23006798fc8b6f4cef2eaad267d53ff4c1779bd1af8725cc11b72a63f3"}, + {file = "Pillow-5.4.1-cp37-cp37m-win32.whl", hash = "sha256:b117287a5bdc81f1bac891187275ec7e829e961b8032c9e5ff38b70fd036c78f"}, + {file = "Pillow-5.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:d1722b7aa4b40cf93ac3c80d3edd48bf93b9208241d166a14ad8e7a20ee1d4f3"}, + {file = "Pillow-5.4.1-pp260-pypy_41-win32.whl", hash = "sha256:a3d90022f2202bbb14da991f26ca7a30b7e4c62bf0f8bf9825603b22d7e87494"}, + {file = "Pillow-5.4.1-pp360-pp360-win32.whl", hash = "sha256:a756ecf9f4b9b3ed49a680a649af45a8767ad038de39e6c030919c2f443eb000"}, + {file = "Pillow-5.4.1-py2.7-macosx-10.13-x86_64.egg", hash = "sha256:634209852cc06c0c1243cc74f8fdc8f7444d866221de51125f7b696d775ec5ca"}, + {file = "Pillow-5.4.1-py2.7-win-amd64.egg", hash = "sha256:0cf0208500df8d0c3cad6383cd98a2d038b0678fd4f777a8f7e442c5faeee81d"}, + {file = "Pillow-5.4.1-py2.7-win32.egg", hash = "sha256:f71ff657e63a9b24cac254bb8c9bd3c89c7a1b5e00ee4b3997ca1c18100dac28"}, + {file = "Pillow-5.4.1-py3.4-win-amd64.egg", hash = "sha256:01a501be4ae05fd714d269cb9c9f145518e58e73faa3f140ddb67fae0c2607b1"}, + {file = "Pillow-5.4.1-py3.4-win32.egg", hash = "sha256:4baab2d2da57b0d9d544a2ce0f461374dd90ccbcf723fe46689aff906d43a964"}, + {file = "Pillow-5.4.1-py3.5-win-amd64.egg", hash = "sha256:f62b1aeb5c2ced8babd4fbba9c74cbef9de309f5ed106184b12d9778a3971f15"}, + {file = "Pillow-5.4.1-py3.5-win32.egg", hash = "sha256:e9f13711780c981d6eadd6042af40e172548c54b06266a1aabda7de192db0838"}, + {file = "Pillow-5.4.1-py3.6-win-amd64.egg", hash = "sha256:07c35919f983c2c593498edcc126ad3a94154184899297cc9d27a6587672cbaa"}, + {file = "Pillow-5.4.1-py3.6-win32.egg", hash = "sha256:87fe838f9dac0597f05f2605c0700b1926f9390c95df6af45d83141e0c514bd9"}, + {file = "Pillow-5.4.1-py3.7-win-amd64.egg", hash = "sha256:c8939dba1a37960a502b1a030a4465c46dd2c2bca7adf05fa3af6bea594e720e"}, + {file = "Pillow-5.4.1-py3.7-win32.egg", hash = "sha256:5337ac3280312aa065ed0a8ec1e4b6142e9f15c31baed36b5cd964745853243f"}, + {file = "Pillow-5.4.1.tar.gz", hash = "sha256:5233664eadfa342c639b9b9977190d64ad7aca4edc51a966394d7e08e7f38a9f"}, + {file = "Pillow-5.4.1.win-amd64-py2.7.exe", hash = "sha256:e1555d4fda1db8005de72acf2ded1af660febad09b4708430091159e8ae1963e"}, + {file = "Pillow-5.4.1.win-amd64-py3.4.exe", hash = "sha256:52e2e56fc3706d8791761a157115dc8391319720ad60cc32992350fda74b6be2"}, + {file = "Pillow-5.4.1.win-amd64-py3.5.exe", hash = "sha256:ba6ef2bd62671c7fb9cdb3277414e87a5cd38b86721039ada1464f7452ad30b2"}, + {file = "Pillow-5.4.1.win-amd64-py3.6.exe", hash = "sha256:39fbd5d62167197318a0371b2a9c699ce261b6800bb493eadde2ba30d868fe8c"}, + {file = "Pillow-5.4.1.win-amd64-py3.7.exe", hash = "sha256:5ccd97e0f01f42b7e35907272f0f8ad2c3660a482d799a0c564c7d50e83604d4"}, + {file = "Pillow-5.4.1.win32-py2.7.exe", hash = "sha256:db418635ea20528f247203bf131b40636f77c8209a045b89fa3badb89e1fcea0"}, + {file = "Pillow-5.4.1.win32-py3.4.exe", hash = "sha256:ac036b6a6bac7010c58e643d78c234c2f7dc8bb7e591bd8bc3555cf4b1527c28"}, + {file = "Pillow-5.4.1.win32-py3.5.exe", hash = "sha256:f0e3288b92ca5dbb1649bd00e80ef652a72b657dc94989fa9c348253d179054b"}, + {file = "Pillow-5.4.1.win32-py3.6.exe", hash = "sha256:4132c78200372045bb348fcad8d52518c8f5cfc077b1089949381ee4a61f1c6d"}, + {file = "Pillow-5.4.1.win32-py3.7.exe", hash = "sha256:75d1f20bd8072eff92c5f457c266a61619a02d03ece56544195c56d41a1a0522"}, +] +pygments = [ + {file = "Pygments-2.5.2-py2.py3-none-any.whl", hash = "sha256:2a3fe295e54a20164a9df49c75fa58526d3be48e14aceba6d6b1e8ac0bfd6f1b"}, + {file = "Pygments-2.5.2.tar.gz", hash = "sha256:98c8aa5a9f778fcd1026a17361ddaf7330d1b7c62ae97c3bb0ae73e0b9b6b0fe"}, +] +pylint = [ + {file = "pylint-2.4.4-py3-none-any.whl", hash = "sha256:886e6afc935ea2590b462664b161ca9a5e40168ea99e5300935f6591ad467df4"}, + {file = "pylint-2.4.4.tar.gz", hash = "sha256:3db5468ad013380e987410a8d6956226963aed94ecb5f9d3a28acca6d9ac36cd"}, +] +pyparsing = [ + {file = "pyparsing-2.4.5-py2.py3-none-any.whl", hash = "sha256:20f995ecd72f2a1f4bf6b072b63b22e2eb457836601e76d6e5dfcd75436acc1f"}, + {file = "pyparsing-2.4.5.tar.gz", hash = "sha256:4ca62001be367f01bd3e92ecbb79070272a9d4964dce6a48a82ff0b8bc7e683a"}, +] +pytz = [ + {file = "pytz-2019.3-py2.py3-none-any.whl", hash = "sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d"}, + {file = "pytz-2019.3.tar.gz", hash = "sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be"}, +] +pyyaml = [ + {file = "PyYAML-3.13-cp27-cp27m-win32.whl", hash = "sha256:d5eef459e30b09f5a098b9cea68bebfeb268697f78d647bd255a085371ac7f3f"}, + {file = "PyYAML-3.13-cp27-cp27m-win_amd64.whl", hash = "sha256:e01d3203230e1786cd91ccfdc8f8454c8069c91bee3962ad93b87a4b2860f537"}, + {file = "PyYAML-3.13-cp34-cp34m-win32.whl", hash = "sha256:558dd60b890ba8fd982e05941927a3911dc409a63dcb8b634feaa0cda69330d3"}, + {file = "PyYAML-3.13-cp34-cp34m-win_amd64.whl", hash = "sha256:d46d7982b62e0729ad0175a9bc7e10a566fc07b224d2c79fafb5e032727eaa04"}, + {file = "PyYAML-3.13-cp35-cp35m-win32.whl", hash = "sha256:a7c28b45d9f99102fa092bb213aa12e0aaf9a6a1f5e395d36166639c1f96c3a1"}, + {file = "PyYAML-3.13-cp35-cp35m-win_amd64.whl", hash = "sha256:bc558586e6045763782014934bfaf39d48b8ae85a2713117d16c39864085c613"}, + {file = "PyYAML-3.13-cp36-cp36m-win32.whl", hash = "sha256:40c71b8e076d0550b2e6380bada1f1cd1017b882f7e16f09a65be98e017f211a"}, + {file = "PyYAML-3.13-cp36-cp36m-win_amd64.whl", hash = "sha256:3d7da3009c0f3e783b2c873687652d83b1bbfd5c88e9813fb7e5b03c0dd3108b"}, + {file = "PyYAML-3.13-cp37-cp37m-win32.whl", hash = "sha256:e170a9e6fcfd19021dd29845af83bb79236068bf5fd4df3327c1be18182b2531"}, + {file = "PyYAML-3.13-cp37-cp37m-win_amd64.whl", hash = "sha256:aa7dd4a6a427aed7df6fb7f08a580d68d9b118d90310374716ae90b710280af1"}, + {file = "PyYAML-3.13.tar.gz", hash = "sha256:3ef3092145e9b70e3ddd2c7ad59bdd0252a94dfe3949721633e41344de00a6bf"}, +] +requests = [ + {file = "requests-2.22.0-py2.py3-none-any.whl", hash = "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"}, + {file = "requests-2.22.0.tar.gz", hash = "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4"}, +] +rope = [ + {file = "rope-0.14.0-py2-none-any.whl", hash = "sha256:6b728fdc3e98a83446c27a91fc5d56808a004f8beab7a31ab1d7224cecc7d969"}, + {file = "rope-0.14.0-py3-none-any.whl", hash = "sha256:f0dcf719b63200d492b85535ebe5ea9b29e0d0b8aebeb87fe03fc1a65924fdaf"}, + {file = "rope-0.14.0.tar.gz", hash = "sha256:c5c5a6a87f7b1a2095fb311135e2a3d1f194f5ecb96900fdd0a9100881f48aaf"}, +] +"rpi.gpio" = [ + {file = "RPi.GPIO-0.6.5.tar.gz", hash = "sha256:a4210ad63bfe844e43995286de0d3950dfacfa0f3799bb9392770ac54a7d2e47"}, +] +scipy = [ + {file = "scipy-1.3.0-cp35-cp35m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:4907040f62b91c2e170359c3d36c000af783f0fa1516a83d6c1517cde0af5340"}, + {file = "scipy-1.3.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:1db9f964ed9c52dc5bd6127f0dd90ac89791daa690a5665cc01eae185912e1ba"}, + {file = "scipy-1.3.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:adadeeae5500de0da2b9e8dd478520d0a9945b577b2198f2462555e68f58e7ef"}, + {file = "scipy-1.3.0-cp35-cp35m-win32.whl", hash = "sha256:03b1e0775edbe6a4c64effb05fff2ce1429b76d29d754aa5ee2d848b60033351"}, + {file = "scipy-1.3.0-cp35-cp35m-win_amd64.whl", hash = "sha256:a7695a378c2ce402405ea37b12c7a338a8755e081869bd6b95858893ceb617ae"}, + {file = "scipy-1.3.0-cp36-cp36m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:826b9f5fbb7f908a13aa1efd4b7321e36992f5868d5d8311c7b40cf9b11ca0e7"}, + {file = "scipy-1.3.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:b283a76a83fe463c9587a2c88003f800e08c3929dfbeba833b78260f9c209785"}, + {file = "scipy-1.3.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:db61a640ca20f237317d27bc658c1fc54c7581ff7f6502d112922dc285bdabee"}, + {file = "scipy-1.3.0-cp36-cp36m-win32.whl", hash = "sha256:409846be9d6bdcbd78b9e5afe2f64b2da5a923dd7c1cd0615ce589489533fdbb"}, + {file = "scipy-1.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:c19a7389ab3cd712058a8c3c9ffd8d27a57f3d84b9c91a931f542682bb3d269d"}, + {file = "scipy-1.3.0-cp37-cp37m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:09d008237baabf52a5d4f5a6fcf9b3c03408f3f61a69c404472a16861a73917e"}, + {file = "scipy-1.3.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:a84c31e8409b420c3ca57fd30c7589378d6fdc8d155d866a7f8e6e80dec6fd06"}, + {file = "scipy-1.3.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:c5ea60ece0c0c1c849025bfc541b60a6751b491b6f11dd9ef37ab5b8c9041921"}, + {file = "scipy-1.3.0-cp37-cp37m-win32.whl", hash = "sha256:6c0543f2fdd38dee631fb023c0f31c284a532d205590b393d72009c14847f5b1"}, + {file = "scipy-1.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:10325f0ffac2400b1ec09537b7e403419dcd25d9fee602a44e8a32119af9079e"}, + {file = "scipy-1.3.0.tar.gz", hash = "sha256:c3bb4bd2aca82fb498247deeac12265921fe231502a6bc6edea3ee7fe6c40a7a"}, +] +six = [ + {file = "six-1.13.0-py2.py3-none-any.whl", hash = "sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd"}, + {file = "six-1.13.0.tar.gz", hash = "sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66"}, +] +snowballstemmer = [ + {file = "snowballstemmer-2.0.0-py2.py3-none-any.whl", hash = "sha256:209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0"}, + {file = "snowballstemmer-2.0.0.tar.gz", hash = "sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52"}, +] +sphinx = [ + {file = "Sphinx-2.2.2-py3-none-any.whl", hash = "sha256:3b16e48e791a322d584489ab28d8800652123d1fbfdd173e2965a31d40bf22d7"}, + {file = "Sphinx-2.2.2.tar.gz", hash = "sha256:559c1a8ed1365a982f77650720b41114414139a635692a23c2990824d0a84cf2"}, +] +sphinxcontrib-applehelp = [ + {file = "sphinxcontrib-applehelp-1.0.1.tar.gz", hash = "sha256:edaa0ab2b2bc74403149cb0209d6775c96de797dfd5b5e2a71981309efab3897"}, + {file = "sphinxcontrib_applehelp-1.0.1-py2.py3-none-any.whl", hash = "sha256:fb8dee85af95e5c30c91f10e7eb3c8967308518e0f7488a2828ef7bc191d0d5d"}, +] +sphinxcontrib-devhelp = [ + {file = "sphinxcontrib-devhelp-1.0.1.tar.gz", hash = "sha256:6c64b077937330a9128a4da74586e8c2130262f014689b4b89e2d08ee7294a34"}, + {file = "sphinxcontrib_devhelp-1.0.1-py2.py3-none-any.whl", hash = "sha256:9512ecb00a2b0821a146736b39f7aeb90759834b07e81e8cc23a9c70bacb9981"}, +] +sphinxcontrib-htmlhelp = [ + {file = "sphinxcontrib-htmlhelp-1.0.2.tar.gz", hash = "sha256:4670f99f8951bd78cd4ad2ab962f798f5618b17675c35c5ac3b2132a14ea8422"}, + {file = "sphinxcontrib_htmlhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:d4fd39a65a625c9df86d7fa8a2d9f3cd8299a3a4b15db63b50aac9e161d8eff7"}, +] +sphinxcontrib-httpdomain = [ + {file = "sphinxcontrib-httpdomain-1.7.0.tar.gz", hash = "sha256:ac40b4fba58c76b073b03931c7b8ead611066a6aebccafb34dc19694f4eb6335"}, + {file = "sphinxcontrib_httpdomain-1.7.0-py2.py3-none-any.whl", hash = "sha256:1fb5375007d70bf180cdd1c79e741082be7aa2d37ba99efe561e1c2e3f38191e"}, +] +sphinxcontrib-jsmath = [ + {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, + {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, +] +sphinxcontrib-qthelp = [ + {file = "sphinxcontrib-qthelp-1.0.2.tar.gz", hash = "sha256:79465ce11ae5694ff165becda529a600c754f4bc459778778c7017374d4d406f"}, + {file = "sphinxcontrib_qthelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:513049b93031beb1f57d4daea74068a4feb77aa5630f856fcff2e50de14e9a20"}, +] +sphinxcontrib-serializinghtml = [ + {file = "sphinxcontrib-serializinghtml-1.1.3.tar.gz", hash = "sha256:c0efb33f8052c04fd7a26c0a07f1678e8512e0faec19f4aa8f2473a8b81d5227"}, + {file = "sphinxcontrib_serializinghtml-1.1.3-py2.py3-none-any.whl", hash = "sha256:db6615af393650bf1151a6cd39120c29abaf93cc60db8c48eb2dddbfdc3a9768"}, +] +toml = [ + {file = "toml-0.10.0-py2.7.egg", hash = "sha256:f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"}, + {file = "toml-0.10.0-py2.py3-none-any.whl", hash = "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"}, + {file = "toml-0.10.0.tar.gz", hash = "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c"}, +] +typed-ast = [ + {file = "typed_ast-1.4.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:262c247a82d005e43b5b7f69aff746370538e176131c32dda9cb0f324d27141e"}, + {file = "typed_ast-1.4.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:71211d26ffd12d63a83e079ff258ac9d56a1376a25bc80b1cdcdf601b855b90b"}, + {file = "typed_ast-1.4.0-cp35-cp35m-win32.whl", hash = "sha256:630968c5cdee51a11c05a30453f8cd65e0cc1d2ad0d9192819df9978984529f4"}, + {file = "typed_ast-1.4.0-cp35-cp35m-win_amd64.whl", hash = "sha256:ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12"}, + {file = "typed_ast-1.4.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:4e0b70c6fc4d010f8107726af5fd37921b666f5b31d9331f0bd24ad9a088e631"}, + {file = "typed_ast-1.4.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:bc6c7d3fa1325a0c6613512a093bc2a2a15aeec350451cbdf9e1d4bffe3e3233"}, + {file = "typed_ast-1.4.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:cc34a6f5b426748a507dd5d1de4c1978f2eb5626d51326e43280941206c209e1"}, + {file = "typed_ast-1.4.0-cp36-cp36m-win32.whl", hash = "sha256:d896919306dd0aa22d0132f62a1b78d11aaf4c9fc5b3410d3c666b818191630a"}, + {file = "typed_ast-1.4.0-cp36-cp36m-win_amd64.whl", hash = "sha256:354c16e5babd09f5cb0ee000d54cfa38401d8b8891eefa878ac772f827181a3c"}, + {file = "typed_ast-1.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95bd11af7eafc16e829af2d3df510cecfd4387f6453355188342c3e79a2ec87a"}, + {file = "typed_ast-1.4.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:18511a0b3e7922276346bcb47e2ef9f38fb90fd31cb9223eed42c85d1312344e"}, + {file = "typed_ast-1.4.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d7c45933b1bdfaf9f36c579671fec15d25b06c8398f113dab64c18ed1adda01d"}, + {file = "typed_ast-1.4.0-cp37-cp37m-win32.whl", hash = "sha256:d755f03c1e4a51e9b24d899561fec4ccaf51f210d52abdf8c07ee2849b212a36"}, + {file = "typed_ast-1.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2b907eb046d049bcd9892e3076c7a6456c93a25bebfe554e931620c90e6a25b0"}, + {file = "typed_ast-1.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fdc1c9bbf79510b76408840e009ed65958feba92a88833cdceecff93ae8fff66"}, + {file = "typed_ast-1.4.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:7954560051331d003b4e2b3eb822d9dd2e376fa4f6d98fee32f452f52dd6ebb2"}, + {file = "typed_ast-1.4.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:48e5b1e71f25cfdef98b013263a88d7145879fbb2d5185f2a0c79fa7ebbeae47"}, + {file = "typed_ast-1.4.0-cp38-cp38-win32.whl", hash = "sha256:1170afa46a3799e18b4c977777ce137bb53c7485379d9706af8a59f2ea1aa161"}, + {file = "typed_ast-1.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:838997f4310012cf2e1ad3803bce2f3402e9ffb71ded61b5ee22617b3a7f6b6e"}, + {file = "typed_ast-1.4.0.tar.gz", hash = "sha256:66480f95b8167c9c5c5c87f32cf437d585937970f3fc24386f313a4c97b44e34"}, +] +urllib3 = [ + {file = "urllib3-1.25.7-py2.py3-none-any.whl", hash = "sha256:a8a318824cc77d1fd4b2bec2ded92646630d7fe8619497b142c84a9e6f5a7293"}, + {file = "urllib3-1.25.7.tar.gz", hash = "sha256:f3c5fd51747d450d4dcf6f923c81f78f811aab8205fda64b0aba34a4e48b0745"}, +] +webargs = [ + {file = "webargs-5.5.2-py2-none-any.whl", hash = "sha256:3f9dc15de183d356c9a0acc159c100ea0506c0c240c1e6f1d8b308c5fed4dbbd"}, + {file = "webargs-5.5.2-py3-none-any.whl", hash = "sha256:fa4ad3ad9b38bedd26c619264fdc50d7ae014b49186736bca851e5b5228f2a1b"}, + {file = "webargs-5.5.2.tar.gz", hash = "sha256:3beca296598067cec24a0b6f91c0afcc19b6e3c4d84ab026b931669628bb47b4"}, +] +werkzeug = [ + {file = "Werkzeug-0.16.0-py2.py3-none-any.whl", hash = "sha256:e5f4a1f98b52b18a93da705a7458e55afb26f32bff83ff5d19189f92462d65c4"}, + {file = "Werkzeug-0.16.0.tar.gz", hash = "sha256:7280924747b5733b246fe23972186c6b348f9ae29724135a6dfc1e53cea433e7"}, +] +wrapt = [ + {file = "wrapt-1.11.2.tar.gz", hash = "sha256:565a021fd19419476b9362b05eeaa094178de64f8361e44468f9e9d7843901e1"}, +] diff --git a/pyproject.toml b/pyproject.toml index f35db200..c51dc73e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,8 @@ scipy = "1.3.0" # Exact version until we can guarantee a wheel picamera = { git = "https://github.com/rwb27/picamera.git", branch = "lens-shading", optional = true } # Lens shading requires >1.14 or RWB fork, lens-shading branch. "RPi.GPIO" = { version = "^0.6.5", optional = true } +marshmallow = "^3.3" +webargs = "^5.5.2" [tool.poetry.extras] rpi = ["picamera", "RPi.GPIO"]