Went on a PEP-8 rampage
This commit is contained in:
parent
f99ad30fb6
commit
0948c9308a
36 changed files with 186 additions and 218 deletions
|
|
@ -5,4 +5,4 @@ from .microscope import Microscope
|
|||
from . import config
|
||||
from . import utilities
|
||||
from . import task
|
||||
from . import lock
|
||||
from . import lock
|
||||
|
|
|
|||
|
|
@ -1,31 +1,16 @@
|
|||
#!/usr/bin/env python
|
||||
"""
|
||||
TODO: Implement API route to cleanly shut down server
|
||||
TODO: Implement plugin API routes somehow
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
from importlib import import_module
|
||||
import time
|
||||
import datetime
|
||||
import os
|
||||
|
||||
from flask import (
|
||||
Flask, render_template, Response, url_for,
|
||||
redirect, request, jsonify, send_file, abort,
|
||||
make_response)
|
||||
|
||||
from flask.views import MethodView
|
||||
from werkzeug.exceptions import default_exceptions
|
||||
from serial import SerialException
|
||||
Flask, render_template)
|
||||
|
||||
from flask_cors import CORS
|
||||
|
||||
from openflexure_microscope.api.utilities import list_routes
|
||||
from openflexure_microscope.api.exceptions import JSONExceptionHandler
|
||||
|
||||
from openflexure_microscope import Microscope
|
||||
from openflexure_microscope.exceptions import LockError
|
||||
from openflexure_microscope.camera.pi import StreamingCamera
|
||||
from openflexure_microscope.stage.openflexure import Stage
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ from flask import jsonify
|
|||
from werkzeug.exceptions import default_exceptions
|
||||
from werkzeug.exceptions import HTTPException
|
||||
|
||||
|
||||
class JSONExceptionHandler(object):
|
||||
|
||||
def __init__(self, app=None):
|
||||
|
|
@ -24,7 +25,6 @@ class JSONExceptionHandler(object):
|
|||
}
|
||||
return jsonify(response)
|
||||
|
||||
|
||||
def init_app(self, app):
|
||||
self.app = app
|
||||
self.register(HTTPException)
|
||||
|
|
@ -32,4 +32,4 @@ class JSONExceptionHandler(object):
|
|||
self.register(code)
|
||||
|
||||
def register(self, exception_or_code, handler=None):
|
||||
self.app.errorhandler(exception_or_code)(handler or self.std_handler)
|
||||
self.app.errorhandler(exception_or_code)(handler or self.std_handler)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import pprint
|
||||
import logging
|
||||
import copy
|
||||
from werkzeug.exceptions import BadRequest
|
||||
|
||||
|
||||
class JsonPayload:
|
||||
def __init__(self, request):
|
||||
"""
|
||||
|
|
@ -60,10 +60,9 @@ def gen(camera):
|
|||
|
||||
def get_bool(get_arg):
|
||||
"""Convert GET request argument string to a Python bool"""
|
||||
if (
|
||||
get_arg == 'true' or
|
||||
get_arg == 'True' or
|
||||
get_arg == '1'):
|
||||
if (get_arg == 'true' or
|
||||
get_arg == 'True' or
|
||||
get_arg == '1'):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
|
|
|||
|
|
@ -197,4 +197,4 @@ def construct_blueprint(microscope_obj):
|
|||
view_func=ConfigAPI.as_view('config', microscope=microscope_obj)
|
||||
)
|
||||
|
||||
return(blueprint)
|
||||
return blueprint
|
||||
|
|
|
|||
|
|
@ -2,9 +2,7 @@ from openflexure_microscope.api.utilities import gen, get_bool, JsonPayload
|
|||
from openflexure_microscope.api.v1.views import MicroscopeView
|
||||
from openflexure_microscope.utilities import filter_dict
|
||||
|
||||
from flask import Response, Blueprint, jsonify, request, abort, url_for, redirect, send_file
|
||||
|
||||
import logging
|
||||
from flask import Response, jsonify, request, abort, url_for, redirect, send_file
|
||||
|
||||
|
||||
class ListAPI(MicroscopeView):
|
||||
|
|
@ -368,7 +366,7 @@ class MetadataAPI(MicroscopeView):
|
|||
|
||||
# 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.metadataname, as_attachment=as_attachment), code=307)
|
||||
return redirect(url_for('capture_download', capture_id=capture_id, filename=capture_obj.metadataname), code=307)
|
||||
|
||||
# Download the metadata using the requested filename
|
||||
data = capture_obj.yaml
|
||||
|
|
@ -377,6 +375,7 @@ class MetadataAPI(MicroscopeView):
|
|||
data,
|
||||
mimetype="text/yaml")
|
||||
|
||||
|
||||
class TagsAPI(MicroscopeView):
|
||||
def get(self, capture_id):
|
||||
"""
|
||||
|
|
@ -402,7 +401,7 @@ class TagsAPI(MicroscopeView):
|
|||
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'))
|
||||
metadata_tags = filter_dict(capture_obj.state, ['metadata', 'tags'])
|
||||
|
||||
return jsonify(metadata_tags)
|
||||
|
||||
|
|
@ -440,7 +439,7 @@ class TagsAPI(MicroscopeView):
|
|||
for tag in data_dict:
|
||||
capture_obj.put_tag(str(tag))
|
||||
|
||||
metadata_tags = filter_dict(capture_obj.state, ('metadata', 'tags'))
|
||||
metadata_tags = filter_dict(capture_obj.state, ['metadata', 'tags'])
|
||||
|
||||
return jsonify(metadata_tags)
|
||||
|
||||
|
|
@ -477,6 +476,6 @@ class TagsAPI(MicroscopeView):
|
|||
for tag in data_dict:
|
||||
capture_obj.delete_tag(str(tag))
|
||||
|
||||
metadata_tags = filter_dict(capture_obj.state, ('metadata', 'tags'))
|
||||
metadata_tags = filter_dict(capture_obj.state, ['metadata', 'tags'])
|
||||
|
||||
return jsonify(metadata_tags)
|
||||
return jsonify(metadata_tags)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
from openflexure_microscope.api.v1.views import MicroscopeView
|
||||
from openflexure_microscope.api.utilities import JsonPayload
|
||||
|
||||
from flask import Response, Blueprint, jsonify, request, abort, url_for, redirect, send_file
|
||||
|
||||
import logging
|
||||
from flask import jsonify, request
|
||||
|
||||
|
||||
class ZoomAPI(MicroscopeView):
|
||||
|
|
@ -102,4 +100,4 @@ class OverlayAPI(MicroscopeView):
|
|||
self.microscope.camera.camera.annotate_text = text
|
||||
self.microscope.camera.camera.annotate_text_size = size
|
||||
|
||||
return jsonify({'text': text, 'size': size})
|
||||
return jsonify({'text': text, 'size': size})
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
from openflexure_microscope.api.v1.views import MicroscopeView
|
||||
|
||||
from flask import Response, Blueprint, jsonify, request, abort, url_for, redirect, send_file
|
||||
|
||||
import logging
|
||||
from flask import jsonify
|
||||
|
||||
|
||||
class GPUPreviewAPI(MicroscopeView):
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
from openflexure_microscope.api.v1.views import MicroscopeViewPlugin
|
||||
|
||||
from flask import Response, Blueprint, jsonify
|
||||
from flask import Blueprint
|
||||
|
||||
import logging, warnings
|
||||
import logging
|
||||
import warnings
|
||||
|
||||
|
||||
def construct_blueprint(microscope_obj, plugin_paths=[], include_default=True):
|
||||
def construct_blueprint(microscope_obj):
|
||||
|
||||
blueprint = Blueprint('plugin_blueprint', __name__)
|
||||
|
||||
|
|
@ -56,4 +57,4 @@ def construct_blueprint(microscope_obj, plugin_paths=[], include_default=True):
|
|||
"No valid 'api_views' dictionary found in {}".format(plugin_obj)
|
||||
)
|
||||
|
||||
return(blueprint)
|
||||
return blueprint
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ from openflexure_microscope.api.utilities import gen, JsonPayload
|
|||
from openflexure_microscope.api.v1.views import MicroscopeView
|
||||
from openflexure_microscope.utilities import axes_to_array, filter_dict
|
||||
|
||||
from flask import Response, Blueprint, jsonify, request
|
||||
from flask import Blueprint, jsonify, request
|
||||
|
||||
import logging
|
||||
|
||||
|
|
@ -42,7 +42,7 @@ class PositionAPI(MicroscopeView):
|
|||
}
|
||||
|
||||
"""
|
||||
out = filter_dict(self.microscope.state, ('stage', 'position'))
|
||||
out = filter_dict(self.microscope.state, ['stage', 'position'])
|
||||
return jsonify(out)
|
||||
|
||||
def post(self):
|
||||
|
|
@ -85,7 +85,7 @@ class PositionAPI(MicroscopeView):
|
|||
with self.microscope.stage.lock:
|
||||
self.microscope.stage.move_rel(position)
|
||||
|
||||
out = filter_dict(self.microscope.state, ('stage', 'position'))
|
||||
out = filter_dict(self.microscope.state, ['stage', 'position'])
|
||||
|
||||
return jsonify(out)
|
||||
|
||||
|
|
@ -99,4 +99,4 @@ def construct_blueprint(microscope_obj):
|
|||
view_func=PositionAPI.as_view('position', microscope=microscope_obj)
|
||||
)
|
||||
|
||||
return(blueprint)
|
||||
return blueprint
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from openflexure_microscope.api.v1.views import MicroscopeView
|
||||
from flask import jsonify, abort, Blueprint
|
||||
|
||||
|
||||
class TaskListAPI(MicroscopeView):
|
||||
|
||||
def get(self):
|
||||
|
|
@ -65,6 +66,7 @@ class TaskListAPI(MicroscopeView):
|
|||
|
||||
return jsonify(data)
|
||||
|
||||
|
||||
class TaskAPI(MicroscopeView):
|
||||
|
||||
def get(self, task_id):
|
||||
|
|
@ -135,6 +137,7 @@ class TaskAPI(MicroscopeView):
|
|||
|
||||
return jsonify(data)
|
||||
|
||||
|
||||
def construct_blueprint(microscope_obj):
|
||||
|
||||
blueprint = Blueprint('task_blueprint', __name__)
|
||||
|
|
@ -149,4 +152,4 @@ def construct_blueprint(microscope_obj):
|
|||
view_func=TaskAPI.as_view('task', microscope=microscope_obj)
|
||||
)
|
||||
|
||||
return(blueprint)
|
||||
return blueprint
|
||||
|
|
|
|||
|
|
@ -23,4 +23,4 @@ class MicroscopeViewPlugin(MicroscopeView):
|
|||
|
||||
self.plugin = plugin
|
||||
|
||||
MicroscopeView.__init__(self, microscope=microscope, **kwargs)
|
||||
MicroscopeView.__init__(self, microscope=microscope, **kwargs)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import time
|
||||
import io
|
||||
import os
|
||||
import threading
|
||||
from PIL import Image
|
||||
import datetime
|
||||
import yaml
|
||||
import logging
|
||||
|
|
@ -35,6 +33,18 @@ def generate_basename():
|
|||
return datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
||||
|
||||
|
||||
def generate_numbered_basename(obj_list: list) -> str:
|
||||
initial_basename = generate_basename()
|
||||
basename = initial_basename
|
||||
# Handle clashing
|
||||
iterator = 1
|
||||
while basename in [obj.basename for obj in obj_list]:
|
||||
basename = initial_basename + "_{}".format(iterator)
|
||||
iterator += 1
|
||||
|
||||
return basename
|
||||
|
||||
|
||||
class CameraEvent(object):
|
||||
"""
|
||||
A frame-signaller object used by any instances or subclasses of BaseCamera.
|
||||
|
|
@ -79,18 +89,6 @@ class CameraEvent(object):
|
|||
self.events[get_ident()][0].clear()
|
||||
|
||||
|
||||
def generate_basename(obj_list: list) -> str:
|
||||
initial_basename = generate_basename()
|
||||
basename = initial_basename
|
||||
# Handle clashing
|
||||
iterator = 1
|
||||
while basename in [obj.basename for obj in obj_list]:
|
||||
basename = initial_basename + "_{}".format(iterator)
|
||||
iterator += 1
|
||||
|
||||
return basename
|
||||
|
||||
|
||||
class BaseCamera(object):
|
||||
"""
|
||||
Base implementation of StreamingCamera.
|
||||
|
|
@ -155,7 +153,7 @@ class BaseCamera(object):
|
|||
|
||||
# START AND STOP WORKER THREAD
|
||||
|
||||
def start_worker(self, timeout: int=5) -> bool:
|
||||
def start_worker(self, timeout: int = 5) -> bool:
|
||||
"""Start the background camera thread if it isn't running yet."""
|
||||
timeout_time = time.time() + timeout
|
||||
|
||||
|
|
@ -176,7 +174,7 @@ class BaseCamera(object):
|
|||
time.sleep(0)
|
||||
return True
|
||||
|
||||
def stop_worker(self, timeout: int=5) -> bool:
|
||||
def stop_worker(self, timeout: int = 5) -> bool:
|
||||
"""Flag worker thread for stop. Waits for thread close or timeout."""
|
||||
logging.debug("Stopping worker thread")
|
||||
timeout_time = time.time() + timeout
|
||||
|
|
@ -217,13 +215,13 @@ class BaseCamera(object):
|
|||
"""Return the latest recorded video."""
|
||||
return last_entry(self.videos)
|
||||
|
||||
def image_from_id(self, id):
|
||||
def image_from_id(self, image_id):
|
||||
"""Return an image StreamObject with a matching ID."""
|
||||
return entry_by_id(id, self.images)
|
||||
return entry_by_id(image_id, self.images)
|
||||
|
||||
def video_from_id(self, id):
|
||||
def video_from_id(self, video_id):
|
||||
"""Return a video StreamObject with a matching ID."""
|
||||
return entry_by_id(id, self.videos)
|
||||
return entry_by_id(video_id, self.videos)
|
||||
|
||||
# MANAGE CAPTURE DATABASE
|
||||
|
||||
|
|
@ -277,25 +275,24 @@ class BaseCamera(object):
|
|||
|
||||
def new_image(
|
||||
self,
|
||||
write_to_file: bool=False,
|
||||
temporary: bool=True,
|
||||
filename: str=None,
|
||||
fmt: str='jpeg',
|
||||
shunt_others: bool=True):
|
||||
write_to_file: bool = False,
|
||||
temporary: bool = True,
|
||||
filename: str = None,
|
||||
fmt: str = 'jpeg'):
|
||||
|
||||
"""
|
||||
Create a new image capture object. Adds to the image list, and shunt all others.
|
||||
|
||||
Args:
|
||||
write_to_file (bool): Should the StreamObject write to a file, or an in-memory byte stream.
|
||||
keep_on_disk (bool): Should the data be deleted after session ends. Creating the capture with a content manager sets this to true.
|
||||
temporary (bool): Should the data be deleted after session ends. Creating the capture with a content manager sets this to true.
|
||||
filename (str): Name of the stored file. Defaults to timestamp.
|
||||
fmt (str): Format of the capture.
|
||||
"""
|
||||
|
||||
# Generate file name
|
||||
if not filename:
|
||||
filename = generate_basename(self.images)
|
||||
filename = generate_numbered_basename(self.images)
|
||||
logging.debug(filename)
|
||||
|
||||
# Create capture object
|
||||
|
|
@ -317,11 +314,10 @@ class BaseCamera(object):
|
|||
|
||||
def new_video(
|
||||
self,
|
||||
write_to_file: bool=True,
|
||||
temporary: bool=False,
|
||||
filename: str=None,
|
||||
fmt: str='h264',
|
||||
shunt_others: bool=True):
|
||||
write_to_file: bool = True,
|
||||
temporary: bool = False,
|
||||
filename: str = None,
|
||||
fmt: str = 'h264'):
|
||||
|
||||
"""
|
||||
Create a new video capture object. Adds to the image list, and shunt all others.
|
||||
|
|
@ -335,7 +331,7 @@ class BaseCamera(object):
|
|||
|
||||
# Generate file name
|
||||
if not filename:
|
||||
filename = generate_basename(self.videos)
|
||||
filename = generate_numbered_basename(self.videos)
|
||||
logging.debug(filename)
|
||||
|
||||
# Create capture object
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ thumbnail_size = (60, 60)
|
|||
BASE_CAPTURE_PATH = os.path.join(os.path.expanduser('~'), 'micrographs')
|
||||
TEMP_CAPTURE_PATH = os.path.join(BASE_CAPTURE_PATH, 'tmp')
|
||||
|
||||
|
||||
def clear_tmp():
|
||||
global TEMP_CAPTURE_PATH
|
||||
|
||||
|
|
@ -24,6 +25,7 @@ def clear_tmp():
|
|||
os.remove(f)
|
||||
logging.debug("Removed {}".format(f))
|
||||
|
||||
|
||||
def capture_from_dict(capture_dict):
|
||||
capture = CaptureObject(create_metadata_file=False) # Create a placeholder capture
|
||||
|
||||
|
|
@ -48,14 +50,15 @@ class CaptureObject(object):
|
|||
|
||||
Note: Captures cannot be stored in a lower-level directory than BASE_CAPTURE_PATH.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
write_to_file: bool=False,
|
||||
temporary: bool=False,
|
||||
filename: str='',
|
||||
folder: str='',
|
||||
fmt: str='',
|
||||
create_metadata_file: bool=True) -> None:
|
||||
write_to_file: bool = False,
|
||||
temporary: bool = False,
|
||||
filename: str = '',
|
||||
folder: str = '',
|
||||
fmt: str = '',
|
||||
create_metadata_file: bool = True) -> None:
|
||||
"""Create a new StreamObject, to manage capture data."""
|
||||
|
||||
# Store a nice ID
|
||||
|
|
@ -102,7 +105,9 @@ class CaptureObject(object):
|
|||
|
||||
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))
|
||||
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
|
||||
|
||||
|
|
@ -129,7 +134,7 @@ class CaptureObject(object):
|
|||
else:
|
||||
logging.debug("Target for {} set to 'file'".format(self.id))
|
||||
self.stream = self.file
|
||||
|
||||
|
||||
# Save initial metadata file
|
||||
if create_metadata_file:
|
||||
self.save_metadata()
|
||||
|
|
@ -170,7 +175,8 @@ class CaptureObject(object):
|
|||
|
||||
def split_file_path(self, filepath):
|
||||
"""Takes a full file path, and splits it into separated class properties."""
|
||||
self.filefolder, self.filename = os.path.split(filepath) # Split the full file path into a folder and a filename
|
||||
self.filefolder, self.filename = os.path.split(
|
||||
filepath) # Split the full file path into a folder and a filename
|
||||
self.basename = os.path.splitext(self.filename)[0] # Split the filename out from it's file extension
|
||||
self.metadataname = "{}.yaml".format(self.basename)
|
||||
|
||||
|
|
@ -234,18 +240,10 @@ class CaptureObject(object):
|
|||
@property
|
||||
def metadata(self) -> dict:
|
||||
# Create basic metadata dictionary
|
||||
d = {
|
||||
'id': self.id,
|
||||
'filename': self.filename,
|
||||
'path': self.file,
|
||||
'time': self.timestring,
|
||||
'format': self.format,
|
||||
'tags': self.tags
|
||||
}
|
||||
d = {'id': self.id, 'filename': self.filename, 'path': self.file, 'time': self.timestring,
|
||||
'format': self.format, 'tags': self.tags, 'custom': self._metadata}
|
||||
|
||||
# Add custom metadata to dictionary
|
||||
d['custom'] = self._metadata
|
||||
|
||||
return d
|
||||
|
||||
@property
|
||||
|
|
@ -261,14 +259,10 @@ class CaptureObject(object):
|
|||
"""Return dictionary of StreamObject properties."""
|
||||
|
||||
# Create basic state dictionary
|
||||
d = {
|
||||
'locked': self.locked,
|
||||
'temporary': self.temporary,
|
||||
}
|
||||
d = {'locked': self.locked, 'temporary': self.temporary, 'metadata': self.metadata,
|
||||
'metadata_path': self.metadata_file}
|
||||
|
||||
# Add metadata to state
|
||||
d['metadata'] = self.metadata
|
||||
d['metadata_path'] = self.metadata_file
|
||||
|
||||
# Check bytestream
|
||||
if self.stream_exists:
|
||||
|
|
@ -404,4 +398,5 @@ class CaptureObject(object):
|
|||
if self.temporary:
|
||||
self.delete()
|
||||
|
||||
atexit.register(clear_tmp)
|
||||
|
||||
atexit.register(clear_tmp)
|
||||
|
|
|
|||
|
|
@ -36,7 +36,6 @@ def to_map(data, func):
|
|||
Args:
|
||||
data: Input iterable data
|
||||
func: Function to apply to all non-iterable values
|
||||
excluded_keys: Any dictionary keys to exclude from the returned data
|
||||
"""
|
||||
# If the object is a dictionary
|
||||
if isinstance(data, abc.Mapping):
|
||||
|
|
@ -58,7 +57,6 @@ def json_map(data, clean_keys=True):
|
|||
Args:
|
||||
data: Input dictionary
|
||||
clean_keys: Modify any keys unsuitable for JSON return
|
||||
|
||||
"""
|
||||
|
||||
# Do not overwrite original data dictionary
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
from threading import ThreadError
|
||||
|
||||
|
||||
class TaskDeniedException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class LockError(ThreadError):
|
||||
ERROR_CODES = {
|
||||
'ACQUIRE_ERROR': "Unable to acquire. Lock in use by another thread.",
|
||||
|
|
@ -22,4 +24,4 @@ class LockError(ThreadError):
|
|||
ThreadError.__init__(self)
|
||||
|
||||
def __str__(self):
|
||||
return self.string
|
||||
return self.string
|
||||
|
|
|
|||
|
|
@ -53,4 +53,4 @@ class CompositeLock(object):
|
|||
|
||||
def release(self):
|
||||
for lock in self.locks:
|
||||
lock.release()
|
||||
lock.release()
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
Defines a microscope object, binding a camera and stage with basic functionality.
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
import numpy as np
|
||||
import uuid
|
||||
|
||||
|
|
|
|||
|
|
@ -3,21 +3,21 @@ import numpy as np
|
|||
from openflexure_microscope.api.v1.views import MicroscopeViewPlugin
|
||||
from openflexure_microscope.api.utilities import JsonPayload
|
||||
|
||||
from flask import request, Response, escape, jsonify
|
||||
from flask import request, jsonify
|
||||
|
||||
import logging
|
||||
|
||||
class MeasureSharpnessAPI(MicroscopeViewPlugin):
|
||||
def post(self):
|
||||
payload = JsonPayload(request)
|
||||
return jsonify({'sharpness': self.plugin.measure_sharpness()})
|
||||
|
||||
|
||||
class AutofocusAPI(MicroscopeViewPlugin):
|
||||
def post(self):
|
||||
payload = JsonPayload(request)
|
||||
|
||||
# Figure out the range of z values to use
|
||||
dz = payload.param("dz", default=np.linspace(-300,300,7), convert=np.array)
|
||||
dz = payload.param("dz", default=np.linspace(-300, 300, 7), convert=np.array)
|
||||
|
||||
print("Running autofocus...")
|
||||
task = self.microscope.task.start(self.plugin.autofocus, dz)
|
||||
|
|
|
|||
|
|
@ -1,21 +1,24 @@
|
|||
import numpy as np
|
||||
from scipy import ndimage
|
||||
|
||||
|
||||
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)
|
||||
# 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]])
|
||||
return np.sum([np.sum(ndimage.filters.convolve(gray, W)**2) for W in [edge, edge.T]])
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ class AutofocusPlugin(MicroscopePlugin):
|
|||
positions = []
|
||||
camera.annotate_text = ""
|
||||
|
||||
for i in stage.scan_z(dz, return_to_start=False):
|
||||
for _ in stage.scan_z(dz, return_to_start=False):
|
||||
positions.append(stage.position[2])
|
||||
time.sleep(settle)
|
||||
sharpnesses.append(self.measure_sharpness(metric_fn))
|
||||
|
|
@ -46,4 +46,4 @@ class AutofocusPlugin(MicroscopePlugin):
|
|||
|
||||
def measure_sharpness(self, metric_fn=sharpness_sum_lap2):
|
||||
"""Measure the sharpness of the camera's current view."""
|
||||
return metric_fn(self.microscope.camera.array(use_video_port=True, resize=(640,480)))
|
||||
return metric_fn(self.microscope.camera.array(use_video_port=True, resize=(640, 480)))
|
||||
|
|
|
|||
|
|
@ -1,28 +1,26 @@
|
|||
import time
|
||||
import numpy as np
|
||||
|
||||
from openflexure_microscope.plugins import MicroscopePlugin
|
||||
from openflexure_microscope.utilities import set_properties
|
||||
from openflexure_microscope.api.v1.views import MicroscopeViewPlugin
|
||||
from openflexure_microscope.api.utilities import JsonPayload
|
||||
|
||||
from flask import request, Response, escape, jsonify
|
||||
from flask import request, jsonify
|
||||
import logging
|
||||
|
||||
from .recalibrate_utils import recalibrate_camera
|
||||
|
||||
|
||||
class RecalibrateAPIView(MicroscopeViewPlugin):
|
||||
def post(self):
|
||||
payload = JsonPayload(request)
|
||||
|
||||
# Figure out the range of z values to use
|
||||
# TODO: Figure out the range of z values to use
|
||||
|
||||
print("Starting microscope recalibration...")
|
||||
task = self.microscope.task.start(self.plugin.recalibrate)
|
||||
|
||||
# return a handle on the autofocus task
|
||||
# Return a handle on the autofocus task
|
||||
return jsonify(task.state, 202)
|
||||
|
||||
|
||||
class Plugin(MicroscopePlugin):
|
||||
"""
|
||||
A set of default plugins
|
||||
|
|
@ -45,16 +43,14 @@ class Plugin(MicroscopePlugin):
|
|||
streaming = scamera.state['stream_active']
|
||||
if streaming:
|
||||
logging.info("Stopping stream before recalibration")
|
||||
scamera.stop_stream_recording(resolution=(640,480))
|
||||
scamera.stop_stream_recording(resolution=(640, 480))
|
||||
old_resolution = scamera.camera.resolution
|
||||
try:
|
||||
scamera.camera.resolution=(640,480)
|
||||
scamera.camera.resolution = (640, 480)
|
||||
recalibrate_camera(scamera.camera)
|
||||
finally:
|
||||
scamera.camera.resolution=old_resolution
|
||||
scamera.camera.resolution = old_resolution
|
||||
self.microscope.save_config()
|
||||
if streaming:
|
||||
logging.info("Restarting stream after recalibration")
|
||||
scamera.start_stream_recording()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -3,12 +3,14 @@ from picamera import PiCamera
|
|||
from picamera.array import PiRGBArray, PiBayerArray
|
||||
import time
|
||||
|
||||
|
||||
def rgb_image(camera, resize=None, **kwargs):
|
||||
"""Capture an image and return an RGB numpy array"""
|
||||
with PiRGBArray(camera, size=resize) as output:
|
||||
camera.capture(output, format='rgb', resize=resize, **kwargs)
|
||||
return output.array
|
||||
|
||||
|
||||
def flat_lens_shading_table(camera):
|
||||
"""Return a flat (i.e. unity gain) lens shading table.
|
||||
|
||||
|
|
@ -20,6 +22,7 @@ def flat_lens_shading_table(camera):
|
|||
raise ImportError("This program requires the forked picamera library with lens shading support")
|
||||
return np.zeros(camera._lens_shading_table_shape(), dtype=np.uint8) + 32
|
||||
|
||||
|
||||
def adjust_exposure_to_setpoint(camera, setpoint):
|
||||
"""Adjust the camera's exposure time until the maximum pixel value is <setpoint>."""
|
||||
print("Adjusting shutter speed to hit setpoint {}".format(setpoint), end="")
|
||||
|
|
@ -29,11 +32,12 @@ def adjust_exposure_to_setpoint(camera, setpoint):
|
|||
time.sleep(1)
|
||||
print("done")
|
||||
|
||||
|
||||
def auto_expose_and_freeze_settings(camera):
|
||||
"""Freeze the settings after auto-exposing to white illumination"""
|
||||
print("Allowing the camera to auto-expose")
|
||||
camera.awb_mode="auto"
|
||||
camera.exposure_mode="auto"
|
||||
camera.awb_mode = "auto"
|
||||
camera.exposure_mode = "auto"
|
||||
for i in range(6):
|
||||
print(".", end="")
|
||||
time.sleep(0.5)
|
||||
|
|
@ -54,18 +58,19 @@ def auto_expose_and_freeze_settings(camera):
|
|||
|
||||
def channels_from_bayer_array(bayer_array):
|
||||
"""Given the 'array' from a PiBayerArray, return the 4 channels."""
|
||||
bayer_pattern = [(i//2, i%2) for i in range(4)]
|
||||
bayer_pattern = [(i//2, i % 2) for i in range(4)]
|
||||
channels = np.zeros((4, bayer_array.shape[0]//2, bayer_array.shape[1]//2), dtype=bayer_array.dtype)
|
||||
for i, offset in enumerate(bayer_pattern):
|
||||
# We simplify life by dealing with only one channel at a time.
|
||||
channels[i, :, :] = np.sum(bayer_array[offset[0]::2, offset[1]::2, :], axis=2)
|
||||
channels[i, :, :] = np.sum(bayer_array[offset[0]::2, offset[1]::2, :], axis=2)
|
||||
|
||||
return channels
|
||||
|
||||
|
||||
def lst_from_channels(channels):
|
||||
"""Given the 4 Bayer colour channels from a white image, generate a LST."""
|
||||
full_resolution = np.array(channels.shape[1:]) * 2 # channels have been binned
|
||||
#lst_resolution = list(np.ceil(full_resolution / 64.0).astype(int))
|
||||
full_resolution = np.array(channels.shape[1:]) * 2 # channels have been binned
|
||||
# lst_resolution = list(np.ceil(full_resolution / 64.0).astype(int))
|
||||
lst_resolution = [(r // 64) + 1 for r in full_resolution]
|
||||
# NB the size of the LST is 1/64th of the image, but rounded UP.
|
||||
print("Generating a lens shading table at {}x{}".format(*lst_resolution))
|
||||
|
|
@ -73,7 +78,7 @@ def lst_from_channels(channels):
|
|||
for i in range(lens_shading.shape[0]):
|
||||
image_channel = channels[i, :, :]
|
||||
iw, ih = image_channel.shape
|
||||
ls_channel = lens_shading[i,:,:]
|
||||
ls_channel = lens_shading[i, :, :]
|
||||
lw, lh = ls_channel.shape
|
||||
# The lens shading table is rounded **up** in size to 1/64th of the size of
|
||||
# the image. Rather than handle edge images separately, I'm just going to
|
||||
|
|
@ -84,19 +89,23 @@ def lst_from_channels(channels):
|
|||
# less computationally efficient!
|
||||
padded_image_channel = np.pad(image_channel,
|
||||
[(0, lw*32 - iw), (0, lh*32 - ih)],
|
||||
mode="edge") # Pad image to the right and bottom
|
||||
print("Channel shape: {}x{}, shading table shape: {}x{}, after padding {}".format(iw,ih,lw*32,lh*32,padded_image_channel.shape))
|
||||
mode="edge") # Pad image to the right and bottom
|
||||
print("Channel shape: {}x{}, shading table shape: {}x{}, after padding {}".format(iw,
|
||||
ih,
|
||||
lw*32,
|
||||
lh*32,
|
||||
padded_image_channel.shape))
|
||||
# Next, fill the shading table (except edge pixels). Please excuse the
|
||||
# for loop - I know it's not fast but this code needn't be!
|
||||
box = 3 # We average together a square of this side length for each pixel.
|
||||
box = 3 # We average together a square of this side length for each pixel.
|
||||
# NB this isn't quite what 6by9's program does - it averages 3 pixels
|
||||
# horizontally, but not vertically.
|
||||
for dx in np.arange(box) - box//2:
|
||||
for dy in np.arange(box) - box//2:
|
||||
ls_channel[:,:] += padded_image_channel[16+dx::32,16+dy::32] - 64
|
||||
ls_channel[:, :] += padded_image_channel[16+dx::32, 16+dy::32] - 64
|
||||
ls_channel /= box**2
|
||||
# The original C code written by 6by9 normalises to the central 64 pixels in each channel.
|
||||
#ls_channel /= np.mean(image_channel[iw//2-4:iw//2+4, ih//2-4:ih//2+4])
|
||||
# ls_channel /= np.mean(image_channel[iw//2-4:iw//2+4, ih//2-4:ih//2+4])
|
||||
# I have had better results just normalising to the maximum:
|
||||
ls_channel /= np.max(ls_channel)
|
||||
# NB the central pixel should now be *approximately* 1.0 (may not be exactly
|
||||
|
|
@ -107,11 +116,12 @@ def lst_from_channels(channels):
|
|||
|
||||
# What we actually want to calculate is the gains needed to compensate for the
|
||||
# lens shading - that's 1/lens_shading_table_float as we currently have it.
|
||||
gains = 32.0/lens_shading # 32 is unity gain
|
||||
gains[gains > 255] = 255 # clip at 255, maximum gain is 255/32
|
||||
gains[gains < 32] = 32 # clip at 32, minimum gain is 1 (is this necessary?)
|
||||
gains = 32.0/lens_shading # 32 is unity gain
|
||||
gains[gains > 255] = 255 # clip at 255, maximum gain is 255/32
|
||||
gains[gains < 32] = 32 # clip at 32, minimum gain is 1 (is this necessary?)
|
||||
lens_shading_table = gains.astype(np.uint8)
|
||||
return lens_shading_table[::-1,:,:].copy()
|
||||
return lens_shading_table[::-1, :, :].copy()
|
||||
|
||||
|
||||
def recalibrate_camera(camera):
|
||||
"""Reset the lens shading table and exposure settings.
|
||||
|
|
@ -124,7 +134,7 @@ def recalibrate_camera(camera):
|
|||
``StreamingCamera``.
|
||||
"""
|
||||
camera.lens_shading_table = flat_lens_shading_table(camera)
|
||||
discarded = rgb_image(camera) # for some reason the camera won't work unless I do this!
|
||||
_ = rgb_image(camera) # for some reason the camera won't work unless I do this!
|
||||
|
||||
with PiBayerArray(camera) as a:
|
||||
camera.capture(a, format="jpeg", bayer=True)
|
||||
|
|
@ -132,18 +142,19 @@ def recalibrate_camera(camera):
|
|||
|
||||
# Now we need to calculate a lens shading table that would make this flat.
|
||||
# raw_image is a 3D array, with full resolution and 3 colour channels. No
|
||||
# demosaicing has been done, so 2/3 of the values are zero (3/4 for R and B
|
||||
# channels, 1/2 for green because there's twize as many green pixels).
|
||||
# de-mosaicing has been done, so 2/3 of the values are zero (3/4 for R and B
|
||||
# channels, 1/2 for green because there's twice as many green pixels).
|
||||
channels = channels_from_bayer_array(raw_image)
|
||||
lens_shading_table = lst_from_channels(channels)
|
||||
|
||||
camera.lens_shading_table=lens_shading_table
|
||||
test = rgb_image(camera)
|
||||
camera.lens_shading_table = lens_shading_table
|
||||
_ = rgb_image(camera)
|
||||
|
||||
# Fix the AWB gains so the image is neutral
|
||||
channel_means = np.mean(np.mean(rgb_image(camera), axis=0, dtype=np.float), axis=0)
|
||||
old_gains = camera.awb_gains
|
||||
camera.awb_gains = (channel_means[1]/channel_means[0] * old_gains[0], channel_means[1]/channel_means[2]*old_gains[1])
|
||||
camera.awb_gains = (channel_means[1]/channel_means[0] * old_gains[0],
|
||||
channel_means[1]/channel_means[2]*old_gains[1])
|
||||
time.sleep(1)
|
||||
# Ensure the background is bright but not saturated
|
||||
adjust_exposure_to_setpoint(camera, 230)
|
||||
|
|
@ -157,4 +168,3 @@ if __name__ == "__main__":
|
|||
recalibrate_camera(camera)
|
||||
print("Done.")
|
||||
time.sleep(2)
|
||||
|
||||
|
|
|
|||
|
|
@ -4,8 +4,6 @@ from openflexure_microscope.exceptions import TaskDeniedException
|
|||
|
||||
from flask import request, Response, escape, jsonify, abort
|
||||
|
||||
import logging
|
||||
|
||||
|
||||
class IdentifyAPI(MicroscopeViewPlugin):
|
||||
"""
|
||||
|
|
@ -53,6 +51,7 @@ class HelloWorldAPI(MicroscopeViewPlugin):
|
|||
|
||||
return Response(self.microscope.plugin_string)
|
||||
|
||||
|
||||
class LongRunningAPI(MicroscopeViewPlugin):
|
||||
"""
|
||||
An example API plugin that uses a long-running plugin method.
|
||||
|
|
@ -75,6 +74,7 @@ class LongRunningAPI(MicroscopeViewPlugin):
|
|||
except TaskDeniedException:
|
||||
return abort(409)
|
||||
|
||||
|
||||
class SomeExceptionAPI(MicroscopeViewPlugin):
|
||||
"""
|
||||
An example API plugin that uses a long-running but broken plugin method.
|
||||
|
|
@ -92,4 +92,4 @@ class SomeExceptionAPI(MicroscopeViewPlugin):
|
|||
return jsonify(task.state), 202
|
||||
|
||||
except TaskDeniedException:
|
||||
return abort(409)
|
||||
return abort(409)
|
||||
|
|
|
|||
|
|
@ -58,4 +58,4 @@ class Plugin(MicroscopePlugin):
|
|||
|
||||
print("Long-running task finished! Releasing locks.")
|
||||
|
||||
return n_array
|
||||
return n_array
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import inspect
|
|||
import logging
|
||||
|
||||
|
||||
class bcolors:
|
||||
class ConColors:
|
||||
HEADER = '\033[95m'
|
||||
OKBLUE = '\033[94m'
|
||||
OKGREEN = '\033[92m'
|
||||
|
|
@ -23,7 +23,7 @@ def module_from_file(plugin_path):
|
|||
|
||||
# Check if the path is to a file
|
||||
if not os.path.isfile(plugin_path):
|
||||
logging.warning(bcolors.FAIL + "No valid plugin found at {}.".format(plugin_path) + bcolors.ENDC)
|
||||
logging.warning(ConColors.FAIL + "No valid plugin found at {}.".format(plugin_path) + ConColors.ENDC)
|
||||
return None, None, None
|
||||
|
||||
else:
|
||||
|
|
@ -63,6 +63,7 @@ def check_module(module_path):
|
|||
# If all checks pass, return True
|
||||
return True
|
||||
|
||||
|
||||
def name_from_module(plugin_path):
|
||||
path_array = plugin_path.split('.')
|
||||
|
||||
|
|
@ -100,7 +101,7 @@ def load_plugin_class(plugin_path, plugin_class_name):
|
|||
try:
|
||||
plugin_class = getattr(plugin_module, plugin_class_name)
|
||||
except AttributeError:
|
||||
logging.warning(bcolors.FAIL + "Class {} does not exist in plugin {}. Skipping.".format(plugin_class_name, plugin_path) + bcolors.ENDC)
|
||||
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
|
||||
|
|
@ -112,7 +113,7 @@ def class_from_map(plugin_map):
|
|||
plugin_arr = plugin_map.split(':')
|
||||
|
||||
if not len(plugin_arr) == 2:
|
||||
logging.warning(bcolors.WARNING + "Malformed plugin map {}. Skipping.".format(plugin_map) + bcolors.ENDC)
|
||||
logging.warning(ConColors.WARNING + "Malformed plugin map {}. Skipping.".format(plugin_map) + ConColors.ENDC)
|
||||
return None, None
|
||||
else:
|
||||
return load_plugin_class(*plugin_arr)
|
||||
|
|
@ -155,7 +156,7 @@ class PluginMount(object):
|
|||
plugin_object = plugin_class()
|
||||
|
||||
if hasattr(self, plugin_name): # If a plugin with the same name is already attached.
|
||||
logging.warning(bcolors.WARNING + "A plugin named {} has already been loaded. Skipping {}.".format(plugin_name, plugin_map) + bcolors.ENDC)
|
||||
logging.warning(ConColors.WARNING + "A plugin named {} has already been loaded. Skipping {}.".format(plugin_name, plugin_map) + ConColors.ENDC)
|
||||
|
||||
elif isinstance(plugin_object, MicroscopePlugin): # If plugin_object is an instance of MicroscopePlugin
|
||||
# Attach plugin_object to the plugin mount
|
||||
|
|
@ -165,10 +166,10 @@ class PluginMount(object):
|
|||
# Grant plugin access to the hardware
|
||||
plugin_object.microscope = self.parent
|
||||
|
||||
logging.info(bcolors.OKGREEN + "Plugin {} loaded as {}.".format(plugin_map, plugin_name) + bcolors.ENDC)
|
||||
logging.info(ConColors.OKGREEN + "Plugin {} loaded as {}.".format(plugin_map, plugin_name) + ConColors.ENDC)
|
||||
|
||||
|
||||
class MicroscopePlugin():
|
||||
class MicroscopePlugin:
|
||||
"""
|
||||
Parent class for all microscope plugins.
|
||||
|
||||
|
|
|
|||
|
|
@ -10,4 +10,4 @@ class Plugin(MicroscopePlugin):
|
|||
"""
|
||||
Tests for access to Microscope.camera, and Microscope.stage
|
||||
"""
|
||||
return (self.microscope.camera, self.microscope.stage)
|
||||
return self.microscope.camera, self.microscope.stage
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ from openflexure_microscope.lock import StrictLock
|
|||
|
||||
import logging
|
||||
|
||||
|
||||
# TODO: Implement lock on movement
|
||||
class Stage(OpenFlexureStage):
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
|
@ -16,7 +17,7 @@ class Stage(OpenFlexureStage):
|
|||
|
||||
try:
|
||||
OpenFlexureStage.__init__(self, *args, **kwargs)
|
||||
except SerialException as e:
|
||||
except SerialException:
|
||||
logging.error("No stage found. Aborting stage.")
|
||||
logging.warning("Stage lock can be acquired, but any stage methods will fail and raise exceptions.")
|
||||
|
||||
|
|
@ -25,4 +26,4 @@ class Stage(OpenFlexureStage):
|
|||
Overrides :py:function:`openflexure_stage.stage.OpenFlexureStage._move_rel_nobacklash` to acquire lock first.
|
||||
"""
|
||||
with self.lock:
|
||||
OpenFlexureStage._move_rel_nobacklash(self, *args, **kwargs)
|
||||
OpenFlexureStage._move_rel_nobacklash(self, *args, **kwargs)
|
||||
|
|
|
|||
|
|
@ -1,14 +1,13 @@
|
|||
from threading import Thread
|
||||
from functools import wraps
|
||||
import datetime
|
||||
import logging
|
||||
import traceback
|
||||
import time
|
||||
import uuid
|
||||
|
||||
from openflexure_microscope.exceptions import TaskDeniedException
|
||||
from openflexure_microscope.utilities import entry_by_id
|
||||
|
||||
|
||||
class TaskOrchestrator:
|
||||
"""
|
||||
Class responsible for spawning threaded tasks, and storing their returns.
|
||||
|
|
@ -76,7 +75,7 @@ class TaskOrchestrator:
|
|||
Method returning bool describing if a particular task is allowed to run.
|
||||
Currently always allows tasks to start, as locks determine access to hardware.
|
||||
"""
|
||||
#return not any([task._running for task in self.tasks])
|
||||
# return not any([task._running for task in self.tasks])
|
||||
return True
|
||||
|
||||
|
||||
|
|
@ -139,4 +138,4 @@ class Task:
|
|||
self._running = True # Set start command
|
||||
thread = Thread(target=self.run) # Define thread
|
||||
thread.daemon = True # Stop this thread when main thread closes
|
||||
thread.start() # Start thread
|
||||
thread.start() # Start thread
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import copy
|
||||
import operator
|
||||
from fractions import Fraction
|
||||
from functools import reduce
|
||||
from contextlib import contextmanager
|
||||
|
||||
|
||||
@contextmanager
|
||||
def set_properties(obj, **kwargs):
|
||||
"""A context manager to set, then reset, certain properties of an object.
|
||||
|
|
@ -25,7 +25,8 @@ def set_properties(obj, **kwargs):
|
|||
for k, v in saved_properties.items():
|
||||
setattr(obj, k, v)
|
||||
|
||||
def axes_to_array(coordinate_dictionary, axis_keys=['x', 'y', 'z'], base_array=None):
|
||||
|
||||
def axes_to_array(coordinate_dictionary, axis_keys=('x', 'y', 'z'), base_array=None):
|
||||
"""Takes key-value pairs of a JSON value, and maps onto an array"""
|
||||
# If no base array is given
|
||||
if not base_array:
|
||||
|
|
@ -42,19 +43,21 @@ def axes_to_array(coordinate_dictionary, axis_keys=['x', 'y', 'z'], base_array=N
|
|||
|
||||
return base_array
|
||||
|
||||
def filter_dict(dictionary: dict, keys: list):
|
||||
# Get value by recursively applying getitem
|
||||
val = reduce(operator.getitem, keys, dictionary)
|
||||
|
||||
# Create new dictionary by running reduce on key, val pairs
|
||||
out = reduce(lambda x, y: {y: x}, reversed(keys), val)
|
||||
|
||||
return out
|
||||
|
||||
def entry_by_id(id: str, object_list: list):
|
||||
def filter_dict(dictionary: dict, keys: list):
|
||||
# Get value by recursively applying getitem
|
||||
val = reduce(operator.getitem, keys, dictionary)
|
||||
|
||||
# Create new dictionary by running reduce on key, val pairs
|
||||
out = reduce(lambda x, y: {y: x}, reversed(keys), val)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
def entry_by_id(entry_id: str, object_list: list):
|
||||
"""Return an object from a list, if <object>.id matches id argument."""
|
||||
found = None
|
||||
for o in object_list:
|
||||
if o.id == id:
|
||||
if o.id == entry_id:
|
||||
found = o
|
||||
return found
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue