Skip to content

utils.aind_smartsheet

SmartsheetRow

Bases: BaseModel

A parsed row of the scheduling smartsheet.

Fields are populated from the sheet's column names (or by field name). Blank or null string values become None, and every column not modeled here is kept in model_extra.

SmartsheetScheduleClient

SmartsheetScheduleClient(
    base_url: str = DEFAULT_SMARTSHEET_ENDPOINT,
    timeout: float | tuple[float, float] | None = (1, 4),
    validator: Callable[
        [str], str | None
    ] = validate_username,
)

Client for the AIND behavior scheduling smartsheet, looked up by animal.

Rows are parsed into rows and are cached per subject, so a session only costs one request. get_row degrades to a logged warning and None when the service is unreachable, but add_scientific_contact and get_project_name require their values and raise ValueError without them.

Example
ss = SmartsheetScheduleClient()
session = ss.add_scientific_contact(session)
watchdog_settings.project_name = ss.get_project_name(session)

Parameters:

Name Type Description Default
base_url str

Root URL of the smartsheet service.

DEFAULT_SMARTSHEET_ENDPOINT
timeout float | tuple[float, float] | None

Timeout in seconds for each HTTP request, or a (connect, read) pair.

(1, 4)
validator Callable[[str], str | None]

Validates the scientific contact's username, returning the canonical name or None to reject it. Defaults to the Active Directory lookup.

validate_username
Source code in src/clabe/utils/aind_smartsheet.py
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
def __init__(
    self,
    base_url: str = DEFAULT_SMARTSHEET_ENDPOINT,
    timeout: float | tuple[float, float] | None = (1, 4),
    validator: Callable[[str], str | None] = validate_username,
) -> None:
    """
    Args:
        base_url: Root URL of the smartsheet service.
        timeout: Timeout in seconds for each HTTP request, or a (connect, read) pair.
        validator: Validates the scientific contact's username, returning the canonical
            name or None to reject it. Defaults to the Active Directory lookup.
    """
    self._base_url = base_url.rstrip("/")
    self._timeout = timeout
    self._session = requests.Session()
    self._session.mount(
        "http://", HTTPAdapter(max_retries=Retry(total=2, backoff_factor=0.3, status_forcelist=(502, 503, 504)))
    )
    self._validator = validator
    self._rows: dict[str, SmartsheetRow | None] = {}

get_row

get_row(subject: str) -> SmartsheetRow | None

Fetches the parsed sheet row for an animal.

Parameters:

Name Type Description Default
subject str

The animal (mouse) id.

required

Returns:

Type Description
SmartsheetRow | None

The row, or None if the animal is not found or the service is unreachable.

SmartsheetRow | None

Only found rows and definitive not-found results are cached; failures are retried on the next call.

Source code in src/clabe/utils/aind_smartsheet.py
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
def get_row(self, subject: str) -> SmartsheetRow | None:
    """
    Fetches the parsed sheet row for an animal.

    Args:
        subject: The animal (mouse) id.

    Returns:
        The row, or None if the animal is not found or the service is unreachable.
        Only found rows and definitive not-found results are cached; failures are retried on the next call.
    """
    if subject in self._rows:
        return self._rows[subject]
    try:
        response = self._session.get(f"{self._base_url}/rows/{quote(subject, safe='')}", timeout=self._timeout)
        if response.status_code == 404:
            self._rows[subject] = None  # definitive; transient failures below are not cached
            return None
        response.raise_for_status()
        self._rows[subject] = SmartsheetRow.model_validate(response.json())
        return self._rows[subject]
    except (requests.RequestException, ValueError) as e:  # pydantic's ValidationError is a ValueError
        logger.warning("Failed to fetch smartsheet row for subject '%s': %s", subject, e)
        return None

add_scientific_contact

add_scientific_contact(session: Session) -> Session

Returns a copy of the session with the animal's validated scientific contact added to the end of experimenter, with duplicate names removed (first occurrence kept). The given session is never modified.

The scientific contact is required: this raises instead of degrading.

Parameters:

Name Type Description Default
session Session

The session to build on.

required

Returns:

Type Description
Session

The updated copy of the session, so callers must use the return value.

Raises:

Type Description
ValueError

If the animal's row cannot be retrieved, the row has no scientific contact, or the username fails validation.

Source code in src/clabe/utils/aind_smartsheet.py
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
def add_scientific_contact(self, session: Session) -> Session:
    """
    Returns a copy of the session with the animal's validated scientific contact added
    to the end of ``experimenter``, with duplicate names removed (first occurrence kept).
    The given session is never modified.

    The scientific contact is required: this raises instead of degrading.

    Args:
        session: The session to build on.

    Returns:
        The updated copy of the session, so callers must use the return value.

    Raises:
        ValueError: If the animal's row cannot be retrieved, the row has no scientific contact,
            or the username fails validation.
    """
    username = self._require_row(session).scientific_contact_username
    if username is None:
        raise ValueError(f"The smartsheet row for subject '{session.subject}' has no scientific contact.")
    canonical = self._validator(username)
    if canonical is None:
        raise ValueError(f"Scientific contact '{username}' for subject '{session.subject}' is not valid.")
    experimenter = list(dict.fromkeys([*session.experimenter, canonical]))
    return session.model_copy(update={"experimenter": experimenter})

get_project_name

get_project_name(session: Session) -> str

Returns the project name recorded for the session's animal.

The project name is required: this raises instead of degrading.

Parameters:

Name Type Description Default
session Session

The session whose animal to look up.

required

Returns:

Type Description
str

The project name.

Raises:

Type Description
ValueError

If the animal's row cannot be retrieved or has no project name.

Source code in src/clabe/utils/aind_smartsheet.py
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
def get_project_name(self, session: Session) -> str:
    """
    Returns the project name recorded for the session's animal.

    The project name is required: this raises instead of degrading.

    Args:
        session: The session whose animal to look up.

    Returns:
        The project name.

    Raises:
        ValueError: If the animal's row cannot be retrieved or has no project name.
    """
    project_name = self._require_row(session).project_name
    if project_name is None:
        raise ValueError(f"The smartsheet row for subject '{session.subject}' has no project name.")
    return project_name