Merge branch 'optimised-capture-threadpools' into 'master'

Optimised capture IO

See merge request openflexure/openflexure-microscope-server!62
This commit is contained in:
Joel Collins 2020-06-15 16:28:25 +00:00
commit aa9f621476
12 changed files with 220 additions and 175 deletions

View file

@ -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

View file

@ -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")

View file

@ -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")

View file

@ -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

View file

@ -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

View file

@ -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:

View file

@ -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

View file

@ -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):

View file

@ -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):

115
poetry.lock generated
View file

@ -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"},

View file

@ -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"]

View file

@ -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