Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -387,4 +387,5 @@ max-locals=50

# Exceptions that will emit a warning when being caught. Defaults to
# "Exception"
overgeneral-exceptions=Exception
overgeneral-exceptions=builtins.BaseException,
bultins.Exception
45 changes: 23 additions & 22 deletions environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,28 @@ channels:
- conda-forge
- defaults
dependencies:
- pip=22.3.1
- python=3.11.0
- pip=24.0.0
- python=3.12.0
- pip:
- astroid==2.12.13
- attrs==22.1.0
- coverage==6.5.0
- astroid>=3.2.3
- attrs>=23.2.0
- coverage>=7.6.0
- jupyter==1.0.0
- matplotlib==3.6.2
- mock==4.0.3
- numpy==1.23.5
- pandas==1.2.3
- pillow==9.3.0
- pylint==2.15.6
- pytest==7.2.0
- python-dotenv==0.21.0
- requests==2.28.1
- scikit-image==0.19.3
- scikit-learn==1.1.3
- setuptools==65.5.1
- sphinx-argparse==0.2.2
- sphinx==5.3.0
- tifffile==2022.10.10
- tox==3.27.1
- tqdm==4.64.1
- matplotlib>=3.9.0
- mock>=5.1.0
- numpy<2.0.0
- pandas>=2.2.0
- pillow>=10.4.0
- pylint>=3.2.0
- py>=1.11.0
- pytest>=8.3.0
- python-dotenv>=1.0.0
- requests>=2.32.0
- scikit-image>=0.24.0
- scikit-learn>=1.5.0
- setuptools>=70.3.0
- sphinx-argparse>=0.4.0
- sphinx>=7.4.0
- tifffile<2024.6.18
- tox>=4.16.0
- tqdm>=4.66.0
8 changes: 4 additions & 4 deletions mibidata/tests/test_segmentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,9 +187,9 @@ def test_extract_cell_dataframe(self):
second_total = [1, 3]
# Check coords and areas only
expected_from_labels = pd.DataFrame(
np.array([areas, x_centroids, y_centroids]).T,
np.array([areas, x_centroids, y_centroids]).T.astype(np.int64),
columns=['area', 'x_centroid', 'y_centroid'],
index=pd.Index(labels, name='label'))
index=pd.Index(labels, name='label', dtype=np.int64))
pdt.assert_frame_equal(
segmentation.extract_cell_dataframe(cell_labels),
expected_from_labels)
Expand Down Expand Up @@ -272,13 +272,13 @@ def test_filter_by_size(self):
[1, 1, 3, 3],
[4, 4, 3, 3],
[0, 4, 3, 3]
])
], dtype=np.int64)
expected = np.array([
[0, 1, 1, 0],
[1, 1, 0, 0],
[2, 2, 0, 0],
[0, 2, 0, 0]
])
], dtype=np.int64)
df = segmentation.extract_cell_dataframe(expected)
filtered_image, filtered_df = segmentation.filter_by_size(
cell_labels, 3, 5)
Expand Down
82 changes: 44 additions & 38 deletions mibitracker/request_helpers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"""Helper class for making and retrying requests to the MIBItracker.

Copyright (C) 2021 Ionpath, Inc. All rights reserved."""

