Source code for catpy.client
# -*- coding: utf-8 -*-
import json
from six import string_types
import requests
import numpy as np
[docs]def make_url(base_url, *args):
"""
Given any number of URL components, join them as if they were a path regardless of trailing and prepending slashes
Examples
--------
>>> make_url('google.com', 'mail')
'google.com/mail'
>>> make_url('google.com/', '/mail')
'google.com/mail'
"""
for arg in args:
arg_str = str(arg)
joiner = '' if base_url.endswith('/') else '/'
relative = arg_str[1:] if arg_str.startswith('/') else arg_str
base_url = requests.compat.urljoin(base_url + joiner, relative)
return base_url
[docs]class CatmaidClient(object):
"""
Python object handling authentication, request pooling etc. for requests made to a CATMAID server.
"""
def __init__(self, base_url, token=None, auth_name=None, auth_pass=None, project_id=None):
"""
Instantiate CatmaidClient object for handling requests to a CATMAID server.
Parameters
----------
base_url : str
URL at which CATMAID server is running
token : str
API token as assigned by CATMAID server
auth_name : str
HTTP auth username
auth_pass : str
HTTP auth password
project_id : int
(Optional)
"""
self.base_url = base_url
self._session = requests.Session()
if auth_name is not None and auth_pass is not None:
self.set_http_auth(auth_name, auth_pass)
if token is not None:
self.set_api_token(token)
self.project_id = project_id
[docs] def set_http_auth(self, username, password):
"""
Set HTTP authorization for CatmaidClient in place.
Parameters
----------
username : str
HTTP authorization username
password : str
HTTP authorization password
Returns
-------
CatmaidClient
Reference to the same, now-authenticated CatmaidClient instance
"""
self._session.auth = (username, password)
return self
[docs] def set_api_token(self, token):
"""
Set CatmaidClient to use the given API token in place.
Parameters
----------
token : str
API token associated with your CATMAID account
Returns
-------
CatmaidClient
Reference to the same, now-authenticated CatmaidClient instance
"""
self._session.headers['X-Authorization'] = 'Token ' + token
return self
def _make_request_url(self, arg):
"""
Create an absolute request URL for the CATMAID server.
Parameters
----------
arg : str or tuple of str
Relative URL (to the base_url). If a tuple is passed, its elements will be joined with '/'.
Returns
-------
str
"""
if isinstance(arg, string_types):
return make_url(self.base_url, arg)
else:
return make_url(self.base_url, *arg)
@classmethod
[docs] def from_json(cls, path, with_project_id=True):
"""
Return a CatmaidClient instance with credentials matching those in a JSON file. Should have the property
`base_url` as a minimum.
If HTTP authentication is required, should have the properties `auth_name` and `auth_pass`.
If you intend to use an authorized CATMAID account (required for some endpoints), should have the property
`token`.
Can optionally include the property `project_id`.
Parameters
----------
path : str
Path to the JSON credentials file
with_project_id : bool
Whether to look for the `project_id` field (it can be set later on the returned CatmaidClient instance)
Returns
-------
CatmaidClient
Instance of the API, authenticated with
"""
with open(path) as f:
credentials = json.load(f)
return cls(
credentials['base_url'],
credentials.get('token'),
credentials.get('auth_name'),
credentials.get('auth_pass'),
credentials.get('project_id') if with_project_id else None
)
[docs] def get(self, relative_url, params=None, raw=False):
"""
Get data from a running instance of CATMAID.
Parameters
----------
relative_url : str or tuple of str
URL to send the request to, relative to the base_url. If a tuple is passed, its elements will be joined
with '/'.
params: dict or str, optional
JSON-like key/value data to be included in the get URL (defaults to empty)
raw: bool, optional
Whether to return the response as a string (defaults to returning a dict)
Returns
-------
dict or str
Data returned from CATMAID: type depends on the 'raw' parameter.
"""
return self.fetch(relative_url, method='GET', data=params, raw=raw)
[docs] def post(self, relative_url, data=None, raw=False):
"""
Post data to a running instance of CATMAID.
Parameters
----------
relative_url : str or tuple of str
URL to send the request to, relative to the base_url. If a tuple is passed, its elements will be joined
with '/'.
data: dict or str, optional
JSON-like key/value data to be included in the request as a payload (defaults to empty)
raw: bool, optional
Whether to return the response as a string (defaults to returning a dict)
Returns
-------
dict or str
Data returned from CATMAID: type depends on the 'raw' parameter.
"""
return self.fetch(relative_url, method='POST', data=data, raw=raw)
[docs] def fetch(self, relative_url, method='GET', data=None, raw=False):
"""
Interact with the CATMAID server in a manner very similar to the javascript CATMAID.fetch API.
Parameters
----------
relative_url : str or tuple of str
URL to send the request to, relative to the base_url. If a tuple is passed, its elements will be joined
with '/'.
method: {'GET', 'POST'}, optional
HTTP method to use (the default is 'GET')
data: dict or str, optional
JSON-like key/value data to be included in the request as a payload (defaults to empty)
raw: bool, optional
Whether to return the response as a string (defaults to returning a dict)
Returns
-------
dict or str
Data returned from CATMAID: type depends on the 'raw' parameter.
"""
url = self._make_request_url(relative_url)
data = data or dict()
if method.upper() == 'GET':
response = self._session.get(url, params=data)
elif method.upper() == 'POST':
response = self._session.post(url, data=data)
else:
raise ValueError('Unknown HTTP method {}'.format(repr(method)))
response.raise_for_status()
return response.json() if not raw else response.text
[docs]class CoordinateTransformer(object):
def __init__(self, resolution=None, translation=None):
"""
Helper class for transforming between stack and project coordinates.
Parameters
----------
resolution : dict
x, y and z resolution of the stack
translation : dict
x, y and z the location of the stack's origin (0, 0, 0) in project space
"""
if resolution is None:
resolution = dict()
if translation is None:
translation = dict()
self.resolution = {dim: resolution.get(dim, 1) for dim in 'xyz'}
self.translation = {dim: translation.get(dim, 0) for dim in 'xyz'}
self._resolution_arrays = dict()
self._translation_arrays = dict()
@classmethod
[docs] def from_catmaid(cls, catmaid_client, stack_id):
"""
Return a CoordinateTransformer for a particular CATMAID stack.
Parameters
----------
catmaid_client : CatmaidClient
Authenticated instance of CatmaidClient
stack_id : int
Returns
-------
CoordinateTransformer
"""
stack_info = catmaid_client.get((catmaid_client.project_id, 'stack', stack_id, 'info'))
return cls(stack_info['resolution'], stack_info['translation'])
def _get_resolution_array(self, dims):
if dims not in self._resolution_arrays:
self._resolution_arrays[dims] = np.array([self.resolution[dim] for dim in dims])
return self._resolution_arrays[dims]
def _get_translation_array(self, dims):
if dims not in self._resolution_arrays:
self._translation_arrays[dims] = np.array([self.translation[dim] for dim in dims])
return self._translation_arrays[dims]
[docs] def project_to_stack_coord(self, dim, project_coord):
return (project_coord - self.translation[dim]) / self.resolution[dim]
[docs] def project_to_stack(self, project_coords):
"""
Take a point in project space and transform it into stack space.
Parameters
----------
project_coords : dict
x, y, and/or z coordinates in project / real space
Returns
-------
dict
coordinates transformed into stack / voxel space
"""
return {dim: self.project_to_stack_coord(dim, proj_coord) for dim, proj_coord in project_coords.items()}
[docs] def project_to_stack_array(self, arr, dims='xyz'):
"""
Take an array of points in project space and transform them into stack space.
Parameters
----------
arr : array-like
M by N array containing M coordinates in project / real space in N dimensions
dims : str
Order of dimensions in columns, default 'xyz'
Returns
-------
np.ndarray
M by N array containing M coordinates in stack / voxel space in N dimensions
"""
arr = np.array(arr)
resolution_arr = self._get_resolution_array(dims)
translation_arr = self._get_translation_array(dims)
return (arr - translation_arr) / resolution_arr
[docs] def stack_to_project_coord(self, dim, stack_coord):
return stack_coord * self.resolution[dim] + self.translation[dim]
[docs] def stack_to_project(self, stack_coords):
"""
Take a point in stack space and transform it into project space.
Parameters
----------
stack_coords : dict
x, y, and/or z coordinates in stack / voxel space
Returns
-------
dict
coordinates transformed into project / real space
"""
return {dim: self.stack_to_project_coord(dim, stack_coord) for dim, stack_coord in stack_coords.items()}
[docs] def stack_to_project_array(self, arr, dims='xyz'):
"""
Take an array of points in stack space and transform them into project space.
Parameters
----------
arr : array-like
M by N array containing M coordinates in stack / voxel space in N dimensions
dims : array-like or str
Order of dimensions in columns, default (x, y, z)
Returns
-------
np.ndarray
M by N array containing M coordinates in project / real space in N dimensions
"""
arr = np.array(arr)
resolution_arr = self._get_resolution_array(dims)
translation_arr = self._get_translation_array(dims)
return arr * resolution_arr + translation_arr