Microscope capture objects now just handle on-disk data

This commit is contained in:
Joel Collins 2019-09-16 16:35:32 +01:00
parent a832dfb2f1
commit dc794d4df8
10 changed files with 125 additions and 311 deletions

View file

@ -12,32 +12,11 @@ import atexit
from openflexure_microscope.camera import piexif
from openflexure_microscope.camera.piexif._exceptions import InvalidImageDataError
"""
Attributes:
BASE_CAPTURE_PATH (str): Base path to store all captures
TEMP_CAPTURE_PATH (str): Base path to store all temporary captures (automatically emptied)
"""
PIL_FORMATS = ["JPG", "JPEG", "PNG", "TIF", "TIFF"]
EXIF_FORMATS = ["JPG", "JPEG", "TIF", "TIFF"]
THUMBNAIL_SIZE = (200, 150)
BASE_CAPTURE_PATH = os.path.join(os.path.expanduser("~"), "micrographs")
TEMP_CAPTURE_PATH = os.path.join(BASE_CAPTURE_PATH, "tmp")
# TODO: Move these methods to a camera utilities module?
def clear_tmp():
"""
Removes all files in the ``TEMP_CAPTURE_PATH`` directory
"""
global TEMP_CAPTURE_PATH
if os.path.isdir(TEMP_CAPTURE_PATH):
logging.info("Clearing {}...".format(TEMP_CAPTURE_PATH))
shutil.rmtree(TEMP_CAPTURE_PATH)
logging.debug("Cleared {}.".format(TEMP_CAPTURE_PATH))
def pull_usercomment_dict(filepath):
"""
@ -68,11 +47,11 @@ def make_file_list(directory, formats):
return files
def build_captures_from_exif():
global BASE_CAPTURE_PATH, EXIF_FORMATS
def build_captures_from_exif(capture_path):
global EXIF_FORMATS
logging.debug("Reloading captures from {}...".format(BASE_CAPTURE_PATH))
files = make_file_list(BASE_CAPTURE_PATH, EXIF_FORMATS)
logging.debug("Reloading captures from {}...".format(capture_path))
files = make_file_list(capture_path, EXIF_FORMATS)
captures = []
for f in files:
@ -119,27 +98,20 @@ def capture_from_exif(path, exif_dict):
class CaptureObject(object):
"""
StreamObject used to store and process capture data, and metadata.
StreamObject used to store and process on-disk capture data, and metadata.
Attributes:
timestring (str): Timestring of capture creation time
temporary (bool): Mark the capture as temporary, to be deleted as the server closes,
or as resources are required
_metadata (dict): Dictionary of custom metadata to be included in metadata file
tags (list): List of tags. Essentially just as extra custom metadata field, but useful for quick organisation
bytestream (:py:class:`io.BytesIO`): Byte bytestream that data will be written to
filefolder (str): Folder in which the capture file will be stored
filename (str): Full name of the capture file
basename (str): Filename of the capture, without a file extension
format (str): Format of the capture data
Notes:
Captures cannot be stored in a lower-level directory than BASE_CAPTURE_PATH.
"""
def __init__(
self, write_to_file: bool = False, temporary: bool = False, filepath: str = ""
) -> None:
def __init__(self, filepath) -> None:
"""Create a new StreamObject, to manage capture data."""
# Store a nice ID
@ -147,14 +119,8 @@ class CaptureObject(object):
logging.debug("Created StreamObject {}".format(self.id))
self.timestring = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
# Keep on disk after close by default
self.temporary = temporary
# Create file name. Default to UUID
if not filepath:
self.file = self.build_file_path()
else:
self.file = filepath
self.file = filepath
self.split_file_path(self.file)
# Dictionary for storing custom metadata
@ -163,54 +129,11 @@ class CaptureObject(object):
# List for storing tags
self.tags = []
# Byte bytestream properties
self.bytestream = io.BytesIO()
# Set default write target
if not write_to_file:
self.stream = self.bytestream
else:
self.stream = self.file
# Log if created by context manager
self.context_manager = False
# Thumbnail (populated only for PIL captures)
self.thumb_bytes = None
def __enter__(self):
"""Create StreamObject in context, to auto-clean disk data."""
logging.debug(
"Entering context for {}. Stored files will be cleaned up automatically regardless of location.".format(
self.id
)
)
self.temporary = True # Flag file to be removed on close.
self.context_manager = True # Used in metadata
logging.info("Rebuilding as a temporary capture...")
self.build_file_path()
return self
def __exit__(self, *args):
"""Exit StreamObject, and auto-clean disk data."""
logging.info("Cleaning up {}".format(self.id))
self.close()
def build_file_path(self):
"""
Construct a full file path, based on filename, folder, and file format.
Defaults to UUID.
"""
global TEMP_CAPTURE_PATH, BASE_CAPTURE_PATH
if self.temporary:
base_dir = TEMP_CAPTURE_PATH
else:
base_dir = BASE_CAPTURE_PATH
return os.path.join(
base_dir, self.filename
) # Full file name by joining given folder to given name
def open(self, mode):
return open(self.file, mode)
def split_file_path(self, filepath):
"""
@ -230,19 +153,7 @@ class CaptureObject(object):
os.makedirs(self.filefolder)
@property
def stream_exists(self, auto_rewind=True) -> bool:
"""Check if capture data BytesIO bytestream exists in memory."""
if auto_rewind:
self.bytestream.seek(0) # Rewind the data bytes for reading
if self.bytestream.getvalue(): # If data bytestream contains data
self.bytestream.seek(0) # Rewind the data bytes for reading
return True
else:
self.bytestream.seek(0) # Rewind the data bytes for reading
return False
@property
def file_exists(self) -> bool:
def exists(self) -> bool:
"""Check if capture data file exists on disk."""
if os.path.isfile(self.file):
return True
@ -293,7 +204,7 @@ class CaptureObject(object):
"""
global EXIF_FORMATS
if self.format.upper() in EXIF_FORMATS and self.file_exists:
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)
@ -331,15 +242,6 @@ class CaptureObject(object):
"""
return yaml.dump(self.metadata, default_flow_style=False)
@property
def exists(self) -> bool:
"""
Check if either an in-memory byte stream or on-disk file of capture data exists.
If False, the capture data cannot be obtained.
"""
return self.stream_exists or self.file_exists
@property
def state(self) -> dict:
"""
@ -347,13 +249,7 @@ class CaptureObject(object):
"""
# Create basic state dictionary
d = {"path": self.file, "temporary": self.temporary, "metadata": self.metadata}
# Check bytestream
if self.stream_exists:
d["bytestream"] = True
else:
d["bytestream"] = False
d = {"path": self.file, "metadata": self.metadata}
# Combined availability of data
if self.exists:
@ -367,27 +263,17 @@ class CaptureObject(object):
def data(self) -> io.BytesIO:
"""
Return a byte string of the capture data.
If the capture data exists in-memory, this will be loaded. If the data only exists
on disk, this will automatically fall back to loading from disk.
"""
self.bytestream.seek(0) # Rewind the data bytes for reading
if self.stream_exists: # If data bytestream contains data
if self.exists: # If data file exists
logging.info("Opening from file {}".format(self.file))
with open(self.file, "rb") as f:
d = io.BytesIO(f.read()) # Load bytes from file
d.seek(0) # Rewind loaded bytestream
# Create a copy of the bytestream bytes
logging.debug("STREAM EXISTS")
data = io.BytesIO(self.bytestream.getbuffer())
else: # If data bytestream is empty
if self.file_exists: # If data file exists
logging.info("Opening from file {}".format(self.file))
with open(self.file, "rb") as f:
d = io.BytesIO(f.read()) # Load bytes from file
d.seek(0) # Rewind loaded bytestream
# Create a copy of the bytestream bytes
data = io.BytesIO(d.getbuffer())
else:
data = None
data = io.BytesIO(d.getbuffer())
else:
data = None
return data # Read and return bytes data
@ -418,41 +304,13 @@ class CaptureObject(object):
data = io.BytesIO(self.thumb_bytes.getbuffer())
return data
def load_file(self) -> bool:
"""Load data stored on disk to the in-memory bytestream."""
if self.file_exists: # If data file exists
with open(self.file, "rb") as f:
self.bytestream = io.BytesIO(f.read()) # Load bytes from file
self.bytestream.seek(0) # Rewind data bytes again
return True
else:
return False
def save_file(self) -> bool:
"""Write the StreamObjects bytestream to a file."""
if self.stream_exists: # If there's a bytestream to save
with open(self.file, "ab") as f: # Load file as bytes
logging.debug("Writing bytestream to file {}".format(self.file))
f.seek(0, 0) # Seek to the start of the file
f.write(self.binary) # Write data bytes to file
return True
else:
return False
def save(self) -> None:
"""Write stream to file, and save/update metadata file"""
# Try to save the file (only succeeds if an unsaved stream exists)
self.save_file()
# If a stream OR file exists, save the metadata file
if self.exists:
self.save_metadata()
def delete_stream(self):
"""Clear the BytesIO bytestream of the StreamObject."""
self.bytestream = io.BytesIO()
def delete_file(self) -> bool:
def delete(self) -> bool:
"""If the StreamObject has been saved, delete the file."""
if os.path.isfile(self.file):
@ -463,25 +321,10 @@ class CaptureObject(object):
else:
return False
def delete(self):
"""Entirely delete all capture data."""
logging.info("Deleting {}".format(self.id))
self.delete_stream()
self.delete_file()
def shunt(self):
"""Demote the StreamObject from being stored in memory."""
if not self.file_exists: # If file doesn't already exist
self.save() # Save bytestream to disk, if it exists
self.delete_stream() # Delete the bytestream from memory
# TODO: Remove
pass
def close(self):
"""Both clear the bytestream, and delete any associated on-disk data."""
logging.info("Closing {}".format(self.id))
self.delete_stream()
# Delete the file from disk if temporary
if self.temporary:
self.delete()
atexit.register(clear_tmp)
# TODO: Remove
pass