-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Initial add * Update dependencies * refactor: expand SlimsBaseModel and client * feat: add mouse model and fetch method * feat: add user model and fetch method * Feat client unit tests (#11) * docs: set line length to 88 * tests: adds test coverage --------- Co-authored-by: jtyoung84 <[email protected]>
- Loading branch information
1 parent
3c933df
commit 5ed1f4c
Showing
18 changed files
with
2,519 additions
and
24 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,3 +4,4 @@ exclude = | |
__pycache__, | ||
build | ||
max-complexity = 10 | ||
max-line-length = 88 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -109,6 +109,7 @@ venv/ | |
ENV/ | ||
env.bak/ | ||
venv.bak/ | ||
.conda | ||
|
||
# Spyder project settings | ||
.spyderproject | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,9 @@ | ||
"""Init package""" | ||
|
||
__version__ = "0.0.0" | ||
|
||
from aind_slims_api.configuration import AindSlimsApiSettings | ||
|
||
config = AindSlimsApiSettings() | ||
|
||
from aind_slims_api.core import SlimsClient # noqa |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
""" Library Configuration model """ | ||
|
||
from pydantic import SecretStr | ||
from pydantic_settings import BaseSettings | ||
|
||
|
||
class AindSlimsApiSettings(BaseSettings): | ||
"""Settings for SLIMS Client | ||
Per pydantic-settings docs | ||
https://docs.pydantic.dev/latest/concepts/pydantic_settings/ | ||
Loads slims credentials from environment variables if present""" | ||
|
||
slims_url: str = "https://aind-test.us.slims.agilent.com/slimsrest/" | ||
slims_username: str = "" | ||
slims_password: SecretStr = "" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
"""Contents: | ||
Utilities for creating pydantic models for SLIMS data: | ||
SlimsBaseModel - to be subclassed for SLIMS pydantic models | ||
UnitSpec - To be included in a type annotation of a Quantity field | ||
SlimsClient - Basic wrapper around slims-python-api client with convenience | ||
methods and integration with SlimsBaseModel subtypes | ||
""" | ||
|
||
import logging | ||
from functools import lru_cache | ||
from typing import Literal, Optional | ||
|
||
from slims.criteria import Criterion, conjunction, equals | ||
from slims.internal import Record as SlimsRecord | ||
from slims.slims import Slims, _SlimsApiException | ||
|
||
from aind_slims_api import config | ||
|
||
logger = logging.getLogger() | ||
|
||
# List of slims tables manually accessed, there are many more | ||
SLIMSTABLES = Literal[ | ||
"Project", | ||
"Content", | ||
"ContentEvent", | ||
"Unit", | ||
"Result", | ||
"Test", | ||
"User", | ||
"Groups", | ||
] | ||
|
||
|
||
class SlimsClient: | ||
"""Wrapper around slims-python-api client with convenience methods""" | ||
|
||
def __init__(self, url=None, username=None, password=None): | ||
"""Create object and try to connect to database""" | ||
self.url = url or config.slims_url | ||
self.db: Optional[Slims] = None | ||
|
||
self.connect( | ||
self.url, | ||
username or config.slims_username, | ||
password or config.slims_password.get_secret_value(), | ||
) | ||
|
||
def connect(self, url: str, username: str, password: str): | ||
"""Connect to the database""" | ||
self.db = Slims( | ||
"slims", | ||
url, | ||
username, | ||
password, | ||
) | ||
|
||
def fetch( | ||
self, | ||
table: SLIMSTABLES, | ||
*args, | ||
sort: Optional[str | list[str]] = None, | ||
start: Optional[int] = None, | ||
end: Optional[int] = None, | ||
**kwargs, | ||
) -> list[SlimsRecord]: | ||
"""Fetch from the SLIMS database | ||
Args: | ||
table (str): SLIMS table to query | ||
sort (str | list[str], optional): Fields to sort by; e.g. date | ||
start (int, optional): The first row to return | ||
end (int, optional): The last row to return | ||
*args (Slims.criteria.Criterion): Optional criteria to apply | ||
**kwargs (dict[str,str]): "field=value" filters | ||
Returns: | ||
records (list[SlimsRecord] | None): Matching records, if any | ||
""" | ||
criteria = conjunction() | ||
for arg in args: | ||
if isinstance(arg, Criterion): | ||
criteria.add(arg) | ||
|
||
for k, v in kwargs.items(): | ||
criteria.add(equals(k, v)) | ||
try: | ||
records = self.db.fetch( | ||
table, | ||
criteria, | ||
sort=sort, | ||
start=start, | ||
end=end, | ||
) | ||
except _SlimsApiException as e: | ||
# TODO: Add better error handling | ||
# Let's just raise error for the time being | ||
raise e | ||
|
||
return records | ||
|
||
@lru_cache(maxsize=None) | ||
def fetch_pk(self, table: SLIMSTABLES, *args, **kwargs) -> int | None: | ||
"""SlimsClient.fetch but returns the pk of the first returned record""" | ||
records = self.fetch(table, *args, **kwargs) | ||
if len(records) > 0: | ||
return records[0].pk() | ||
else: | ||
return None | ||
|
||
def fetch_user(self, user_name: str): | ||
"""Fetches a user by username""" | ||
return self.fetch("User", user_userName=user_name) | ||
|
||
def add(self, table: SLIMSTABLES, data: dict): | ||
"""Add a SLIMS record to a given SLIMS table""" | ||
record = self.db.add(table, data) | ||
logger.info(f"SLIMS Add: {table}/{record.pk()}") | ||
return record | ||
|
||
def update(self, table: SLIMSTABLES, pk: int, data: dict): | ||
"""Update a SLIMS record""" | ||
record = self.db.fetch_by_pk(table, pk) | ||
if record is None: | ||
raise ValueError(f'No data in SLIMS "{table}" table for pk "{pk}"') | ||
new_record = record.update(data) | ||
logger.info(f"SLIMS Update: {table}/{pk}") | ||
return new_record | ||
|
||
def rest_link(self, table: SLIMSTABLES, **kwargs): | ||
"""Construct a url link to a SLIMS table with arbitrary filters""" | ||
base_url = f"{self.url}/rest/{table}" | ||
queries = [f"?{k}={v}" for k, v in kwargs.items()] | ||
return base_url + "".join(queries) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
"""Contains a model for the mouse content, and a method for fetching it""" | ||
|
||
import logging | ||
from typing import Optional | ||
|
||
from aind_slims_api.core import SlimsClient | ||
|
||
logger = logging.getLogger() | ||
|
||
|
||
def fetch_mouse_content( | ||
client: SlimsClient, | ||
mouse_name: str, | ||
) -> Optional[dict]: | ||
"""Fetches mouse information for a mouse with labtracks id {mouse_name}""" | ||
mice = client.fetch( | ||
"Content", | ||
cntp_name="Mouse", | ||
cntn_barCode=mouse_name, | ||
) | ||
|
||
if len(mice) > 0: | ||
mouse_details = mice[0] | ||
if len(mice) > 1: | ||
logger.warning( | ||
f"Warning, Multiple mice in SLIMS with barcode " | ||
f"{mouse_name}, using pk={mouse_details.cntn_pk.value}" | ||
) | ||
else: | ||
logger.warning("Warning, Mouse not in SLIMS") | ||
mouse_details = None | ||
|
||
return None if mouse_details is None else mouse_details.json_entity |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
"""Contains a model for a user, and a method for fetching it""" | ||
|
||
import logging | ||
from typing import Optional | ||
|
||
from aind_slims_api.core import SlimsClient | ||
|
||
logger = logging.getLogger() | ||
|
||
|
||
def fetch_user( | ||
client: SlimsClient, | ||
username: str, | ||
) -> Optional[dict]: | ||
"""Fetches user information for a user with username {username}""" | ||
users = client.fetch( | ||
"User", | ||
user_userName=username, | ||
) | ||
|
||
if len(users) > 0: | ||
user_details = users[0] | ||
if len(users) > 1: | ||
logger.warning( | ||
f"Warning, Multiple users in SLIMS with " | ||
f"username {[u.json_entity for u in users]}, " | ||
f"using pk={user_details.pk()}" | ||
) | ||
else: | ||
logger.warning("Warning, User not in SLIMS") | ||
user_details = None | ||
|
||
return None if user_details is None else user_details.json_entity |
Oops, something went wrong.