import datetime
import io
import json
Expand Down Expand Up @@ -94,7 +93,7 @@ def __init__(self,
# The token will be used in lieu of email and password if provided
if token is not None:
self.session.headers.update({
'Authorization': 'JWT {}'.format(token)
'Authorization': f'JWT {token}'
})
self.session.options(self.url)
elif email is not None and password is not None:
Expand All @@ -104,7 +103,7 @@ def __init__(self,
'Provide either both an email and password or a token'
)

retry = Retry(status=retries, method_whitelist=retry_methods,
retry = Retry(status=retries, allowed_methods=retry_methods,
status_forcelist=retry_codes, backoff_factor=0.3)
# Set this session to use these retry settings for all http[s] requests
self.session.mount('http://', HTTPAdapter(max_retries=retry))
Expand All @@ -113,24 +112,24 @@ def __init__(self,
def _auth(self, url, email, password):
"""Adds an authorization token to the session's default header."""
response = self.session.post(
'{}/api-token-auth/'.format(url),
f'{url}/api-token-auth/',
headers={'content-type': 'application/json'},
data=json.dumps({'email': email, 'password': password}))
token = response.json()['token']
self.session.headers.update({'Authorization': 'JWT {}'.format(token)})
self.session.headers.update({'Authorization': f'JWT {token}'})

def refresh(self):
"""Refreshes the authorization token stored in the session header.

Raises HTTP 400 if attempting to refresh an expired token."""
token = self.session.post( # use the session to avoid recursion
'{}/api-token-refresh/'.format(self.url),
f'{self.url}/api-token-refresh/',
data=json.dumps(
{'token': self.session.headers['Authorization'][4:]}
),
headers={'content-type': 'application/json'},
).json()['token']
self.session.headers.update({'Authorization': 'JWT {}'.format(token)})
self.session.headers.update({'Authorization': f'JWT {token}'})

def _check_refresh(self):
current_time = datetime.datetime.now()
Expand All @@ -143,7 +142,7 @@ def _check_refresh(self):
@staticmethod
def _prepare_route(route):
if not route.startswith('/'):
return '/{}'.format(route)
return f'/{route}'
return route

def get(self, route, *args, **kwargs):
Expand All @@ -159,8 +158,8 @@ def get(self, route, *args, **kwargs):
The response from ``requests.Session.get``.
"""
self._check_refresh()
return self.session.get('{}{}'.format(
self.url, self._prepare_route(route)), *args, **kwargs)
return self.session.get(f'{self.url}{self._prepare_route(route)}',
*args, **kwargs)

def post(self, route, *args, **kwargs):
"""Makes a POST request to the url using the session.
Expand All @@ -174,8 +173,8 @@ def post(self, route, *args, **kwargs):
The response from ``requests.Session.post``.
"""
self._check_refresh()
return self.session.post('{}{}'.format(
self.url, self._prepare_route(route)), *args, **kwargs)
return self.session.post(f'{self.url}{self._prepare_route(route)}',
*args, **kwargs)

def put(self, route, *args, **kwargs):
"""Makes a PUT request to the url using the session.
Expand All @@ -189,8 +188,8 @@ def put(self, route, *args, **kwargs):
The response from ``requests.Session.put``.
"""
self._check_refresh()
return self.session.put('{}{}'.format(
self.url, self._prepare_route(route)), *args, **kwargs)
return self.session.put(f'{self.url}{self._prepare_route(route)}',
*args, **kwargs)

def delete(self, route, *args, **kwargs):
"""Makes a DELETE request to the url using the session.
Expand All @@ -204,8 +203,8 @@ def delete(self, route, *args, **kwargs):
The response from ``requests.Session.delete``.
"""
self._check_refresh()
return self.session.delete('{}{}'.format(
self.url, self._prepare_route(route)), *args, **kwargs)
return self.session.delete(f'{self.url}{self._prepare_route(route)}',
*args, **kwargs)

def download_file(self, path):
"""Downloads a file from MIBItracker storage.
Expand Down Expand Up @@ -264,13 +263,14 @@ def copy_run(self, old_label, new_label, **kwargs):
"""
# We don't expect there to be many runs with the same label, so it is
# safe to turn paging off.
response = self.get('/runs/?label={}&paging=no'.format(old_label))
response = self.get(f'/runs/?label={old_label}&paging=no')
data = response.json()
try:
assert len(data) == 1
except AssertionError:
raise MibiTrackerError('Expected 1 run with label {}, but {} were '
'found'.format(old_label, len(data)))
except AssertionError as err:
raise MibiTrackerError(f'Expected 1 run with label {old_label},'
f' but {len(data)} were found')\
from err
data = data[0]

# Get the XMLs from the original runs. They may not have the date and
Expand All @@ -288,7 +288,7 @@ def copy_run(self, old_label, new_label, **kwargs):
'project': data['project'] and data['project']['id'], # optional
'description': data['description'],
'operator': data['operator'], # Not yet used, but field exists
'user_run_date': '{}T00:00:00'.format(data['run_date']),
'user_run_date': f"{data['run_date']}T00:00:00",
}
# The old run date supercedes the possibly-missing date in the xml
# A timestamp is temporarily added for the JSON encoded but will be
Expand Down Expand Up @@ -320,15 +320,15 @@ def copy_run_image_metadata(self, old_run_label, new_run_label, **kwargs):
response JSON returned when updating the new images.
"""
old_images = self.get(
'/images/?run__label={}&paging=no'.format(old_run_label))
f'/images/?run__label={old_run_label}&paging=no')

image_map = {}
for item in old_images.json():

# Get image from copied run
response = self.get(
'/images/?run__label={}&folder={}&paging=no'.format(
new_run_label, item['folder']))
f"/images/?run__label={new_run_label}&folder={item['folder']}"
'&paging=no')
assert len(response.json()) == 1
new_image = response.json()[0]
# Update the section and tissue of the copied image using
Expand Down Expand Up @@ -371,6 +371,7 @@ def copy_run_image_metadata(self, old_run_label, new_run_label, **kwargs):
content_type = 'image/png'

buf = self.download_file(sed_path)
# pylint: disable=possibly-used-before-assignment
files = {'attachment': (sed, buf, content_type)}
data = updated_image
else:
Expand All @@ -381,7 +382,7 @@ def copy_run_image_metadata(self, old_run_label, new_run_label, **kwargs):
headers.update({'content-type': 'application/json'})

response = self.put(
'/images/{}/'.format(new_image['id']),
f"/images/{new_image['id']}/",
files=files,
data=data,
headers=headers
Expand Down Expand Up @@ -429,11 +430,12 @@ def upload_mibitiff(self, tiff_file, run_id=None):
try:
with open(tiff_file, 'rb') as fh:
self._upload_mibitiff(response['url'], fh)
except TypeError:
except TypeError as err:
try:
tiff_file.seek(0)
except:
raise TypeError('tiff_file must be a string or file object')
raise TypeError('tiff_file must be a string'
'or file object') from err
self._upload_mibitiff(response['url'], tiff_file)
return self.post(
'/upload_mibitiff/',
Expand All @@ -447,7 +449,7 @@ def _upload_channel(self, image_id, image_file, filename):
files = {
'attachment': (filename,
image_file,
'image/{}'.format(ext[1:].lower()))
f'image/{ext[1:].lower()}')
}

response = self.post(
Expand Down Expand Up @@ -480,17 +482,19 @@ def upload_channel(self, image_id, image_file, filename=None):
if not filename:
filename = os.path.basename(image_file)
return self._upload_channel(image_id, fh, filename)
except TypeError:
except TypeError as err:
try:
image_file.seek(0)
except:
raise TypeError('image_file must be a string or file object')
raise TypeError('image_file must be a '
'string or file object') from err
if not filename:
try:
filename = os.path.basename(image_file.name)
except AttributeError:
except AttributeError as aErr:
raise ValueError('filename must be provided with a file '
'object that does not have a name')
'object that does not have a name'
) from aErr
return self._upload_channel(image_id, image_file, filename)

def run_images(self, run_label):
Expand Down Expand Up @@ -518,7 +522,7 @@ def image_conjugates(self, image_id):
section assigned, or if its section does not have a panel assigned.
"""
return self.get(
'/images/{}/conjugates/'.format(image_id),
f'/images/{image_id}/conjugates/',
params={'paging': 'no'}).json()

def image_id(self, run_label, fov_id):
Expand Down Expand Up @@ -580,7 +584,7 @@ def get_mibi_image(self, image_id):
Return:
A MibiImage instance of the requested image.
"""
image_info = self.get('images/{}/'.format(image_id)).json()
image_info = self.get(f'images/{image_id}/').json()
tiff_path = '/'.join((image_info['run']['path'], image_info['folder'],
'summed_image.tiff'))
tiff_data = self.download_file(tiff_path)
Expand All @@ -605,10 +609,11 @@ def get_channel_data(self, image_id, channel_name):
response.raise_for_status()
except HTTPError as e:
if e.response.status_code == 404:
raise MibiTrackerError(
f'Channel \'{channel_name}\' not found in the image.')
raise MibiTrackerError(f'Channel \'{channel_name}\' '
'not found in the image.') from e
raise e

# pylint: disable=missing-timeout
png = requests.get(response.json()['url'])
buf = io.BytesIO()
buf.write(png.content)
Expand Down Expand Up @@ -647,7 +652,7 @@ class StatusCheckedSession(requests.Session):
"""Raises for HTTP errors and adds any response JSON to the message."""

def __init__(self, timeout=SESSION_TIMEOUT):
super(StatusCheckedSession, self).__init__()
super().__init__()
self.timeout = timeout

@staticmethod
Expand All @@ -659,7 +664,8 @@ def _check_status(response):
response_json = response.json()
except json.decoder.JSONDecodeError:
response_json = None
raise HTTPError(str(e), response_json, response=response)
raise HTTPError(str(e), response_json,
response=response) from e
return response

def _set_timeout(self, kwargs):
Expand Down
20 changes: 10 additions & 10 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,17 @@
url='https://github.com/ionpath/mibilib',
description='Python utilities for IONpath MIBItracker and MIBItiff data',
license='GNU General Public License v3.0',
python_requires='~=3.11.0',
python_requires='>=3.8',
install_requires=[
'matplotlib==3.6.2',
'numpy==1.23.5',
'pandas==1.2.3',
'pillow==9.3.0',
'requests>=2.28.1',
'scikit-image==0.19.3',
'scikit-learn==1.1.3',
'tifffile==2022.10.10',
'tqdm==4.64.1',
'matplotlib>=3.6',
'numpy<2.0',
'pandas>=1.0',
'pillow>=9.0',
'requests>=2.28',
'scikit-image>=0.19',
'scikit-learn>=1.1',
'tifffile<2024.6.18',
'tqdm>=4.64',
],
packages=['mibitracker', 'mibidata']
)