diff --git a/openflexure_microscope/api/app.py b/openflexure_microscope/api/app.py index b1f83a20..61112ad0 100644 --- a/openflexure_microscope/api/app.py +++ b/openflexure_microscope/api/app.py @@ -3,13 +3,20 @@ from gevent import monkey monkey.patch_all() +import sys import time import atexit import logging, logging.handlers +# Look for debug flag +if "-d" in sys.argv or "--debug" in sys.argv: + log_level = logging.DEBUG +else: + log_level = logging.INFO + # Set root logger level logger = logging.getLogger() -logger.setLevel(logging.INFO) +logger.setLevel(log_level) import os import pkg_resources diff --git a/openflexure_microscope/api/v2/views/streams.py b/openflexure_microscope/api/v2/views/streams.py index 7fe23d2d..f8b8cbe7 100644 --- a/openflexure_microscope/api/v2/views/streams.py +++ b/openflexure_microscope/api/v2/views/streams.py @@ -25,7 +25,7 @@ class MjpegStream(PropertyView): frame from the camera object, passed to the Flask response, and then repeats until the connection is closed. - Without monkey patching with gevent, or using a native threaded server, the stream + Without monkey patching, or using a native threaded server, the stream will block all proceeding requests. """ microscope = find_component("org.openflexure.microscope") diff --git a/openflexure_microscope/camera/base.py b/openflexure_microscope/camera/base.py index d8531a1f..d099ee43 100644 --- a/openflexure_microscope/camera/base.py +++ b/openflexure_microscope/camera/base.py @@ -2,12 +2,10 @@ import time import os import shutil -import threading import datetime import logging import threading -import gevent from abc import ABCMeta, abstractmethod @@ -25,7 +23,7 @@ class BaseCamera(metaclass=ABCMeta): self.thread = None self.camera = None - self.lock = StrictLock(timeout=1, name="Camera") + self.lock = StrictLock(name="Camera") self.frame = None self.last_access = 0 @@ -93,9 +91,10 @@ class BaseCamera(metaclass=ABCMeta): self.stop = False if not self.stream_active: - # Spawn a greenlet to handle stream - # start background frame thread - self.thread = gevent.spawn(self._thread) + # Start background frame thread + self.thread = threading.Thread(target=self._thread) + self.thread.daemon = True + self.thread.start() # wait until frames are available logging.info("Waiting for frames") diff --git a/openflexure_microscope/camera/mock.py b/openflexure_microscope/camera/mock.py index 9dc727c4..ec61bc7e 100644 --- a/openflexure_microscope/camera/mock.py +++ b/openflexure_microscope/camera/mock.py @@ -197,19 +197,14 @@ class MissingCamera(BaseCamera): bayer (bool): Store raw bayer data in capture """ - if isinstance(output, CaptureObject): - target = output.file - else: - target = output - with self.lock: - if isinstance(target, str): - target = open(target, "wb") + if isinstance(output, str): + output = open(output, "wb") - target.write(self.stream.getvalue()) + output.write(self.stream.getvalue()) - if isinstance(target, str): - target.close() + if isinstance(output, str): + output.close() # HANDLE STREAM FRAMES diff --git a/openflexure_microscope/camera/pi.py b/openflexure_microscope/camera/pi.py index a73ea5d1..1ed7f5e2 100644 --- a/openflexure_microscope/camera/pi.py +++ b/openflexure_microscope/camera/pi.py @@ -108,18 +108,14 @@ class PiCameraStreamer(BaseCamera): 1312, 976, ) #: tuple: Resolution for numpy array captures - self.jpeg_quality = 75 #: int: JPEG quality + self.jpeg_quality = 100 #: int: JPEG quality + self.mjpeg_quality = 75 #: int: MJPEG quality # Set default lens shading table path self.picamera_lst_path = settings_file_path( "picamera_lst.npy" ) #: str: Path of .npy lens shading table file - # Create an empty stream - self.stream = io.BytesIO() - - # Start streaming - self.start_worker() @property def configuration(self): @@ -159,6 +155,7 @@ class PiCameraStreamer(BaseCamera): "image_resolution": self.image_resolution, "numpy_resolution": self.numpy_resolution, "jpeg_quality": self.jpeg_quality, + "mjpeg_quality": self.mjpeg_quality, "picamera": {}, } ) @@ -479,7 +476,7 @@ class PiCameraStreamer(BaseCamera): self.camera.start_recording( self.stream, format="mjpeg", - quality=self.jpeg_quality, + quality=self.mjpeg_quality, bitrate=-1, # RWB: disable bitrate control # (bitrate control makes JPEG size less good as a focus # metric) @@ -520,12 +517,6 @@ class PiCameraStreamer(BaseCamera): Returns: output_object (str/BytesIO): Target object. """ - - if isinstance(output, CaptureObject): - target = output.file - else: - target = output - with self.lock: logging.info("Capturing to {}".format(output)) @@ -535,20 +526,20 @@ class PiCameraStreamer(BaseCamera): time.sleep(0.1) self.camera.capture( - target, + output, format=fmt, - quality=100, + quality=self.jpeg_quality, resize=resize, bayer=(not use_video_port) and bayer, use_video_port=use_video_port, ) - time.sleep(0.1) + #time.sleep(0.1) # Set resolution and start stream recording if necessary if not use_video_port: self.start_stream_recording() - return target + return output def yuv( self, use_video_port: bool = True, resize: Tuple[int, int] = None diff --git a/openflexure_microscope/captures/capture.py b/openflexure_microscope/captures/capture.py index 70bee3ec..ed617236 100644 --- a/openflexure_microscope/captures/capture.py +++ b/openflexure_microscope/captures/capture.py @@ -10,6 +10,9 @@ from PIL import Image import dateutil.parser import atexit +from gevent.fileobject import FileObjectThread +import gevent + from collections import OrderedDict from openflexure_microscope.camera import piexif @@ -118,12 +121,18 @@ def capture_from_exif(path, exif_dict): class CaptureObject(object): """ - StreamObject used to store and process on-disk capture data, and metadata. + File-like object used to store and process on-disk capture data, and metadata. Serves to simplify modifying properties of on-disk capture data. """ def __init__(self, filepath) -> None: """Create a new StreamObject, to manage capture data.""" + # Stream for buffering capture data + self.stream = io.BytesIO() + # Event to notify when the stream has finished writing to disk + self.file_ready = gevent.event.Event() + # Access lock for the disk file + self.lock = gevent.lock.BoundedSemaphore() # Store a nice ID self.id = uuid.uuid4() #: str: Unique capture ID @@ -148,6 +157,23 @@ class CaptureObject(object): # Thumbnail (populated only for PIL captures) self.thumb_bytes = None + def write(self, s): + self.stream.write(s) + + def _stream_to_file(self): + with self.lock: + logging.info(f"Writing to disk {self.file}") + with FileObjectThread(open(self.file, "wb"), 'wb') as outfile: + outfile.write(self.stream.getbuffer()) + self.stream.close() + self.file_ready.set() + logging.info(f"Finished writing to disk {self.file}") + + def flush(self): + logging.debug(f"Flushing {self.file}") + gevent.spawn(self._stream_to_file) + logging.debug(f"Returning flushing {self.file}") + def open(self, mode): return open(self.file, mode) @@ -227,18 +253,20 @@ class CaptureObject(object): global EXIF_FORMATS if self.format.upper() in EXIF_FORMATS and self.exists: - logging.debug("Writing exif data to capture file") - # Extract current Exif data - exif_dict = piexif.load(self.file) - # Serialize metadata - metadata_string = json.dumps(self.metadata, cls=JSONEncoder) - logging.debug(f"Saving metadata string to file: {metadata_string}") - # Insert metadata into exif_dict - exif_dict["Exif"][piexif.ExifIFD.UserComment] = metadata_string.encode() - # Convert new exif dict to exif bytes - exif_bytes = piexif.dump(exif_dict) - # Insert exif into file - piexif.insert(exif_bytes, self.file) + self.file_ready.wait() + with self.lock: + logging.debug("Writing exif data to capture file") + # Extract current Exif data + exif_dict = piexif.load(self.file) + # Serialize metadata + metadata_string = json.dumps(self.metadata, cls=JSONEncoder) + logging.debug(f"Saving metadata string to file: {metadata_string}") + # Insert metadata into exif_dict + exif_dict["Exif"][piexif.ExifIFD.UserComment] = metadata_string.encode() + # Convert new exif dict to exif bytes + exif_bytes = piexif.dump(exif_dict) + # Insert exif into file + piexif.insert(exif_bytes, self.file) @property def metadata(self) -> dict: diff --git a/openflexure_microscope/microscope.py b/openflexure_microscope/microscope.py index c1fe348d..3733a2e4 100644 --- a/openflexure_microscope/microscope.py +++ b/openflexure_microscope/microscope.py @@ -7,6 +7,8 @@ import pkg_resources import uuid from typing import Tuple +import gevent + from openflexure_microscope.captures import CaptureManager from openflexure_microscope.stage.mock import MissingStage @@ -151,39 +153,41 @@ class Microscope: Return: dict: Dictionary containing complete microscope state """ - state = {"camera": self.camera.state, "stage": self.stage.state} - return state + with self.lock: + state = {"camera": self.camera.state, "stage": self.stage.state} + return state def update_settings(self, settings: dict): """ Applies a settings dictionary to the microscope. Missing parameters will be left untouched. """ - logging.debug("Microscope: Applying settings: {}".format(settings)) + with self.lock: + logging.debug("Microscope: Applying settings: {}".format(settings)) - # If attached to a camera - if ("camera" in settings) and self.camera: - self.camera.update_settings(settings.get("camera", {})) + # If attached to a camera + if ("camera" in settings) and self.camera: + self.camera.update_settings(settings.get("camera", {})) - # If attached to a stage - if ("stage" in settings) and self.stage: - self.stage.update_settings(settings.get("stage", {})) + # If attached to a stage + if ("stage" in settings) and self.stage: + self.stage.update_settings(settings.get("stage", {})) - # Capture manager - self.captures.update_settings(settings.get("captures", {})) + # Capture manager + self.captures.update_settings(settings.get("captures", {})) - # Microscope settings - if "id" in settings: - self.id = settings["id"] - if "name" in settings: - self.name = settings["name"] - if "fov" in settings: - self.fov = settings["fov"] + # Microscope settings + if "id" in settings: + self.id = settings["id"] + if "name" in settings: + self.name = settings["name"] + if "fov" in settings: + self.fov = settings["fov"] - # Extension settings - if "extensions" in settings: - self.extension_settings.update(settings["extensions"]) + # Extension settings + if "extensions" in settings: + self.extension_settings.update(settings["extensions"]) - # TODO: warn if there are settings that we silently ignore + # TODO: warn if there are settings that we silently ignore def read_settings(self, full: bool = True): """ @@ -195,49 +199,50 @@ class Microscope: This is to ensure that settings for currently disconnected hardware don't get removed from the settings file. """ + with self.lock: - settings_current = { - "id": self.id, - "name": self.name, - "fov": self.fov, - "extensions": self.extension_settings, - } + settings_current = { + "id": self.id, + "name": self.name, + "fov": self.fov, + "extensions": self.extension_settings, + } - # If attached to a camera - if self.camera: - settings_current_camera = self.camera.read_settings() - settings_current["camera"] = settings_current_camera + # If attached to a camera + if self.camera: + settings_current_camera = self.camera.read_settings() + settings_current["camera"] = settings_current_camera - # Store an encoded copy of the PiCamera lens shading table, if it exists - if hasattr(self.camera, "read_lens_shading_table"): - # Read LST. Returns None if no LST is active - lst_arr = self.camera.read_lens_shading_table() + # Store an encoded copy of the PiCamera lens shading table, if it exists + if hasattr(self.camera, "read_lens_shading_table"): + # Read LST. Returns None if no LST is active + lst_arr = self.camera.read_lens_shading_table() - if lst_arr is not None: - b64_string, dtype, shape = serialise_array_b64(lst_arr) + if lst_arr is not None: + b64_string, dtype, shape = serialise_array_b64(lst_arr) - settings_current["camera"]["lens_shading_table"] = { - "@type": "ndarray", - "b64_string": b64_string, - "dtype": dtype, - "shape": shape, - } + settings_current["camera"]["lens_shading_table"] = { + "@type": "ndarray", + "b64_string": b64_string, + "dtype": dtype, + "shape": shape, + } - # If attached to a stage - if self.stage: - settings_current_stage = self.stage.read_settings() - settings_current["stage"] = settings_current_stage + # If attached to a stage + if self.stage: + settings_current_stage = self.stage.read_settings() + settings_current["stage"] = settings_current_stage - # Capture manager - settings_current_captures = self.captures.read_settings() - settings_current["captures"] = settings_current_captures + # Capture manager + settings_current_captures = self.captures.read_settings() + settings_current["captures"] = settings_current_captures - settings_full = self.settings_file.merge(settings_current) + settings_full = self.settings_file.merge(settings_current) - if full: - return settings_full - else: - return settings_current + if full: + return settings_full + else: + return settings_current def save_settings(self): """ @@ -286,6 +291,21 @@ class Microscope: return system_metadata + def add_metadata_to_capture(self, output, metadata, annotations, tags): + logging.debug(f"Waiting for {output.file}") + # Wait for the file to be written to disk + output.file_ready.wait() + logging.info(f"Asynchronously injecting EXIF data into {output.file}") + # Inject system metadata + output.put_metadata({"instrument": self.metadata}) + # Insert custom metadata + output.put_metadata(metadata) + # Insert custom metadata + output.put_annotations(annotations) + # Insert custom tags + output.put_tags(tags) + logging.info(f"Finished injecting EXIF data into {output.file}") + def capture( self, filename: str = None, @@ -299,6 +319,7 @@ class Microscope: tags: list = None, metadata: dict = None, ): + logging.debug(f"Microscope capturing to {filename}") if not annotations: annotations = {} if not metadata: @@ -313,21 +334,18 @@ class Microscope: ) # Capture to output object + logging.info("Starting microscope capture...") self.camera.capture( - output.file, + output, use_video_port=use_video_port, resize=resize, bayer=bayer, fmt=fmt, ) - # Inject system metadata - output.put_metadata({"instrument": self.metadata}) - # Insert custom metadata - output.put_metadata(metadata) - # Insert custom metadata - output.put_annotations(annotations) - # Insert custom tags - output.put_tags(tags) + # Gether metadata from hardware in a greenlet + gevent.get_hub().threadpool.spawn(self.add_metadata_to_capture, output, metadata, annotations, tags) + + logging.info(f"Finished capture to {output.file}") return output diff --git a/openflexure_microscope/stage/base.py b/openflexure_microscope/stage/base.py index b04909b2..092d401a 100644 --- a/openflexure_microscope/stage/base.py +++ b/openflexure_microscope/stage/base.py @@ -11,7 +11,7 @@ class BaseStage(metaclass=ABCMeta): """ def __init__(self): - self.lock = StrictLock(timeout=5) + self.lock = StrictLock(name="Stage") @abstractmethod def update_settings(self, config: dict): diff --git a/openflexure_microscope/stage/sanga.py b/openflexure_microscope/stage/sanga.py index 5fa147ae..e4f372d8 100644 --- a/openflexure_microscope/stage/sanga.py +++ b/openflexure_microscope/stage/sanga.py @@ -51,7 +51,8 @@ class SangaStage(BaseStage): @property def position(self): - return self.board.position + with self.lock: + return self.board.position @property def backlash(self): @@ -108,6 +109,7 @@ class SangaStage(BaseStage): backlash: (default: True) whether to correct for backlash. """ with self.lock: + logging.info(f"Moving sangaboard by {displacement}") if not backlash or self.backlash is None: return self.board.move_rel(displacement, axis=axis) @@ -145,6 +147,7 @@ class SangaStage(BaseStage): """Make an absolute move to a position """ with self.lock: + logging.info(f"Moving sangaboard to {final}") self.board.move_abs(final, **kwargs) def zero_position(self): diff --git a/poetry.lock b/poetry.lock index 83301dc0..bf88ebfb 100644 --- a/poetry.lock +++ b/poetry.lock @@ -12,12 +12,12 @@ description = "A pluggable API specification generator. Currently supports the O name = "apispec" optional = false python-versions = ">=3.5" -version = "3.3.0" +version = "3.3.1" [package.extras] -dev = ["PyYAML (>=3.10)", "prance (>=0.11)", "marshmallow (>=2.19.2)", "pytest", "mock", "flake8 (3.7.9)", "flake8-bugbear (20.1.4)", "pre-commit (>=1.20,<3.0)", "tox"] -docs = ["marshmallow (>=2.19.2)", "pyyaml (5.3)", "sphinx (2.4.1)", "sphinx-issues (1.2.0)", "sphinx-rtd-theme (0.4.3)"] -lint = ["flake8 (3.7.9)", "flake8-bugbear (20.1.4)", "pre-commit (>=1.20,<3.0)"] +dev = ["PyYAML (>=3.10)", "prance (>=0.11)", "marshmallow (>=2.19.2)", "pytest", "mock", "flake8 (3.8.2)", "flake8-bugbear (20.1.4)", "pre-commit (>=2.4,<3.0)", "tox"] +docs = ["marshmallow (>=2.19.2)", "pyyaml (5.3.1)", "sphinx (3.0.4)", "sphinx-issues (1.2.0)", "sphinx-rtd-theme (0.4.3)"] +lint = ["flake8 (3.8.2)", "flake8-bugbear (20.1.4)", "pre-commit (>=2.4,<3.0)"] tests = ["PyYAML (>=3.10)", "prance (>=0.11)", "marshmallow (>=2.19.2)", "pytest", "mock"] validation = ["prance (>=0.11)"] yaml = ["PyYAML (>=3.10)"] @@ -36,7 +36,7 @@ description = "An abstract syntax tree for Python with inference support." name = "astroid" optional = false python-versions = ">=3.5" -version = "2.4.1" +version = "2.4.2" [package.dependencies] lazy-object-proxy = ">=1.4.0,<1.5.0" @@ -110,6 +110,7 @@ ofmclient = [] reference = "f1803f5f655b405a6a902f6c697c9f720f6a18da" type = "git" url = "https://gitlab.com/openflexure/microscope-extensions/camera-stage-mapping.git" + [[package]] category = "main" description = "Pure Python CBOR (de)serializer with extensive tag support" @@ -127,7 +128,7 @@ description = "Python package for providing Mozilla's CA Bundle." name = "certifi" optional = false python-versions = "*" -version = "2020.4.5.1" +version = "2020.4.5.2" [[package]] category = "main" @@ -219,11 +220,11 @@ description = "Coroutine-based network library" name = "gevent" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" -version = "20.5.2" +version = "20.6.0" [package.dependencies] cffi = ">=1.12.2" -greenlet = ">=0.4.14" +greenlet = ">=0.4.16" setuptools = "*" "zope.event" = "*" "zope.interface" = "*" @@ -232,8 +233,8 @@ setuptools = "*" dnspython = ["dnspython (>=1.16.0)", "idna"] docs = ["repoze.sphinx.autointerface", "sphinxcontrib-programoutput"] monitor = ["psutil (>=5.6.1)", "psutil (5.6.3)"] -recommended = ["dnspython (>=1.16.0)", "idna", "cffi (>=1.12.2)", "psutil (>=5.6.1)", "psutil (5.6.3)"] -test = ["dnspython (>=1.16.0)", "idna", "requests", "objgraph", "cffi (>=1.12.2)", "psutil (>=5.6.1)", "psutil (5.6.3)", "futures", "mock", "contextvars (2.4)", "coverage (<5.0)", "coveralls (>=1.7.0)"] +recommended = ["dnspython (>=1.16.0)", "idna", "cffi (>=1.12.2)", "psutil (>=5.6.1)", "psutil (5.6.3)", "selectors2", "backports.socketpair"] +test = ["dnspython (>=1.16.0)", "idna", "requests", "objgraph", "cffi (>=1.12.2)", "psutil (>=5.6.1)", "psutil (5.6.3)", "selectors2", "futures", "mock", "backports.socketpair", "contextvars (2.4)", "coverage (<5.0)", "coveralls (>=1.7.0)"] [[package]] category = "main" @@ -265,11 +266,11 @@ version = "2.9" [[package]] category = "main" -description = "Enumerates all IP addresses on all network adapters of the system." +description = "Cross-platform network interface and IP address enumeration library" name = "ifaddr" optional = false python-versions = "*" -version = "0.1.6" +version = "0.1.7" [[package]] category = "dev" @@ -321,7 +322,7 @@ description = "Python implementation of LabThings, based on the Flask microframe name = "labthings" optional = false python-versions = ">=3.6,<4.0" -version = "0.6.4" +version = "0.6.5" [package.dependencies] Flask = ">=1.1.1,<2.0.0" @@ -332,7 +333,7 @@ gevent = ">=1.4,<21.0" gevent-websocket = ">=0.10.1,<0.11.0" marshmallow = ">=3.4.0,<4.0.0" webargs = ">=6.0.0,<7.0.0" -zeroconf = ">=0.24.5,<0.27.0" +zeroconf = ">=0.24.5,<0.28.0" [[package]] category = "dev" @@ -433,6 +434,7 @@ test = ["coverage", "pytest", "mock", "pillow", "numpy"] reference = "" type = "url" url = "https://github.com/rwb27/picamera/releases/download/v1.13.2b0/picamera-1.13.2b0-py3-none-any.whl" + [[package]] category = "main" description = "Python Imaging Library (Fork)" @@ -475,7 +477,7 @@ description = "python code static checker" name = "pylint" optional = false python-versions = ">=3.5.*" -version = "2.5.2" +version = "2.5.3" [package.dependencies] astroid = ">=2.4.0,<=2.5" @@ -572,7 +574,7 @@ description = "Communication to the Sangaboard unipolar motor driver" name = "sangaboard" optional = false python-versions = "*" -version = "0.2.0" +version = "0.2.3" [package.dependencies] future = "*" @@ -612,7 +614,7 @@ description = "Python documentation generator" name = "sphinx" optional = false python-versions = ">=3.5" -version = "3.0.4" +version = "3.1.1" [package.dependencies] Jinja2 = ">=2.3" @@ -635,7 +637,7 @@ sphinxcontrib-serializinghtml = "*" [package.extras] docs = ["sphinxcontrib-websupport"] -lint = ["flake8 (>=3.5.0)", "flake8-import-order", "mypy (>=0.770)", "docutils-stubs"] +lint = ["flake8 (>=3.5.0)", "flake8-import-order", "mypy (>=0.780)", "docutils-stubs"] test = ["pytest", "pytest-cov", "html5lib", "typed-ast", "cython"] [[package]] @@ -835,7 +837,7 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] rpi = ["RPi.GPIO"] [metadata] -content-hash = "1c577dec4c5e870bacd76d24991f49cb0c7fed212ace179bea075e2849c91e00" +content-hash = "46a88e0322847fc9c13602f72b7de7f10be81bbddc5c0f87e174a540fb427a7c" python-versions = "^3.6" [metadata.files] @@ -844,16 +846,16 @@ alabaster = [ {file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"}, ] apispec = [ - {file = "apispec-3.3.0-py2.py3-none-any.whl", hash = "sha256:9bf4e51d56c9067c60668b78210ae213894f060f85593dc2ad8805eb7d875a2a"}, - {file = "apispec-3.3.0.tar.gz", hash = "sha256:419d0564b899e182c2af50483ea074db8cb05fee60838be58bb4542095d5c08d"}, + {file = "apispec-3.3.1-py2.py3-none-any.whl", hash = "sha256:b65063e11968a8c26dbbe9b4100ee24026a41cc358dd204df279bdedd7d8dea2"}, + {file = "apispec-3.3.1.tar.gz", hash = "sha256:f5244ccca33f7a81309f6b3c9d458e33e869050c2d861b9f8cee24b3ad739d2b"}, ] appdirs = [ {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, ] astroid = [ - {file = "astroid-2.4.1-py3-none-any.whl", hash = "sha256:d8506842a3faf734b81599c8b98dcc423de863adcc1999248480b18bd31a0f38"}, - {file = "astroid-2.4.1.tar.gz", hash = "sha256:4c17cea3e592c21b6e222f673868961bad77e1f985cb1694ed077475a89229c1"}, + {file = "astroid-2.4.2-py3-none-any.whl", hash = "sha256:bc58d83eb610252fd8de6363e39d4f1d0619c894b0ed24603b881c02e64c7386"}, + {file = "astroid-2.4.2.tar.gz", hash = "sha256:2f4078c2a41bf377eea06d71c9d2ba4eb8f6b1af2135bec27bbbb7d8f12bb703"}, ] attrs = [ {file = "attrs-19.3.0-py2.py3-none-any.whl", hash = "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c"}, @@ -872,8 +874,8 @@ cbor2 = [ {file = "cbor2-5.1.0.tar.gz", hash = "sha256:43ce11e8c2fe4971d386d1a60cf83bfa0a4a667b97668ba76acbf5e6398821aa"}, ] certifi = [ - {file = "certifi-2020.4.5.1-py2.py3-none-any.whl", hash = "sha256:1d987a998c75633c40847cc966fcf5904906c920a7f17ef374f5aa4282abd304"}, - {file = "certifi-2020.4.5.1.tar.gz", hash = "sha256:51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519"}, + {file = "certifi-2020.4.5.2-py2.py3-none-any.whl", hash = "sha256:9cd41137dc19af6a5e03b630eefe7d1f458d964d406342dd3edf625839b944cc"}, + {file = "certifi-2020.4.5.2.tar.gz", hash = "sha256:5ad7e9a056d25ffa5082862e36f119f7f7cec6457fa07ee2f8c339814b80c9b1"}, ] cffi = [ {file = "cffi-1.14.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1cae98a7054b5c9391eb3249b86e0e99ab1e02bb0cc0575da191aedadbdf4384"}, @@ -933,28 +935,28 @@ future = [ {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"}, ] gevent = [ - {file = "gevent-20.5.2-cp27-cp27m-macosx_10_15_x86_64.whl", hash = "sha256:4a69373a07a0d4ff74401274d3b30aa47430e76d7e314f23bb9f399da1bbed6e"}, - {file = "gevent-20.5.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:a55ee5c388511362b1f886be57f06062334bc44b91ef215b997435d21901def7"}, - {file = "gevent-20.5.2-cp27-cp27m-win32.whl", hash = "sha256:2436dfbc1f0cfa2c2fc8ec9e824dcc13f8501085c29605b4488c981456f12ed6"}, - {file = "gevent-20.5.2-cp27-cp27m-win_amd64.whl", hash = "sha256:c2c2e0e275b375941be80a174cd47b3ee20ccf457fbec31863808448ec4fcf59"}, - {file = "gevent-20.5.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:1bb8330f1f86460b845b589eeea73b3e2dd52f84bee30178a8eaa0ddd61622a6"}, - {file = "gevent-20.5.2-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:2863c2899f0a379322cb07724c67eb608652e99ab5bdf5bfce936c9bf89f4a87"}, - {file = "gevent-20.5.2-cp35-cp35m-win32.whl", hash = "sha256:de44cb7bf1f74043e7976f2dbf9780da7a99a2b58799675817d1df77ccfde519"}, - {file = "gevent-20.5.2-cp35-cp35m-win_amd64.whl", hash = "sha256:9cec7379b540cd7a8cdffec9ebe799943c07d0f575e6f222bf3515e7655544d7"}, - {file = "gevent-20.5.2-cp36-cp36m-macosx_10_15_x86_64.whl", hash = "sha256:9c4601f67f26e534237d1cbaded3157b2a70a89e7760c819e8afb20e32878553"}, - {file = "gevent-20.5.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:f2932f05b8cfef3d7c285ddbd77008d642bd843c88625e1da4e96a643d4de885"}, - {file = "gevent-20.5.2-cp36-cp36m-win32.whl", hash = "sha256:08de7d46923ab04529e70ff0601522bc7897b7bcef8ac40e8a9f906168518875"}, - {file = "gevent-20.5.2-cp36-cp36m-win_amd64.whl", hash = "sha256:ae6787acd68bd2f8d40e10fd8d031a5da2ac9dae6ddd35390cf4c18261a8bd38"}, - {file = "gevent-20.5.2-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:12276c30ce4d6da84eba674dade04b0cd80fb5808dbfdef01ba49b75d50b3816"}, - {file = "gevent-20.5.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:4a3ee667456ccec0c1d215aeec8d150449d0a6da5d58ba2c6be741249af1f139"}, - {file = "gevent-20.5.2-cp37-cp37m-win32.whl", hash = "sha256:2f138b82527c520fff432e3aa9f606335d23eebfa1e8aeb0e36822e34a91afdd"}, - {file = "gevent-20.5.2-cp37-cp37m-win_amd64.whl", hash = "sha256:899e1cdbfaa7aa0d53a430f2f4cb8d03c00db0ee94872ce2b4bb918817a719f0"}, - {file = "gevent-20.5.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:bd261c12724e3e4be81ce4bd978c3ba47d31bd56444ba9f30332b8e7d4f09b81"}, - {file = "gevent-20.5.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:1886197bcf8aea097c066e212c32a0aed9ad6a0ed245c124f3ff6b7dce7db354"}, - {file = "gevent-20.5.2-cp38-cp38-win32.whl", hash = "sha256:9146860dd8ac0d0b675abc64025ea8dcd3d399de9290a8eec23bbed6d3993be4"}, - {file = "gevent-20.5.2-cp38-cp38-win_amd64.whl", hash = "sha256:67820fd68ba14a38a1645261fc8f3530c4cfad6c45e29f40cc1dda8d5dcc17fb"}, - {file = "gevent-20.5.2-pp27-pypy_73-win32.whl", hash = "sha256:91bfe6c3a2ec4d0027b30eb1aa678ab1f828c7d8961e64ac000d8c4a80fc8806"}, - {file = "gevent-20.5.2.tar.gz", hash = "sha256:2756de36f56b33c46f6cc7146a74ba65afcd1471922c95b6771ce87b279d689c"}, + {file = "gevent-20.6.0-cp27-cp27m-macosx_10_15_x86_64.whl", hash = "sha256:ccac571d5bb7a85be736c0c5e241a5263a36904b358edc2dcceaa3b54a18eb25"}, + {file = "gevent-20.6.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:eb74efa2486b8c2576eaf2ef6a3a782b1a38fe414390a4a3ab411efca91e54f5"}, + {file = "gevent-20.6.0-cp27-cp27m-win32.whl", hash = "sha256:2701ac60993fb74f1f548348641c78160215aa9707b08e37caf10fe63712f5d9"}, + {file = "gevent-20.6.0-cp27-cp27m-win_amd64.whl", hash = "sha256:1a449ab1390fd48cc509d846a62d867e7c548f8f9d5632ca012af2741c97e6d6"}, + {file = "gevent-20.6.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:e963584dd93b8268764297b13d6d99e24d9f1d63d921d5260286b3cde92f2851"}, + {file = "gevent-20.6.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:56f2949b068e922bce3695e80bbf484115536901ad77e833b0e1ac7e4d996654"}, + {file = "gevent-20.6.0-cp35-cp35m-win32.whl", hash = "sha256:d4376e9d8f6ecf68de182fed99eb7d7f255b15170edb0bc7d06720552f541c6f"}, + {file = "gevent-20.6.0-cp35-cp35m-win_amd64.whl", hash = "sha256:6d172744705aa401b0014142343e866f98668bd86fc5ba070ace22f4e98f76dd"}, + {file = "gevent-20.6.0-cp36-cp36m-macosx_10_15_x86_64.whl", hash = "sha256:664f061993e1104f901801dc7b305824389f5c05936e7daeabee56bf76837726"}, + {file = "gevent-20.6.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:f9521f4721f50f7e1ade81bf61c17387d2cd901de65701038e74c5e87e2785cc"}, + {file = "gevent-20.6.0-cp36-cp36m-win32.whl", hash = "sha256:779ab592efc30550879546bfe83ee6cb42538080fcd495817868ed7b7b17c3ff"}, + {file = "gevent-20.6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:94241c8e962b5eab49d5459b319454161bdb1a0c53ffe883a94a9952ab916345"}, + {file = "gevent-20.6.0-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:77d9237d17a64054e1a03883d3265d27fe98e49f41cf1dd22b42ee3206f99b53"}, + {file = "gevent-20.6.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:5520e72522906483559bd12eacabf72f130972dabe8e05d159e227eca97cbcf9"}, + {file = "gevent-20.6.0-cp37-cp37m-win32.whl", hash = "sha256:8625256ec3acc9b095050d15c256ad9ce3f8bac833982bd712af14a83991b067"}, + {file = "gevent-20.6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:e5e1f2024fbf753549703cde287f6cc940248d860f617d657c3b3ea0638035f0"}, + {file = "gevent-20.6.0-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:3165510ed37f8ea38345a8a2464d5c700f7cc8828194612889f6c583e2889ff1"}, + {file = "gevent-20.6.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:b5f73ef6442e72774359afcf892c47cbdcb0811a4577fc1b4b11191aa362e353"}, + {file = "gevent-20.6.0-cp38-cp38-win32.whl", hash = "sha256:6e232e1870dfd2e32aaa26083488c116492e9378d288dffca0e38c1521ba502d"}, + {file = "gevent-20.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:dffc2d2bb815a6ab0370e3e478094e0c43f610a3466e241bc150af21293763db"}, + {file = "gevent-20.6.0-pp27-pypy_73-win32.whl", hash = "sha256:ea79e58f7927939ac60d3c73aa9e5d6186c3fea77e4414d768d658e1f58ef7a3"}, + {file = "gevent-20.6.0.tar.gz", hash = "sha256:51cab8f792961a75da39452586bb3474eaa1607ab2c83398745ef1b9ba09cb92"}, ] gevent-websocket = [ {file = "gevent-websocket-0.10.1.tar.gz", hash = "sha256:7eaef32968290c9121f7c35b973e2cc302ffb076d018c9068d2f5ca8b2d85fb0"}, @@ -984,7 +986,8 @@ idna = [ {file = "idna-2.9.tar.gz", hash = "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb"}, ] ifaddr = [ - {file = "ifaddr-0.1.6.tar.gz", hash = "sha256:c19c64882a7ad51a394451dabcbbed72e98b5625ec1e79789924d5ea3e3ecb93"}, + {file = "ifaddr-0.1.7-py2.py3-none-any.whl", hash = "sha256:d1f603952f0a71c9ab4e705754511e4e03b02565bc4cec7188ad6415ff534cd3"}, + {file = "ifaddr-0.1.7.tar.gz", hash = "sha256:1f9e8a6ca6f16db5a37d3356f07b6e52344f6f9f7e806d618537731669eb1a94"}, ] imagesize = [ {file = "imagesize-1.2.0-py2.py3-none-any.whl", hash = "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1"}, @@ -1003,8 +1006,8 @@ jinja2 = [ {file = "Jinja2-2.11.2.tar.gz", hash = "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0"}, ] labthings = [ - {file = "labthings-0.6.4-py3-none-any.whl", hash = "sha256:61e540e4635278008b8f851a1b8104199cae92affd043f229f0ffa9f78c0479e"}, - {file = "labthings-0.6.4.tar.gz", hash = "sha256:d734e74716b2348350d31b48f67cbf96c01c88b6b3185bace287e81fd3aa5826"}, + {file = "labthings-0.6.5-py3-none-any.whl", hash = "sha256:f28fb8b6fa05eabc105c5dc987835ebed8784f9357ccd1f6ef9eb32b6280216c"}, + {file = "labthings-0.6.5.tar.gz", hash = "sha256:adadcf9a1588d1d38c12d36939fe902b30e8f56d50c196fc79161ad58e0331a0"}, ] lazy-object-proxy = [ {file = "lazy-object-proxy-1.4.3.tar.gz", hash = "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0"}, @@ -1219,8 +1222,8 @@ pygments = [ {file = "Pygments-2.6.1.tar.gz", hash = "sha256:647344a061c249a3b74e230c739f434d7ea4d8b1d5f3721bc0f3558049b38f44"}, ] pylint = [ - {file = "pylint-2.5.2-py3-none-any.whl", hash = "sha256:dd506acce0427e9e08fb87274bcaa953d38b50a58207170dbf5b36cf3e16957b"}, - {file = "pylint-2.5.2.tar.gz", hash = "sha256:b95e31850f3af163c2283ed40432f053acbc8fc6eba6a069cb518d9dbf71848c"}, + {file = "pylint-2.5.3-py3-none-any.whl", hash = "sha256:d0ece7d223fe422088b0e8f13fa0a1e8eb745ebffcb8ed53d3e95394b6101a1c"}, + {file = "pylint-2.5.3.tar.gz", hash = "sha256:7dd78437f2d8d019717dbf287772d0b2dbdfd13fc016aa7faa08d67bccc46adc"}, ] pynpm = [ {file = "pynpm-0.1.2-py2.py3-none-any.whl", hash = "sha256:3f03fbf667549f8b8b7e0419eef88d1b21833ce288f96de66fbb761b9f4c4061"}, @@ -1255,8 +1258,8 @@ rope = [ {file = "RPi.GPIO-0.6.5.tar.gz", hash = "sha256:a4210ad63bfe844e43995286de0d3950dfacfa0f3799bb9392770ac54a7d2e47"}, ] sangaboard = [ - {file = "sangaboard-0.2.0-py3-none-any.whl", hash = "sha256:510cb95a56e070ef7a9214f6a0673a92f2269e71e125c54b8bb1c16b1abfb095"}, - {file = "sangaboard-0.2.0.tar.gz", hash = "sha256:3636203179e9748cd27a1856d44de2b1c7adf398d033a8d5d382748903a098f0"}, + {file = "sangaboard-0.2.3-py3-none-any.whl", hash = "sha256:3791159d57a749571f89acdce70aa9c1de82afef71f5186d5e2845c7be44698c"}, + {file = "sangaboard-0.2.3.tar.gz", hash = "sha256:ed0c5a9864f5901d11bed0ed2387da3b5555f75fef72a9f35997304447f9b08c"}, ] scipy = [ {file = "scipy-1.4.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:c5cac0c0387272ee0e789e94a570ac51deb01c796b37fb2aad1fb13f85e2f97d"}, @@ -1290,8 +1293,8 @@ snowballstemmer = [ {file = "snowballstemmer-2.0.0.tar.gz", hash = "sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52"}, ] sphinx = [ - {file = "Sphinx-3.0.4-py3-none-any.whl", hash = "sha256:779a519adbd3a70fc7c468af08c5e74829868b0a5b34587b33340e010291856c"}, - {file = "Sphinx-3.0.4.tar.gz", hash = "sha256:ea64df287958ee5aac46be7ac2b7277305b0381d213728c3a49d8bb9b8415807"}, + {file = "Sphinx-3.1.1-py3-none-any.whl", hash = "sha256:97c9e3bcce2f61d9f5edf131299ee9d1219630598d9f9a8791459a4d9e815be5"}, + {file = "Sphinx-3.1.1.tar.gz", hash = "sha256:74fbead182a611ce1444f50218a1c5fc70b6cc547f64948f5182fb30a2a20258"}, ] sphinxcontrib-applehelp = [ {file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"}, diff --git a/pyproject.toml b/pyproject.toml index 94617f35..0048668f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.masonry.api" [tool.poetry] name = "openflexure-microscope-server" -version = "2.1.3" +version = "2.2.0-beta.0" description = "Python module, and Flask-based web API, to run the OpenFlexure Microscope." authors = [ @@ -42,10 +42,11 @@ opencv-python-headless = [ {version = "4.1.0.25", python = "~3.7"}, # PiWheels build for RPi running Py37 {version = "4.2.0.34", python = "^3.8"} # Latest for Py38 systems ] -labthings = "0.6.4" +labthings = "0.6.5" pynpm = "^0.1.2" camera-stage-mapping = {git = "https://gitlab.com/openflexure/microscope-extensions/camera-stage-mapping.git"} -sangaboard = "^0.2.0" +sangaboard = "^0.2" +gevent = "20.6.0" # Pinned version to guarantee piwheels wheel [tool.poetry.extras] rpi = ["RPi.GPIO"] diff --git a/tests/test_camera.py b/tests/test_camera.py index 6ed5d44e..6369a651 100644 --- a/tests/test_camera.py +++ b/tests/test_camera.py @@ -31,7 +31,7 @@ class TestCaptureMethods(unittest.TestCase): with camera.new_image() as output: camera.capture( - output.file, use_video_port=use_video_port, resize=resize + output, use_video_port=use_video_port, resize=resize ) # Ensure file deletion fails and returns False