from RequestHandler import RequestHandler
from QueryLayer import InfoQuery
from QueryLayer import DataQuery
from urllib2 import URLError
import numpy as np
[docs]class API(RequestHandler):
""" Responds to :data:`bfly.Webserver._webapp` /api endpoint
Attributes
-----------
inherits: :class:`RequestHandler`
:h:`Methods`
"""
[docs] def parse(self, request):
""" Extract details from any of the methods
Overrides :meth:`Database.parse`
Calls :meth:`_meta_info`, :meth:`_feature_info`, \
:meth:`_get_group`, or :meth:`_get_data` to return \
an :class:`InfoQuery` or :class:`DataQuery` as a \
response to the given ``method``
Arguments
----------
request: str
The single method requested in the URL
Returns
---------
:class:`QueryLayer.Query`
contains standard details for each request
"""
super(API, self).parse(request)
meths = self.INPUT.METHODS.LIST
command = self._match_list('method', request, meths)
if command == self.INPUT.METHODS.META.NAME:
return self._meta_info
if command == self.INPUT.METHODS.FEAT.NAME:
return self._feature_info
# Handle all methods the same if in same list
if command in self.INPUT.METHODS.GROUP_LIST:
return self._get_group(command)
if command in self.INPUT.METHODS.IMAGE_LIST:
return self.get_data(command)
return 'Unsupported Request Category'
@property
def _meta_info(self):
""" Loads :class:`InfoQuery` for ``INPUT.METHODS.META``
Returns
--------
:class:`InfoQuery`
made with info from :meth:`_get_group_dict`
"""
# Get needed metadata
info = self.OUTPUT.INFO
in_info = self.INPUT.INFO
methods = self.INPUT.METHODS
# Get all the channel info
meta_info = self._get_group_dict('')
# The input format becomes the output format
out_format = self._get_list_query(in_info.FORMAT)
return InfoQuery(**{
info.CHANNEL.NAME: meta_info[info.CHANNEL.NAME],
info.PATH.NAME: meta_info[info.PATH.NAME],
methods.NAME: methods.META.NAME,
in_info.FORMAT.NAME: out_format
})
@property
def _feature_info(self):
""" Loads :class:`InfoQuery` for ``INPUT.METHODS.FEATURE``
Returns
--------
:class:`InfoQuery`
with a feature passed as `OUTPUT.INFO.NAMES` \
from :meth:`_id_feature` or :meth:`_box_feature`
"""
# Get needed metadata
info = self.OUTPUT.INFO
in_info = self.INPUT.INFO
methods = self.INPUT.METHODS
feats = self.INPUT.FEATURES
# Create empty parameters
id_key = None
all_queries = []
target_bounds = []
# Get all the channel info
meta_info = self._get_group_dict('')
# The path and the output format
path = meta_info[info.PATH.NAME]
out_format = self._get_list_query(in_info.FORMAT)
# Get the name of the feature to load from db
feat = self._get_list_query(feats)
# All features that need id
if feat in feats.ID_LIST:
# get the id
id_key = self._get_int_query(in_info.ID)
# Return names based on id
names = self._id_feature(feat, path, id_key)
# All features that need bounds and id
elif feat in feats.ID_BOX_LIST:
# get the id
id_key = self._get_int_query(in_info.ID)
# get resolution from input
res_xy = self.INPUT.RESOLUTION.XY
resolution = self._get_int_query(res_xy)
# get bounds from input
for key in ['Z','Y','X','DEPTH','HEIGHT','WIDTH']:
term = getattr(self.INPUT.POSITION, key)
target_bounds.append(self._get_int_query(term))
# scale the bounds from resolution
scale = 2**resolution
scales = np.tile([1 ,scale, scale], 2)
bounds = np.uint32(target_bounds * scales)
# Return names based on bounds
names = self._id_box_feature(feat, path, id_key, bounds)
# All features that need bounds
elif feat in feats.BOX_LIST:
# get resolution from input
res_xy = self.INPUT.RESOLUTION.XY
resolution = self._get_int_query(res_xy)
# get bounds from input
for key in ['Z','Y','X','DEPTH','HEIGHT','WIDTH']:
term = getattr(self.INPUT.POSITION, key)
target_bounds.append(self._get_int_query(term))
# scale the bounds from resolution
scale = 2**resolution
scales = np.tile([1 ,scale, scale], 2)
bounds = np.uint32(target_bounds * scales)
# Return names based on bounds
names = self._box_feature(feat, path, bounds)
# All features needing no parameters
else:
names = self._static_feature(feat, path)
# Return an infoquery
return InfoQuery(**{
methods.NAME: methods.FEAT.NAME,
in_info.FORMAT.NAME: out_format,
info.QUERY.NAME: all_queries,
info.NAMES.NAME: names,
info.PATH.NAME: path,
feats.NAME: feat
})
# Get the db table and key
def _db_feature(self, feat):
""" Get the table and key for feature request
Arguments
----------
feat : str
The name of the feature requested
Returns
---------
:class:`Database`
a reference to :data:`_db`
str
the name of the table for the feature
str
the primary key for the table
"""
# Get all keywords
feats = self.INPUT.FEATURES
k_tables = self.RUNTIME.DB.TABLE
# List all the tables in the database
db_list = map(feats.TABLES.get, k_tables.LIST)
# Get the table that handles given request
in_db = (f.NAME for f in db_list if feat in f.LIST)
db_table = next(in_db, '')
# return empty
if not db_table:
return self._db, db_table, 0
# Find the primary key for the table
db_key = k_tables[db_table].KEY.NAME
# Return database, table, and key
return self._db, db_table, db_key
def _id_feature(self, feat, path, id_key):
""" Loads a feature list that needs an id
Calls :meth:`_db_feature` to access database
Arguments
-----------
feat : str
The name of the feature request
path : str
The path to the corresponding image data
id_key : int
The key value for a :class:`Database` table
Returns
--------
list or dict
The feature used to make an :class:`InfoQuery`
"""
# Get input keyword arguments
feats = self.INPUT.FEATURES
# Get metadata for database
k_tables = self.RUNTIME.DB.TABLE
# Shorthand database name, table, key
db, db_table, db_key = self._db_feature(feat)
# Do not have
if not db_table:
return ['Voxel List not Supported']
# Just check record of an ID
if feat in feats.BOOL_LIST:
if feat == k_tables.LIST[0]:
return db.is_neuron(db_table, path, id_key)
else:
return db.is_synapse(db_table, path, id_key)
# If the request gets a keypoint
if feat in feats.POINT_LIST:
# Get the resolution parameter
res_xy = self.INPUT.RESOLUTION.XY
resolution = self._get_int_query(res_xy)
scales = 2**resolution
# Load from either table
if feat == k_tables.LIST[0]:
return db.neuron_keypoint(db_table, path, id_key, scales)
else:
return db.synapse_keypoint(db_table, path, id_key, scales)
# If the request asks for all links
if feat == feats.SYNAPSE_LINKS.NAME:
return db.synapse_parent(db_table, path, id_key)
# Not yet supported
return [db_table]
def _id_box_feature(self, feat, path, id_key, bounds):
""" Loads a feature list that needs a bounding box
Calls :meth:`_db_feature` to access database
Arguments
-----------
feat : str
The name of the feature request
path : str
The path to the corresponding image data
id_key : int
The key value for a :class:`Database` table
bounds : list
The 6-item list of a volume origin and shape
Returns
--------
list or dict
The feature used to make an :class:`InfoQuery`
"""
# Get input keyword arguments
feats = self.INPUT.FEATURES
# Get metadata for database
k_tables = self.RUNTIME.DB.TABLE
# Shorthand database name, table, key
db, db_table, db_key = self._db_feature(feat)
# Do not know
if not db_table:
return ['Feature not understood']
# Get start and end of bounds
start = np.uint32(bounds[:3])
stop = start + bounds[3:]
# Find all synapses where neuron is parent
if feat == feats.NEURON_CHILDREN.NAME:
# return pre and post results
return db.neuron_children(db_table, path, id_key, start, stop)
# Not yet supported
return [db_table]
def _box_feature(self, feat, path, bounds):
""" Loads a feature list that needs a bounding box
Calls :meth:`_db_feature` to access database
Arguments
-----------
feat : str
The name of the feature request
path : str
The path to the corresponding image data
bounds : list
The 6-item list of a volume origin and shape
Returns
--------
list or dict
The feature used to make an :class:`InfoQuery`
"""
# Get input keyword arguments
feats = self.INPUT.FEATURES
# Get metadata for database
k_tables = self.RUNTIME.DB.TABLE
# Shorthand database name, table, key
db, db_table, db_key = self._db_feature(feat)
# Do not know
if not db_table:
return ['Feature not understood']
# Get start and end of bounds
start = np.uint32(bounds[:3])
stop = start + bounds[3:]
# if request for labels in bounds
if feat in feats.LABEL_LIST:
# Find the center points within the bounds
return db.synapse_ids(db_table, path, start, stop)
# Not yet supported
return [db_table]
def _static_feature(self, feat, path):
""" Loads a feature list that needs no parameters
Calls :meth:`_db_feature` to access database
Arguments
-----------
feat : str
The name of the feature request
path : str
The path to the corresponding image data
Returns
--------
list or dict
The feature used to make an :class:`InfoQuery`
"""
# Get input keyword arguments
feats = self.INPUT.FEATURES
# Get metadata for database
k_tables = self.RUNTIME.DB.TABLE
# Shorthand database name, table, key
db, db_table, db_key = self._db_feature(feat)
# Do not know
if not db_table:
return ['Feature not understood']
# Return all keys in the table
return db.all_neurons(db_table, path)
#####
#Lists values from config for group methods
#####
[docs] def get_value(self, g):
""" get the name of a group
Arguments
----------
g: dict
The group from :data:`BFLY_CONFIG`
Returns
--------
str
the name of `g`
"""
return g.get(self.INPUT.GROUP.NAME,'')
def _find_all_groups(self, _method):
""" Pairs all groups needed for the ``_method``
Arguments
----------
_method: str
The name of the group method requested
Returns
--------
list
list of pairs of group query terms and values
"""
group_methods = self.INPUT.METHODS.GROUP_LIST
group_queries = self.INPUT.GROUP.LIST
# List all parent methods of _method
if _method in group_methods:
group_index = group_methods.index(_method)
group_methods = group_methods[:group_index]
group_queries = group_queries[:group_index]
return zip(group_methods, group_queries)
def _get_group_dict(self, _method):
""" get the config dictionary for the requested method
Arguments
----------
_method: str
The name of the method requesting group information
Returns
--------
dict
The requested sub-dictionary from :data:`BFLY_CONFIG`
"""
configured = self.BFLY_CONFIG
# validate each query value in each configured level
for method, query in self._find_all_groups(_method):
valid_groups = configured.get(method,[])
valid_values = map(self.get_value, valid_groups)
# Check query value against all valid query values
query_value = self.get_query_argument(query,'')
self._match_list(query, query_value, valid_values)
# Continue matching query_value from list of valid groups
configured = valid_groups[valid_values.index(query_value)]
return configured
def _get_group(self, _method):
""" Make :class:`InfoQuery` for groups in the requested group
Arguments
----------
_method: str
The name of the method requesting group information
Returns
--------
:class:`InfoQuery`
The :data:`OUTPUT.INFO` ``.NAMES.NAME`` keyword \
has the list of groups in the requested group from \
:meth:`_get_group_dict`
"""
out_format = self._get_list_query(self.INPUT.INFO.FORMAT)
group_list = self._get_group_dict(_method).get(_method,[])
group_values = map(self.get_value, group_list)
# Return an empty query
return InfoQuery(**{
self.INPUT.METHODS.NAME: _method,
self.INPUT.INFO.FORMAT.NAME: out_format,
self.OUTPUT.INFO.NAMES.NAME: group_values
})
#####
#Loads data from tiles for image methods
#####
[docs] def get_data(self, _method):
""" Make :class:`DataQuery` for an image at request path
Arguments
----------
_method: str
The name of the method requesting image information
Returns
--------
:class:`DataQuery`
Created with the :meth:`sub_data` for the full request
"""
k_pos = self.INPUT.POSITION
positions = map(k_pos.get, k_pos.LIST)
# get integer bounds from POSITION LIST
bounds = map(self._get_int_query, positions)
# Create the data query for the full bounds
return self.sub_data(_method, bounds)
[docs] def sub_data(self, _method, bounds):
""" Make :class:`DataQuery` for any subregion or request
Arguments
----------
_method: str
The name of the method requesting image information
bounds: numpy.ndarray
The 6x1 array of z,y,x,depth,width,height values for \
the bounds requested for a data query
Returns
--------
:class:`DataQuery`
The :data:`OUTPUT.INFO` ``.Path.NAME`` keyword \
has the path to data in the requested group from \
:meth:`_get_group_dict`
"""
# Parse all the group terms
meta_dict = self._get_group_dict('')
path_key = self.OUTPUT.INFO.PATH.NAME
offset_key = self.INPUT.IMAGE.OFFSET.NAME
# Begin building needed keywords
terms = {
path_key: meta_dict.get(path_key, ''),
offset_key: meta_dict.get(offset_key, [0,0,0]),
self.INPUT.METHODS.NAME: _method
}
# get terms from IMAGE
for key in ['VIEW','FORMAT']:
term = getattr(self.INPUT.IMAGE, key)
terms[term.NAME] = self._get_list_query(term)
# get integers from bounds
for order in range(6):
key = self.INPUT.POSITION.LIST[order]
terms[key] = bounds[order]
# get integers from RESOLUTION
term = self.INPUT.RESOLUTION.XY
terms[term.NAME] = self._get_int_query(term)
return DataQuery(**terms)
####
# Handles Logs and Exceptions
####
def _try_typecast_int(self, name, result):
""" Try to convert a query result to an integer
Arguments
-----------
name: str
The name of the ``result`` property
result: anything
The value to try to convert to ``int``
Returns
---------
numpy.uint32
If the ``result`` can convert to an integer
"""
try:
return int(result)
except (TypeError, ValueError):
msg = "The {0} {1} is not an integer."
msg = msg.format(name, result)
raise URLError([msg, 400])
def _match_list(self, name, result, whitelist):
""" Check if the query result is in a whitelist
Arguments
-----------
name: str
The name of the ``result`` property
result: anything
The value to check for in the ``whitelist``
whitelist: list
The list of all accepted ``result``
Returns
---------
anything
If the ``result`` is in the ``whitelist``
"""
# Check if the result is in the list
if result in whitelist:
return result
# Create the error message
msg = "The {0} {1} is not in {2}."
msg = msg.format(name, result, whitelist)
raise URLError([msg, 400])
def _get_list_query(self, field):
""" Call :meth:`_match_list` for a given structure
Get a ``result`` from the URL parameter for the ``field``\
using :meth:`get_query_argument`
Arguments
----------
field: :class:`NamedStruct`
* NAME (str) -- the name of the property
* VALUE (anything) -- the default property value
* LIST (list) -- the list of valid property values
Returns
---------
anything
If the ``result`` is in the ``field.LIST``
"""
result = self.get_query_argument(field.NAME, field.VALUE)
return self._match_list(field.NAME, result, field.LIST)
def _get_int_query(self, field):
""" Call :meth:`_try_typecast_int` for a structure
Get a ``result`` from the URL parameter for the ``field``\
using :meth:`get_query_argument`
Arguments
----------
field: :class:`NamedStruct`
* NAME (str) -- the name of the property
* VALUE (anything) -- the default property value
Returns
---------
np.uint32
If the ``result`` can be converted to an integer
"""
result = self.get_query_argument(field.NAME, field.VALUE)
return self._try_typecast_int(field.NAME, result)