diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..5ec24b0
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,40 @@
+name: Test
+
+on:
+ push:
+ branches: [ main, develop, fix-*, feat-* ]
+ pull_request:
+ branches: [ main ]
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ python-version: ['3.10', '3.11', '3.12']
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v5
+ with:
+ python-version: ${{ matrix.python-version }}
+
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install -e .
+ pip install pytest pytest-cov pandas
+
+ - name: Run unit tests
+ run: |
+ python -m pytest tests/ -v --tb=short --cov=cnb_tools --cov-report=term-missing
+
+ - name: Test CLI basics
+ run: |
+ cnb-tools
+ cnb-tools --help
+ cnb-tools submission --help
+
diff --git a/README.md b/README.md
index 42d7c6e..c1ac2f1 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,7 @@
-
+
@@ -33,23 +33,23 @@ but not limited to, [DREAM Challenges].
## Requirements
-- [Python 3.9+]
+- [Python 3.10+]
- [Synapse account]
To fully utilize **cnb-tools**, you must have a Synapse account and
-provide your credentials to the tool. To do so, create a `.synapseConfig`
-file in your home directory and enter the following:
+provide your credentials to the tool. First, generate a Synapse
+[Personal Access Token (PAT)] with all token permissions enabled.
-```yaml
-[authentication]
-authtoken=
-```
+Next, run the following command, then follow the prompts to enter
+your Synapse username and PAT:
-Generate a new Synapse [Personal Access Token (PAT)] with all token
-permissions enabled, then copy-paste it into `authtoken`. Save the file.
+```console
+synapse config
+```
-For security, we recommend updating its permissions so that other
-users on your machine do not have read access to your credentials, e.g.
+This will create a `.synapseConfig` file in your home directory. For
+security, we recommend updating its permissions so that other users
+on your machine do not have read access to your credentials, e.g.
```console
chmod 600 ~/.synapseConfig
@@ -58,10 +58,9 @@ chmod 600 ~/.synapseConfig
## Installation
For best practice, use a Python environment to install **cnb-tools**
-rather than directly into your base env. In our docs, we will be
+rather than directly into your base env. In our docs, we will be
using [miniconda], but you can use [miniforge], [venv], [pyenv], etc.
-
```console
# Create a new env and activate it
conda create -n cnb-tools python=3.12 -y
@@ -114,7 +113,7 @@ docker run --rm \
[https://sage-bionetworks-challenges.github.io/cnb-tools]: https://sage-bionetworks-challenges.github.io/cnb-tools
[https://github.com/Sage-Bionetworks-Challenges/cnb-tools]: https://github.com/Sage-Bionetworks-Challenges/cnb-tools
[DREAM Challenges]: https://dreamchallenges.org/
-[Python 3.9+]: https://www.python.org/downloads/
+[Python 3.10+]: https://www.python.org/downloads/
[Synapse account]: https://www.synapse.org/#!LoginPlace:0
[Personal Access Token (PAT)]: https://www.synapse.org/#!PersonalAccessTokens:
[miniconda]: https://docs.conda.io/projects/miniconda/en/latest/miniconda-install.html
diff --git a/cnb_tools/classes/annotation.py b/cnb_tools/classes/annotation.py
deleted file mode 100644
index 7e914d3..0000000
--- a/cnb_tools/classes/annotation.py
+++ /dev/null
@@ -1,73 +0,0 @@
-"""Class representing annotations of a challenge submission."""
-
-import json
-
-from synapseclient import SubmissionStatus
-from synapseclient.core.exceptions import SynapseHTTPError
-from synapseclient.core.retry import with_retry
-
-from cnb_tools.classes.base import SynapseBase, UnknownSynapseID
-
-
-class SubmissionAnnotation(SynapseBase):
- def __init__(self, sub_id: int):
- super().__init__(sub_id)
- try:
- self._curr_annotations = self.syn.getSubmissionStatus(sub_id)
- except SynapseHTTPError as err:
- raise UnknownSynapseID(
- f"⛔ {err.response.json().get('reason')}. "
- "Check the ID and try again."
- ) from err
-
- @property
- def curr_annotations(self) -> SubmissionStatus:
- """Submission's current list of annotations."""
- return self._curr_annotations
-
- @curr_annotations.setter
- def curr_annotations(self, value: SubmissionStatus):
- self._curr_annotations = value
-
- def __str__(self) -> str:
- to_print = f" Status: {self.curr_annotations.status}\n"
- to_print += "Annotations:\n"
- to_print += json.dumps(self.curr_annotations.submissionAnnotations, indent=2)
- return to_print
-
- # pylint: disable=unsupported-binary-operation
- def update(
- self,
- new_annots: dict[str, str | int | float | bool],
- verbose: bool
- ) -> None:
- self.curr_annotations.submissionAnnotations.update(new_annots)
- self.curr_annotations = self.syn.store(self.curr_annotations)
- print(f"Submission ID {self.uid} annotations updated.")
-
- if verbose:
- print("Annotations:")
- print(json.dumps(self.curr_annotations.submissionAnnotations, indent=2))
-
- def update_with_file(self, annots_file: str, verbose: bool) -> None:
- with open(annots_file, encoding="utf-8") as f:
- new_annots = json.load(f)
-
- # Filter annotations with null and empty-list values
- new_annots = {
- key: value
- for key, value in new_annots.items()
- if value not in [None, []]
- }
- with_retry(
- lambda: self.update(new_annots, verbose),
- wait=3,
- retries=10,
- retry_status_codes=[412, 429, 500, 502, 503, 504],
- verbose=True,
- )
-
- def update_status(self, new_status: str) -> None:
- self.curr_annotations.status = new_status
- self.syn.store(self.curr_annotations)
- print(f"Updated submission ID {self.uid} to status: {new_status}")
diff --git a/cnb_tools/classes/base.py b/cnb_tools/classes/base.py
deleted file mode 100644
index 0d1723a..0000000
--- a/cnb_tools/classes/base.py
+++ /dev/null
@@ -1,53 +0,0 @@
-"""General class representing a Synapse entity."""
-
-import synapseclient
-from synapseclient.core.exceptions import SynapseNoCredentialsError
-
-
-class SynapseBase:
- def __init__(self, uid=None):
- self._syn = self._check_login()
- self._uid = uid
-
- @property
- def syn(self) -> str:
- """Synapse object with authentication."""
- return self._syn
-
- @syn.setter
- def syn(self, _: str) -> None:
- raise SynapseInternalError("syn object is read-only")
-
- @property
- def uid(self) -> str:
- """Synapse entity's unique ID."""
- return self._uid
-
- @uid.setter
- def uid(self, value: str) -> None:
- self._uid = value
-
- # pylint: disable=unsupported-binary-operation
- @staticmethod
- def _check_login() -> synapseclient.Synapse | None:
- try:
- return synapseclient.login(silent=True)
- except SynapseNoCredentialsError as err:
- raise SynapseLoginError(
- f"⛔ {err}\n\n"
- "Steps on how to provide your Synapse credentials to "
- "cnb-tools are available here: "
- "https://sage-bionetworks-challenges.github.io/cnb-tools/#requirements"
- ) from err
-
-
-class SynapseInternalError(Exception):
- pass
-
-
-class SynapseLoginError(SystemExit):
- pass
-
-
-class UnknownSynapseID(SystemExit):
- pass
diff --git a/cnb_tools/classes/participant.py b/cnb_tools/classes/participant.py
deleted file mode 100644
index b2ebc22..0000000
--- a/cnb_tools/classes/participant.py
+++ /dev/null
@@ -1,36 +0,0 @@
-"""Class representing a submission team or individual participant."""
-
-import sys
-
-import typer
-from synapseclient import Team
-from synapseclient.core.exceptions import SynapseHTTPError
-
-from cnb_tools.classes.base import SynapseBase
-
-
-class Participant(SynapseBase):
- def __str__(self) -> str:
- try:
- return self.syn.getTeam(self.uid).get("name")
- except SynapseHTTPError:
- return self.syn.getUserProfile(self.uid).get("userName")
-
- # pylint: disable=unsupported-binary-operation
- def create_team(
- self,
- name: str,
- description: str | None = None,
- can_public_join: bool = False
- ) -> Team:
- try:
- team = self.syn.getTeam(name)
- use_team = typer.confirm(f"Team '{name}' already exists. Use this team?")
- if not use_team:
- sys.exit("OK. Try again with a new challenge name.")
- except ValueError:
- team = Team(
- name=name, description=description, canPublicJoin=can_public_join
- )
- # team = self.syn.store(team)
- return team
diff --git a/cnb_tools/classes/queue.py b/cnb_tools/classes/queue.py
deleted file mode 100644
index 70a802e..0000000
--- a/cnb_tools/classes/queue.py
+++ /dev/null
@@ -1,34 +0,0 @@
-"""Class representing a challenge submission evaluation queue."""
-
-from synapseclient import Evaluation
-from synapseclient.core.exceptions import SynapseHTTPError
-
-from cnb_tools.classes.base import SynapseBase, UnknownSynapseID
-
-
-class Queue(SynapseBase):
- def __init__(self, uid):
- super().__init__(uid)
- try:
- self._queue = self.syn.getEvaluation(uid)
- except SynapseHTTPError as err:
- raise UnknownSynapseID(
- f"⛔ {err.response.json().get('reason')}. "
- "Check the ID and try again."
- ) from err
-
- @property
- def queue(self) -> Evaluation:
- """Synapse evaluation queue."""
- return self._queue
-
- @queue.setter
- def queue(self, value: Evaluation) -> None:
- self._queue = value
-
- def __str__(self) -> str:
- return self.queue.name
-
- def get_challenge_name(self) -> str:
- parent_id = self.queue.contentSource
- return self.syn.get(parent_id).name
diff --git a/cnb_tools/classes/submission.py b/cnb_tools/classes/submission.py
deleted file mode 100644
index 2347274..0000000
--- a/cnb_tools/classes/submission.py
+++ /dev/null
@@ -1,61 +0,0 @@
-"""Class representing a challenge submission."""
-
-from pathlib import Path
-from synapseclient.core.exceptions import SynapseHTTPError
-
-from cnb_tools.classes.annotation import SubmissionAnnotation
-from cnb_tools.classes.base import SynapseBase, UnknownSynapseID
-from cnb_tools.classes.participant import Participant
-from cnb_tools.classes.queue import Queue
-
-
-class Submission(SynapseBase):
- def __init__(self, sub_id: int):
- super().__init__(sub_id)
- try:
- self._submission = self.syn.getSubmission(sub_id, downloadFile=False)
- except SynapseHTTPError as err:
- raise UnknownSynapseID(
- f"⛔ {err.response.json().get('reason')}. "
- "Check the ID and try again."
- ) from err
-
- @property
- def submission(self):
- """Synapse submission."""
- return self._submission
-
- @submission.setter
- def submission(self, value) -> None:
- self._sumission = value
-
- def delete(self) -> None:
- self.syn.delete(self.submission)
- print(f"Submission deleted: {self.uid}")
-
- def download(self, dest) -> None:
- if "dockerDigest" in self.submission:
- print(
- f"Submission ID {self.uid} is a Docker image 🐳 To "
- "'download', run the following:\n\n"
- f"docker pull {self.submission.dockerRepositoryName}"
- f"@{self.submission.dockerDigest}\n\n"
- "If you receive an error, try logging in first with: "
- "docker login docker.synapse.org"
- )
- else:
- self.syn.getSubmission(self.uid, downloadLocation=dest)
- location = Path.cwd() if str(dest) == "." else dest
- print(f"Submission ID {self.uid} downloaded to: {location}")
-
- def info(self, verbose) -> None:
- challenge = Queue(self.submission.evaluationId).get_challenge_name()
- submitter_id = self.submission.get("teamId") or self.submission.get("userId")
- submitter = Participant(submitter_id)
- print(f" ID: {self.uid}")
- print(f" Challenge: {challenge}")
- print(f" Date: {self.submission.createdOn[:10]}")
- print(f" Submitter: {submitter}")
- if verbose:
- annotations = SubmissionAnnotation(self.uid)
- print(annotations)
diff --git a/cnb_tools/commands/submission_cli.py b/cnb_tools/commands/submission_cli.py
index 79df49f..5dacd17 100644
--- a/cnb_tools/commands/submission_cli.py
+++ b/cnb_tools/commands/submission_cli.py
@@ -13,9 +13,8 @@
from typing_extensions import Annotated
import typer
-from cnb_tools.classes.base import UnknownSynapseID
-from cnb_tools.classes.annotation import SubmissionAnnotation
-from cnb_tools.classes.submission import Submission
+from cnb_tools.modules.base import UnknownSynapseID
+from cnb_tools.modules import annotation, submission
class Status(str, Enum):
@@ -49,8 +48,7 @@ def annotate(
] = False,
):
"""Annotate one or more submission(s) with a JSON file."""
- submission_annots = SubmissionAnnotation(submission_id)
- submission_annots.update_with_file(json_file, verbose)
+ annotation.update_annotations_from_file(submission_id, str(json_file), verbose)
@app.command()
@@ -70,8 +68,7 @@ def change_status(
"""Update one or more submission statuses."""
for submission_id in submission_ids:
try:
- submission_annots = SubmissionAnnotation(submission_id)
- submission_annots.update_status(new_status)
+ annotation.update_submission_status(submission_id, new_status.value)
except UnknownSynapseID as err:
if skip_errors:
print(f"Unknown submission ID: {submission_id} - skipping...")
@@ -110,8 +107,7 @@ def delete(
if force:
for submission_id in submission_ids:
try:
- submission = Submission(submission_id)
- submission.delete()
+ submission.delete_submission(submission_id)
except UnknownSynapseID as err:
if skip_errors:
print(f"Unknown submission ID: {submission_id} - skipping...")
@@ -134,8 +130,7 @@ def download(
] = ".",
):
"""Get a submission (file/Docker image)"""
- submission = Submission(submission_id)
- submission.download(dest)
+ submission.download_submission(submission_id, str(dest))
@app.command()
@@ -154,8 +149,7 @@ def info(
] = False,
):
"""Get information about a submission"""
- submission = Submission(submission_id)
- submission.info(verbose)
+ submission.print_submission_info(submission_id, verbose)
@app.command()
@@ -173,7 +167,5 @@ def reset(
):
"""Reset one or more submission to RECEIVED."""
change_status(
- submission_ids=submission_ids,
- new_status="RECEIVED",
- skip_errors=skip_errors
+ submission_ids=submission_ids, new_status="RECEIVED", skip_errors=skip_errors
)
diff --git a/cnb_tools/main_cli.py b/cnb_tools/main_cli.py
index 06fc7ae..17bb07c 100644
--- a/cnb_tools/main_cli.py
+++ b/cnb_tools/main_cli.py
@@ -5,6 +5,7 @@
$ cnb-tools --help
$ cnb-tools --version
"""
+
from typing import Optional
from typing_extensions import Annotated
import typer
diff --git a/cnb_tools/modules/annotation.py b/cnb_tools/modules/annotation.py
new file mode 100644
index 0000000..53ffefd
--- /dev/null
+++ b/cnb_tools/modules/annotation.py
@@ -0,0 +1,128 @@
+"""Module for managing the annotations of challenge submissions.
+
+This module provides utility functions that extend synapseclient for managing
+submission annotations in Synapse challenges.
+"""
+
+import json
+
+from synapseclient import SubmissionStatus
+from synapseclient.core.exceptions import SynapseHTTPError
+from synapseclient.core.retry import with_retry
+
+from cnb_tools.modules.base import get_synapse_client, UnknownSynapseID
+
+
+def get_submission_status(submission_id: int) -> SubmissionStatus:
+ """Get the submission status object containing annotations.
+
+ Args:
+ submission_id: ID of the submission
+
+ Returns:
+ SubmissionStatus object with current annotations
+
+ Raises:
+ UnknownSynapseID: If the submission ID is invalid
+ """
+ syn = get_synapse_client()
+ try:
+ return syn.getSubmissionStatus(submission_id)
+ except SynapseHTTPError as err:
+ raise UnknownSynapseID(
+ f"⛔ {err.response.json().get('reason')}. " "Check the ID and try again."
+ ) from err
+
+
+def format_annotations(submission_status: SubmissionStatus) -> str:
+ """Format submission annotations for display.
+
+ Args:
+ submission_status: SubmissionStatus object
+
+ Returns:
+ Formatted string representation of status and annotations
+ """
+ output = f" Status: {submission_status.status}\n"
+ output += "Annotations:\n"
+ output += json.dumps(submission_status.submissionAnnotations, indent=2)
+ return output
+
+
+def update_annotations(
+ submission_id: int,
+ new_annotations: dict[str, str | int | float | bool],
+ verbose: bool = False,
+) -> SubmissionStatus:
+ """Update submission annotations.
+
+ Args:
+ submission_id: ID of the submission
+ new_annotations: Dictionary of annotations to add/update
+ verbose: If True, print updated annotations
+
+ Returns:
+ Updated SubmissionStatus object
+ """
+ syn = get_synapse_client()
+ status = get_submission_status(submission_id)
+ status.submissionAnnotations.update(new_annotations)
+ status = syn.store(status)
+
+ print(f"Submission ID {submission_id} annotations updated.")
+
+ if verbose:
+ print("Annotations:")
+ print(json.dumps(status.submissionAnnotations, indent=2))
+
+ return status
+
+
+def update_annotations_from_file(
+ submission_id: int, annots_file: str, verbose: bool = False
+) -> SubmissionStatus:
+ """Update submission annotations from a JSON file.
+
+ Args:
+ submission_id: ID of the submission
+ annots_file: Path to JSON file containing annotations
+ verbose: If True, print updated annotations
+
+ Returns:
+ Updated SubmissionStatus object
+ """
+ with open(annots_file, encoding="utf-8") as f:
+ new_annotations = json.load(f)
+
+ # Filter annotations with null and empty-list values
+ new_annotations = {
+ key: value for key, value in new_annotations.items() if value not in [None, []]
+ }
+
+ return with_retry(
+ lambda: update_annotations(submission_id, new_annotations, verbose),
+ wait=3,
+ retries=10,
+ retry_status_codes=[412, 429, 500, 502, 503, 504],
+ verbose=True,
+ )
+
+
+def update_submission_status(submission_id: int, new_status: str) -> SubmissionStatus:
+ """Update submission status.
+
+ Args:
+ submission_id: ID of the submission
+ new_status: New status value (e.g., 'ACCEPTED', 'REJECTED', 'SCORED')
+
+ Returns:
+ Updated SubmissionStatus object
+ """
+ syn = get_synapse_client()
+ status = get_submission_status(submission_id)
+ status.status = new_status
+ status = syn.store(status)
+
+ print(f"Updated submission ID {submission_id} to status: {new_status}")
+
+ return status
diff --git a/cnb_tools/modules/base.py b/cnb_tools/modules/base.py
new file mode 100644
index 0000000..7a5ff2e
--- /dev/null
+++ b/cnb_tools/modules/base.py
@@ -0,0 +1,28 @@
+"""Base module for cnb-tools, including Synapse client management and custom exceptions."""
+
+import synapseclient
+from synapseclient.core.exceptions import SynapseNoCredentialsError
+
+
+def get_synapse_client():
+ try:
+ return synapseclient.login(silent=True)
+ except SynapseNoCredentialsError as err:
+ raise SynapseLoginError(
+ f"⛔ {err}\n\n"
+ "Steps on how to provide your Synapse credentials to "
+ "cnb-tools are available here: "
+ "https://sage-bionetworks-challenges.github.io/cnb-tools/#requirements"
+ ) from err
+
+
+class SynapseInternalError(Exception):
+ pass
+
+
+class SynapseLoginError(SystemExit):
+ pass
+
+
+class UnknownSynapseID(SystemExit):
+ pass
diff --git a/cnb_tools/modules/participant.py b/cnb_tools/modules/participant.py
new file mode 100644
index 0000000..df26593
--- /dev/null
+++ b/cnb_tools/modules/participant.py
@@ -0,0 +1,57 @@
+"""Module for managing submission teams or individual participants.
+
+This module provides utility functions that extend synapseclient for managing
+teams and participants in Synapse challenges.
+"""
+
+import sys
+
+import typer
+from synapseclient import Team
+from synapseclient.core.exceptions import SynapseHTTPError
+
+from cnb_tools.modules.base import get_synapse_client
+
+
+def get_participant_name(participant_id: int) -> str:
+ """Get the name of a participant (team or user).
+
+ Args:
+ participant_id: Team ID or User ID
+
+ Returns:
+ Team name or username
+ """
+ syn = get_synapse_client()
+ try:
+ return syn.getTeam(participant_id).get("name")
+ except SynapseHTTPError:
+ return syn.getUserProfile(participant_id).get("userName")
+
+
+def create_team(
+ name: str, description: str | None = None, can_public_join: bool = False
+) -> Team:
+ """Create a new team or get an existing team by name.
+
+ Args:
+ name: Team name
+ description: Team description (optional)
+ can_public_join: Whether the team can be joined publicly
+
+ Returns:
+ Team object (existing or new)
+
+ Raises:
+ SystemExit: If user chooses not to use an existing team
+ """
+ syn = get_synapse_client()
+ try:
+ team = syn.getTeam(name)
+ use_team = typer.confirm(f"Team '{name}' already exists. Use this team?")
+ if not use_team:
+ sys.exit("OK. Try again with a new challenge name.")
+ except ValueError:
+ team = Team(name=name, description=description, canPublicJoin=can_public_join)
+ # team = syn.store(team)
+ return team
diff --git a/cnb_tools/modules/queue.py b/cnb_tools/modules/queue.py
new file mode 100644
index 0000000..637b0e9
--- /dev/null
+++ b/cnb_tools/modules/queue.py
@@ -0,0 +1,59 @@
+"""Module for managing the challenge submission evaluation queues.
+
+This module provides utility functions that extend synapseclient for managing
+evaluation queues in Synapse challenges.
+"""
+
+from synapseclient import Evaluation
+from synapseclient.core.exceptions import SynapseHTTPError
+
+from cnb_tools.modules.base import get_synapse_client, UnknownSynapseID
+
+
+def get_evaluation(evaluation_id: int) -> Evaluation:
+ """Get an evaluation queue by ID.
+
+ Args:
+ evaluation_id: Evaluation queue ID
+
+ Returns:
+ Evaluation object
+
+ Raises:
+ UnknownSynapseID: If the evaluation ID is invalid
+ """
+ syn = get_synapse_client()
+ try:
+ return syn.getEvaluation(evaluation_id)
+ except SynapseHTTPError as err:
+ raise UnknownSynapseID(
+ f"⛔ {err.response.json().get('reason')}. " "Check the ID and try again."
+ ) from err
+
+
+def get_evaluation_name(evaluation_id: int) -> str:
+ """Get the name of an evaluation queue.
+
+ Args:
+ evaluation_id: Evaluation queue ID
+
+ Returns:
+ Evaluation queue name
+ """
+ evaluation = get_evaluation(evaluation_id)
+ return evaluation.name
+
+
+def get_challenge_name_from_evaluation(evaluation_id: int) -> str:
+ """Get the challenge name for an evaluation queue.
+
+ Args:
+ evaluation_id: Evaluation queue ID
+
+ Returns:
+ Challenge name
+ """
+ syn = get_synapse_client()
+ evaluation = get_evaluation(evaluation_id)
+ parent_id = evaluation.contentSource
+ return syn.get(parent_id).name
diff --git a/cnb_tools/modules/submission.py b/cnb_tools/modules/submission.py
new file mode 100644
index 0000000..0efed20
--- /dev/null
+++ b/cnb_tools/modules/submission.py
@@ -0,0 +1,131 @@
+"""Module for managing challenge submissions.
+
+This module provides utility functions that extend synapseclient for managing
+submissions in Synapse challenges.
+"""
+
+from pathlib import Path
+from synapseclient import Submission as SynapseSubmission
+from synapseclient.core.exceptions import SynapseHTTPError
+
+from cnb_tools.modules.base import get_synapse_client, UnknownSynapseID
+from cnb_tools.modules import annotation
+
+
+def get_submission(submission_id: int, download_file: bool = False) -> SynapseSubmission:
+ """Get a submission by ID.
+
+ Args:
+ submission_id: ID of the submission
+ download_file: If True, download the submission file
+
+ Returns:
+ Synapse Submission object
+
+ Raises:
+ UnknownSynapseID: If the submission ID is invalid
+ """
+ syn = get_synapse_client()
+ try:
+ return syn.getSubmission(submission_id, downloadFile=download_file)
+ except SynapseHTTPError as err:
+ raise UnknownSynapseID(
+ f"⛔ {err.response.json().get('reason')}. "
+ "Check the ID and try again."
+ ) from err
+
+
+def delete_submission(submission_id: int) -> None:
+ """Delete a submission.
+
+ Args:
+ submission_id: ID of the submission to delete
+ """
+ syn = get_synapse_client()
+ submission = get_submission(submission_id)
+ syn.delete(submission)
+ print(f"Submission deleted: {submission_id}")
+
+
+def download_submission(submission_id: int, dest: str = ".") -> None:
+ """Download a submission file or display Docker pull command.
+
+ Args:
+ submission_id: ID of the submission
+ dest: Destination directory for download (default: current directory)
+ """
+ syn = get_synapse_client()
+ submission = get_submission(submission_id)
+
+ if "dockerDigest" in submission:
+ print(
+ f"Submission ID {submission_id} is a Docker image 🐳 To "
+ "'download', run the following:\n\n"
+ f"docker pull {submission.dockerRepositoryName}"
+ f"@{submission.dockerDigest}\n\n"
+ "If you receive an error, try logging in first with: "
+ "docker login docker.synapse.org"
+ )
+ else:
+ syn.getSubmission(submission_id, downloadLocation=dest)
+ location = Path.cwd() if str(dest) == "." else dest
+ print(f"Submission ID {submission_id} downloaded to: {location}")
+
+
+def get_submitter_name(submitter_id: int) -> str:
+ """Get the name of a submitter (team or user).
+
+ Args:
+ submitter_id: Team ID or User ID
+
+ Returns:
+ Team name or username
+ """
+ syn = get_synapse_client()
+ try:
+ return syn.getTeam(submitter_id).get("name")
+ except SynapseHTTPError:
+ return syn.getUserProfile(submitter_id).get("userName")
+
+
+def get_challenge_name(evaluation_id: int) -> str:
+ """Get the challenge name for an evaluation queue.
+
+ Args:
+ evaluation_id: Evaluation queue ID
+
+ Returns:
+ Challenge name
+ """
+ syn = get_synapse_client()
+ try:
+ evaluation = syn.getEvaluation(evaluation_id)
+ parent_id = evaluation.contentSource
+ return syn.get(parent_id).name
+ except SynapseHTTPError as err:
+ raise UnknownSynapseID(
+ f"⛔ {err.response.json().get('reason')}. "
+ "Check the ID and try again."
+ ) from err
+
+
+def print_submission_info(submission_id: int, verbose: bool = False) -> None:
+ """Print information about a submission.
+
+ Args:
+ submission_id: ID of the submission
+ verbose: If True, also print submission annotations
+ """
+ submission = get_submission(submission_id)
+ challenge = get_challenge_name(submission.evaluationId)
+ submitter_id = submission.get("teamId") or submission.get("userId")
+ submitter = get_submitter_name(submitter_id)
+
+ print(f" ID: {submission_id}")
+ print(f" Challenge: {challenge}")
+ print(f" Date: {submission.createdOn[:10]}")
+ print(f" Submitter: {submitter}")
+
+ if verbose:
+ status = annotation.get_submission_status(submission_id)
+ print(annotation.format_annotations(status))
diff --git a/cnb_tools/validation_toolkit.py b/cnb_tools/validation_toolkit.py
index 207d730..0e5af66 100644
--- a/cnb_tools/validation_toolkit.py
+++ b/cnb_tools/validation_toolkit.py
@@ -1,5 +1,3 @@
-from typing import Union
-
from pandas import Series
@@ -100,9 +98,7 @@ def check_nan_values(pred_col: Series) -> str:
return ""
-def check_binary_values(
- pred_col: Series, label1: int = 0, label2: int = 1
-) -> str:
+def check_binary_values(pred_col: Series, label1: int = 0, label2: int = 1) -> str:
"""Check that values are binary (default: 0 or 1).
Tip: Example Use Case
@@ -123,9 +119,7 @@ def check_binary_values(
def check_values_range(
- pred_col: Series,
- min_val: Union[int, float] = 0,
- max_val: Union[int, float] = 1
+ pred_col: Series, min_val: int | float = 0, max_val: int | float = 1
) -> str:
"""Check that values are between min and max values, inclusive.
diff --git a/docs/changelog/release-notes.md b/docs/changelog/release-notes.md
index 23fb3bb..14ba41f 100644
--- a/docs/changelog/release-notes.md
+++ b/docs/changelog/release-notes.md
@@ -7,7 +7,15 @@
- Add How-To tutorial on how the Validation Toolkit can be used
### Bug fixes
-- Remove custom classes, as to prevent future confusion with synapseclient's classes
+- Refactor all modules to remove custom classes ([#24](https://github.com/Sage-Bionetworks-Challenges/cnb-tools/issues/24))
+ - Convert `annotation.py`, `submission.py`, `participant.py`, and `queue.py` to functional utilities
+ - All modules now act as extensions to `synapseclient` rather than wrapping it with custom classes
+ - Synapse authentication is now handled automatically within each function via `get_synapse_client()`
+ - Simplifies API usage and prevents confusion with synapseclient's native classes
+
+### Internal
+- Drop support for Python 3.9 (reached [end of life](https://devguide.python.org/versions/))
+- Add unit tests
## 0.3.2
diff --git a/docs/index.md b/docs/index.md
index 5992611..5bcd313 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -9,7 +9,7 @@
-
+
@@ -29,23 +29,23 @@ but not limited to, [DREAM Challenges].
## Requirements
-- [Python 3.9+]
+- [Python 3.10+]
- [Synapse account]
To fully utilize **cnb-tools**, you must have a Synapse account and
-provide your credentials to the tool. To do so, create a `.synapseConfig`
-file in your home directory and enter the following:
+provide your credentials to the tool. First, generate a Synapse
+[Personal Access Token (PAT)] with all token permissions enabled.
-```yaml
-[authentication]
-authtoken=
-```
+Next, run the following command, then follow the prompts to enter
+your Synapse username and PAT:
-Generate a new Synapse [Personal Access Token (PAT)] with all token
-permissions enabled, then copy-paste it into `authtoken`. Save the file.
+```console
+synapse config
+```
-For security, we recommend updating its permissions so that other
-users on your machine do not have read access to your credentials, e.g.
+This will create a `.synapseConfig` file in your home directory. For
+security, we recommend updating its permissions so that other users
+on your machine do not have read access to your credentials, e.g.
```console
@@ -120,7 +120,7 @@ $ docker run --rm \
[https://sage-bionetworks-challenges.github.io/cnb-tools]: https://sage-bionetworks-challenges.github.io/cnb-tools
[https://github.com/Sage-Bionetworks-Challenges/cnb-tools]: https://github.com/Sage-Bionetworks-Challenges/cnb-tools
[DREAM Challenges]: https://dreamchallenges.org/
-[Python 3.9+]: https://www.python.org/downloads/
+[Python 3.10+]: https://www.python.org/downloads/
[Synapse account]: https://www.synapse.org/#!LoginPlace:0
[Personal Access Token (PAT)]: https://www.synapse.org/#!PersonalAccessTokens:
[miniconda]: https://docs.conda.io/projects/miniconda/en/latest/miniconda-install.html
diff --git a/poetry.lock b/poetry.lock
index c32cc7b..2927b42 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,4 +1,4 @@
-# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
+# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand.
[[package]]
name = "anyio"
@@ -6,6 +6,7 @@ version = "4.3.0"
description = "High level compatibility layer for multiple asynchronous event loop implementations"
optional = false
python-versions = ">=3.8"
+groups = ["main"]
files = [
{file = "anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8"},
{file = "anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6"},
@@ -19,7 +20,7 @@ typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""}
[package.extras]
doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"]
-test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"]
+test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\""]
trio = ["trio (>=0.23)"]
[[package]]
@@ -28,6 +29,7 @@ version = "2.0.4"
description = "Simple LRU cache for asyncio"
optional = false
python-versions = ">=3.8"
+groups = ["main"]
files = [
{file = "async-lru-2.0.4.tar.gz", hash = "sha256:b8a59a5df60805ff63220b2a0c5b5393da5521b113cd5465a44eb037d81a5627"},
{file = "async_lru-2.0.4-py3-none-any.whl", hash = "sha256:ff02944ce3c288c5be660c42dbcca0742b32c3b279d6dceda655190240b99224"},
@@ -42,6 +44,7 @@ version = "1.0.1"
description = "Like atexit, but for asyncio"
optional = false
python-versions = ">=3.6"
+groups = ["main"]
files = [
{file = "asyncio-atexit-1.0.1.tar.gz", hash = "sha256:1d0c71544b8ee2c484d322844ee72c0875dde6f250c0ed5b6993592ab9f7d436"},
{file = "asyncio_atexit-1.0.1-py3-none-any.whl", hash = "sha256:d93d5f7d5633a534abd521ce2896ed0fbe8de170bb1e65ec871d1c20eac9d376"},
@@ -56,6 +59,7 @@ version = "2.15.0"
description = "Internationalization utilities"
optional = false
python-versions = ">=3.8"
+groups = ["docs"]
files = [
{file = "Babel-2.15.0-py3-none-any.whl", hash = "sha256:08706bdad8d0a3413266ab61bd6c34d0c28d6e1e7badf40a2cebe67644e2e1fb"},
{file = "babel-2.15.0.tar.gz", hash = "sha256:8daf0e265d05768bc6c7a314cf1321e9a123afc328cc635c18622a2f30a04413"},
@@ -64,23 +68,13 @@ files = [
[package.extras]
dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"]
-[[package]]
-name = "backoff"
-version = "2.2.1"
-description = "Function decoration for backoff and retry"
-optional = false
-python-versions = ">=3.7,<4.0"
-files = [
- {file = "backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8"},
- {file = "backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba"},
-]
-
[[package]]
name = "certifi"
version = "2024.2.2"
description = "Python package for providing Mozilla's CA Bundle."
optional = false
python-versions = ">=3.6"
+groups = ["main", "docs"]
files = [
{file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"},
{file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"},
@@ -92,6 +86,7 @@ version = "3.3.2"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
optional = false
python-versions = ">=3.7.0"
+groups = ["main", "docs"]
files = [
{file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"},
{file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"},
@@ -191,6 +186,7 @@ version = "8.1.7"
description = "Composable command line interface toolkit"
optional = false
python-versions = ">=3.7"
+groups = ["main", "docs"]
files = [
{file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
{file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
@@ -199,27 +195,18 @@ files = [
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
-[[package]]
-name = "cloudpickle"
-version = "3.0.0"
-description = "Pickler class to extend the standard pickle.Pickler functionality"
-optional = false
-python-versions = ">=3.8"
-files = [
- {file = "cloudpickle-3.0.0-py3-none-any.whl", hash = "sha256:246ee7d0c295602a036e86369c77fecda4ab17b506496730f2f576d9016fd9c7"},
- {file = "cloudpickle-3.0.0.tar.gz", hash = "sha256:996d9a482c6fb4f33c1a35335cf8afd065d2a56e973270364840712d9131a882"},
-]
-
[[package]]
name = "colorama"
version = "0.4.6"
description = "Cross-platform colored terminal text."
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
+groups = ["main", "docs", "test"]
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
+markers = {test = "sys_platform == \"win32\""}
[[package]]
name = "deprecated"
@@ -227,6 +214,7 @@ version = "1.2.14"
description = "Python @deprecated decorator to deprecate old python classes, functions or methods."
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+groups = ["main"]
files = [
{file = "Deprecated-1.2.14-py2.py3-none-any.whl", hash = "sha256:6fac8b097794a90302bdbb17b9b815e732d3c4720583ff1b198499d78470466c"},
{file = "Deprecated-1.2.14.tar.gz", hash = "sha256:e5323eb936458dccc2582dc6f9c322c852a775a27065ff2b0c4970b9d53d01b3"},
@@ -244,6 +232,8 @@ version = "1.2.1"
description = "Backport of PEP 654 (exception groups)"
optional = false
python-versions = ">=3.7"
+groups = ["main", "test"]
+markers = "python_version == \"3.10\""
files = [
{file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"},
{file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"},
@@ -258,6 +248,7 @@ version = "2.1.0"
description = "Copy your docs directly to the gh-pages branch."
optional = false
python-versions = "*"
+groups = ["docs"]
files = [
{file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"},
{file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"},
@@ -271,20 +262,21 @@ dev = ["flake8", "markdown", "twine", "wheel"]
[[package]]
name = "googleapis-common-protos"
-version = "1.63.0"
+version = "1.72.0"
description = "Common protobufs used in Google APIs"
optional = false
python-versions = ">=3.7"
+groups = ["main"]
files = [
- {file = "googleapis-common-protos-1.63.0.tar.gz", hash = "sha256:17ad01b11d5f1d0171c06d3ba5c04c54474e883b66b949722b4938ee2694ef4e"},
- {file = "googleapis_common_protos-1.63.0-py2.py3-none-any.whl", hash = "sha256:ae45f75702f7c08b541f750854a678bd8f534a1a6bace6afe975f1d0a82d6632"},
+ {file = "googleapis_common_protos-1.72.0-py3-none-any.whl", hash = "sha256:4299c5a82d5ae1a9702ada957347726b167f9f8d1fc352477702a1e851ff4038"},
+ {file = "googleapis_common_protos-1.72.0.tar.gz", hash = "sha256:e55a601c1b32b52d7a3e65f43563e2aa61bcd737998ee672ac9b951cd49319f5"},
]
[package.dependencies]
-protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<5.0.0.dev0"
+protobuf = ">=3.20.2,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0"
[package.extras]
-grpc = ["grpcio (>=1.44.0,<2.0.0.dev0)"]
+grpc = ["grpcio (>=1.44.0,<2.0.0)"]
[[package]]
name = "griffe"
@@ -292,6 +284,7 @@ version = "0.45.0"
description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API."
optional = false
python-versions = ">=3.8"
+groups = ["docs"]
files = [
{file = "griffe-0.45.0-py3-none-any.whl", hash = "sha256:90fe5c90e1b0ca7dd6fee78f9009f4e01b37dbc9ab484a9b2c1578915db1e571"},
{file = "griffe-0.45.0.tar.gz", hash = "sha256:85cb2868d026ea51c89bdd589ad3ccc94abc5bd8d5d948e3d4450778a2a05b4a"},
@@ -302,35 +295,37 @@ colorama = ">=0.4"
[[package]]
name = "h11"
-version = "0.14.0"
+version = "0.16.0"
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
+groups = ["main"]
files = [
- {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"},
- {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"},
+ {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"},
+ {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"},
]
[[package]]
name = "httpcore"
-version = "1.0.5"
+version = "1.0.9"
description = "A minimal low-level HTTP client."
optional = false
python-versions = ">=3.8"
+groups = ["main"]
files = [
- {file = "httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"},
- {file = "httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61"},
+ {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"},
+ {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"},
]
[package.dependencies]
certifi = "*"
-h11 = ">=0.13,<0.15"
+h11 = ">=0.16"
[package.extras]
asyncio = ["anyio (>=4.0,<5.0)"]
http2 = ["h2 (>=3,<5)"]
socks = ["socksio (==1.*)"]
-trio = ["trio (>=0.22.0,<0.26.0)"]
+trio = ["trio (>=0.22.0,<1.0)"]
[[package]]
name = "httpx"
@@ -338,6 +333,7 @@ version = "0.27.0"
description = "The next generation HTTP client."
optional = false
python-versions = ">=3.8"
+groups = ["main"]
files = [
{file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"},
{file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"},
@@ -351,7 +347,7 @@ idna = "*"
sniffio = "*"
[package.extras]
-brotli = ["brotli", "brotlicffi"]
+brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""]
cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"]
http2 = ["h2 (>=3,<5)"]
socks = ["socksio (==1.*)"]
@@ -362,6 +358,7 @@ version = "3.7"
description = "Internationalized Domain Names in Applications (IDNA)"
optional = false
python-versions = ">=3.5"
+groups = ["main", "docs"]
files = [
{file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"},
{file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"},
@@ -373,6 +370,7 @@ version = "6.11.0"
description = "Read metadata from Python packages"
optional = false
python-versions = ">=3.8"
+groups = ["main"]
files = [
{file = "importlib_metadata-6.11.0-py3-none-any.whl", hash = "sha256:f0afba6205ad8f8947c7d338b5342d5db2afbfd82f9cbef7879a9539cc12eb9b"},
{file = "importlib_metadata-6.11.0.tar.gz", hash = "sha256:1231cf92d825c9e03cfc4da076a16de6422c863558229ea0b22b675657463443"},
@@ -384,7 +382,7 @@ zipp = ">=0.5"
[package.extras]
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"]
perf = ["ipython"]
-testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"]
+testing = ["flufl.flake8", "importlib-resources (>=1.3) ; python_version < \"3.9\"", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7) ; platform_python_implementation != \"PyPy\"", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1) ; platform_python_implementation != \"PyPy\"", "pytest-perf (>=0.9.2)", "pytest-ruff"]
[[package]]
name = "iniconfig"
@@ -392,6 +390,7 @@ version = "2.0.0"
description = "brain-dead simple config-ini parsing"
optional = false
python-versions = ">=3.7"
+groups = ["test"]
files = [
{file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
@@ -403,6 +402,7 @@ version = "3.1.4"
description = "A very fast and expressive template engine."
optional = false
python-versions = ">=3.7"
+groups = ["docs"]
files = [
{file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"},
{file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"},
@@ -414,34 +414,18 @@ MarkupSafe = ">=2.0"
[package.extras]
i18n = ["Babel (>=2.7)"]
-[[package]]
-name = "loky"
-version = "3.0.0"
-description = "A robust implementation of concurrent.futures.ProcessPoolExecutor"
-optional = false
-python-versions = "*"
-files = [
- {file = "loky-3.0.0-py2.py3-none-any.whl", hash = "sha256:1d5a4d778c7ff09c919aa3fbf2d879a2c7ac936a545c615af40e080a1c902b82"},
- {file = "loky-3.0.0.tar.gz", hash = "sha256:fd8750b24b283a579bafaf0631d114aa4487c682aef6fce01fa3635336297fdf"},
-]
-
-[package.dependencies]
-cloudpickle = "*"
-
[[package]]
name = "markdown"
version = "3.6"
description = "Python implementation of John Gruber's Markdown."
optional = false
python-versions = ">=3.8"
+groups = ["docs"]
files = [
{file = "Markdown-3.6-py3-none-any.whl", hash = "sha256:48f276f4d8cfb8ce6527c8f79e2ee29708508bf4d40aa410fbc3b4ee832c850f"},
{file = "Markdown-3.6.tar.gz", hash = "sha256:ed4f41f6daecbeeb96e576ce414c41d2d876daa9a16cb35fa8ed8c2ddfad0224"},
]
-[package.dependencies]
-importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""}
-
[package.extras]
docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.5)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"]
testing = ["coverage", "pyyaml"]
@@ -452,6 +436,7 @@ version = "3.0.0"
description = "Python port of markdown-it. Markdown parsing, done right!"
optional = false
python-versions = ">=3.8"
+groups = ["main"]
files = [
{file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"},
{file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"},
@@ -476,6 +461,7 @@ version = "2.1.5"
description = "Safely add untrusted strings to HTML/XML markup."
optional = false
python-versions = ">=3.7"
+groups = ["docs"]
files = [
{file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"},
{file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"},
@@ -545,6 +531,7 @@ version = "0.1.2"
description = "Markdown URL utilities"
optional = false
python-versions = ">=3.7"
+groups = ["main"]
files = [
{file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"},
{file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
@@ -556,6 +543,7 @@ version = "1.3.4"
description = "A deep merge function for 🐍."
optional = false
python-versions = ">=3.6"
+groups = ["docs"]
files = [
{file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"},
{file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"},
@@ -567,6 +555,7 @@ version = "1.6.0"
description = "Project documentation with Markdown."
optional = false
python-versions = ">=3.8"
+groups = ["docs"]
files = [
{file = "mkdocs-1.6.0-py3-none-any.whl", hash = "sha256:1eb5cb7676b7d89323e62b56235010216319217d4af5ddc543a91beb8d125ea7"},
{file = "mkdocs-1.6.0.tar.gz", hash = "sha256:a73f735824ef83a4f3bcb7a231dcab23f5a838f88b7efc54a0eef5fbdbc3c512"},
@@ -576,7 +565,6 @@ files = [
click = ">=7.0"
colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""}
ghp-import = ">=1.0"
-importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""}
jinja2 = ">=2.11.1"
markdown = ">=3.3.6"
markupsafe = ">=2.0.1"
@@ -590,7 +578,7 @@ watchdog = ">=2.0"
[package.extras]
i18n = ["babel (>=2.9.0)"]
-min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-import (==1.0)", "importlib-metadata (==4.4)", "jinja2 (==2.11.1)", "markdown (==3.3.6)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "mkdocs-get-deps (==0.2.0)", "packaging (==20.5)", "pathspec (==0.11.1)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "watchdog (==2.0)"]
+min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4) ; platform_system == \"Windows\"", "ghp-import (==1.0)", "importlib-metadata (==4.4) ; python_version < \"3.10\"", "jinja2 (==2.11.1)", "markdown (==3.3.6)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "mkdocs-get-deps (==0.2.0)", "packaging (==20.5)", "pathspec (==0.11.1)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "watchdog (==2.0)"]
[[package]]
name = "mkdocs-autorefs"
@@ -598,6 +586,7 @@ version = "1.0.1"
description = "Automatically link across pages in MkDocs."
optional = false
python-versions = ">=3.8"
+groups = ["docs"]
files = [
{file = "mkdocs_autorefs-1.0.1-py3-none-any.whl", hash = "sha256:aacdfae1ab197780fb7a2dac92ad8a3d8f7ca8049a9cbe56a4218cd52e8da570"},
{file = "mkdocs_autorefs-1.0.1.tar.gz", hash = "sha256:f684edf847eced40b570b57846b15f0bf57fb93ac2c510450775dcf16accb971"},
@@ -614,13 +603,13 @@ version = "0.2.0"
description = "MkDocs extension that lists all dependencies according to a mkdocs.yml file"
optional = false
python-versions = ">=3.8"
+groups = ["docs"]
files = [
{file = "mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134"},
{file = "mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c"},
]
[package.dependencies]
-importlib-metadata = {version = ">=4.3", markers = "python_version < \"3.10\""}
mergedeep = ">=1.3.4"
platformdirs = ">=2.2.0"
pyyaml = ">=5.1"
@@ -631,6 +620,7 @@ version = "9.5.23"
description = "Documentation that simply works"
optional = false
python-versions = ">=3.8"
+groups = ["docs"]
files = [
{file = "mkdocs_material-9.5.23-py3-none-any.whl", hash = "sha256:ffd08a5beaef3cd135aceb58ded8b98bbbbf2b70e5b656f6a14a63c917d9b001"},
{file = "mkdocs_material-9.5.23.tar.gz", hash = "sha256:4627fc3f15de2cba2bde9debc2fd59b9888ef494beabfe67eb352e23d14bf288"},
@@ -660,6 +650,7 @@ version = "1.3.1"
description = "Extension pack for Python Markdown and MkDocs Material."
optional = false
python-versions = ">=3.8"
+groups = ["docs"]
files = [
{file = "mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31"},
{file = "mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443"},
@@ -671,6 +662,7 @@ version = "0.24.3"
description = "Automatic documentation from sources, for MkDocs."
optional = false
python-versions = ">=3.8"
+groups = ["docs"]
files = [
{file = "mkdocstrings-0.24.3-py3-none-any.whl", hash = "sha256:5c9cf2a32958cd161d5428699b79c8b0988856b0d4a8c5baf8395fc1bf4087c3"},
{file = "mkdocstrings-0.24.3.tar.gz", hash = "sha256:f327b234eb8d2551a306735436e157d0a22d45f79963c60a8b585d5f7a94c1d2"},
@@ -678,7 +670,6 @@ files = [
[package.dependencies]
click = ">=7.0"
-importlib-metadata = {version = ">=4.6", markers = "python_version < \"3.10\""}
Jinja2 = ">=2.11.1"
Markdown = ">=3.3"
MarkupSafe = ">=1.1"
@@ -686,7 +677,6 @@ mkdocs = ">=1.4"
mkdocs-autorefs = ">=0.3.1"
platformdirs = ">=2.2.0"
pymdown-extensions = ">=6.3"
-typing-extensions = {version = ">=4.1", markers = "python_version < \"3.10\""}
[package.extras]
crystal = ["mkdocstrings-crystal (>=0.3.4)"]
@@ -699,6 +689,7 @@ version = "1.10.0"
description = "A Python handler for mkdocstrings."
optional = false
python-versions = ">=3.8"
+groups = ["docs"]
files = [
{file = "mkdocstrings_python-1.10.0-py3-none-any.whl", hash = "sha256:ba833fbd9d178a4b9d5cb2553a4df06e51dc1f51e41559a4d2398c16a6f69ecc"},
{file = "mkdocstrings_python-1.10.0.tar.gz", hash = "sha256:71678fac657d4d2bb301eed4e4d2d91499c095fd1f8a90fa76422a87a5693828"},
@@ -714,104 +705,369 @@ version = "1.6.0"
description = "Patch asyncio to allow nested event loops"
optional = false
python-versions = ">=3.5"
+groups = ["main"]
files = [
{file = "nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c"},
{file = "nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe"},
]
+[[package]]
+name = "numpy"
+version = "2.2.6"
+description = "Fundamental package for array computing in Python"
+optional = false
+python-versions = ">=3.10"
+groups = ["main"]
+markers = "python_version == \"3.10\""
+files = [
+ {file = "numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb"},
+ {file = "numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90"},
+ {file = "numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163"},
+ {file = "numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf"},
+ {file = "numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83"},
+ {file = "numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915"},
+ {file = "numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680"},
+ {file = "numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289"},
+ {file = "numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d"},
+ {file = "numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3"},
+ {file = "numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae"},
+ {file = "numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a"},
+ {file = "numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42"},
+ {file = "numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491"},
+ {file = "numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a"},
+ {file = "numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf"},
+ {file = "numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1"},
+ {file = "numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab"},
+ {file = "numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47"},
+ {file = "numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303"},
+ {file = "numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff"},
+ {file = "numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c"},
+ {file = "numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3"},
+ {file = "numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282"},
+ {file = "numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87"},
+ {file = "numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249"},
+ {file = "numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49"},
+ {file = "numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de"},
+ {file = "numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4"},
+ {file = "numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2"},
+ {file = "numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84"},
+ {file = "numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b"},
+ {file = "numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d"},
+ {file = "numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566"},
+ {file = "numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f"},
+ {file = "numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f"},
+ {file = "numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868"},
+ {file = "numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d"},
+ {file = "numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd"},
+ {file = "numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c"},
+ {file = "numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6"},
+ {file = "numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda"},
+ {file = "numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40"},
+ {file = "numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8"},
+ {file = "numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f"},
+ {file = "numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa"},
+ {file = "numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571"},
+ {file = "numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1"},
+ {file = "numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff"},
+ {file = "numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06"},
+ {file = "numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d"},
+ {file = "numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db"},
+ {file = "numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543"},
+ {file = "numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00"},
+ {file = "numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd"},
+]
+
+[[package]]
+name = "numpy"
+version = "2.3.5"
+description = "Fundamental package for array computing in Python"
+optional = false
+python-versions = ">=3.11"
+groups = ["main"]
+markers = "python_version >= \"3.11\""
+files = [
+ {file = "numpy-2.3.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:de5672f4a7b200c15a4127042170a694d4df43c992948f5e1af57f0174beed10"},
+ {file = "numpy-2.3.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:acfd89508504a19ed06ef963ad544ec6664518c863436306153e13e94605c218"},
+ {file = "numpy-2.3.5-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:ffe22d2b05504f786c867c8395de703937f934272eb67586817b46188b4ded6d"},
+ {file = "numpy-2.3.5-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:872a5cf366aec6bb1147336480fef14c9164b154aeb6542327de4970282cd2f5"},
+ {file = "numpy-2.3.5-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3095bdb8dd297e5920b010e96134ed91d852d81d490e787beca7e35ae1d89cf7"},
+ {file = "numpy-2.3.5-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8cba086a43d54ca804ce711b2a940b16e452807acebe7852ff327f1ecd49b0d4"},
+ {file = "numpy-2.3.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6cf9b429b21df6b99f4dee7a1218b8b7ffbbe7df8764dc0bd60ce8a0708fed1e"},
+ {file = "numpy-2.3.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:396084a36abdb603546b119d96528c2f6263921c50df3c8fd7cb28873a237748"},
+ {file = "numpy-2.3.5-cp311-cp311-win32.whl", hash = "sha256:b0c7088a73aef3d687c4deef8452a3ac7c1be4e29ed8bf3b366c8111128ac60c"},
+ {file = "numpy-2.3.5-cp311-cp311-win_amd64.whl", hash = "sha256:a414504bef8945eae5f2d7cb7be2d4af77c5d1cb5e20b296c2c25b61dff2900c"},
+ {file = "numpy-2.3.5-cp311-cp311-win_arm64.whl", hash = "sha256:0cd00b7b36e35398fa2d16af7b907b65304ef8bb4817a550e06e5012929830fa"},
+ {file = "numpy-2.3.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:74ae7b798248fe62021dbf3c914245ad45d1a6b0cb4a29ecb4b31d0bfbc4cc3e"},
+ {file = "numpy-2.3.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee3888d9ff7c14604052b2ca5535a30216aa0a58e948cdd3eeb8d3415f638769"},
+ {file = "numpy-2.3.5-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:612a95a17655e213502f60cfb9bf9408efdc9eb1d5f50535cc6eb365d11b42b5"},
+ {file = "numpy-2.3.5-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3101e5177d114a593d79dd79658650fe28b5a0d8abeb8ce6f437c0e6df5be1a4"},
+ {file = "numpy-2.3.5-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b973c57ff8e184109db042c842423ff4f60446239bd585a5131cc47f06f789d"},
+ {file = "numpy-2.3.5-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d8163f43acde9a73c2a33605353a4f1bc4798745a8b1d73183b28e5b435ae28"},
+ {file = "numpy-2.3.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:51c1e14eb1e154ebd80e860722f9e6ed6ec89714ad2db2d3aa33c31d7c12179b"},
+ {file = "numpy-2.3.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b46b4ec24f7293f23adcd2d146960559aaf8020213de8ad1909dba6c013bf89c"},
+ {file = "numpy-2.3.5-cp312-cp312-win32.whl", hash = "sha256:3997b5b3c9a771e157f9aae01dd579ee35ad7109be18db0e85dbdbe1de06e952"},
+ {file = "numpy-2.3.5-cp312-cp312-win_amd64.whl", hash = "sha256:86945f2ee6d10cdfd67bcb4069c1662dd711f7e2a4343db5cecec06b87cf31aa"},
+ {file = "numpy-2.3.5-cp312-cp312-win_arm64.whl", hash = "sha256:f28620fe26bee16243be2b7b874da327312240a7cdc38b769a697578d2100013"},
+ {file = "numpy-2.3.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d0f23b44f57077c1ede8c5f26b30f706498b4862d3ff0a7298b8411dd2f043ff"},
+ {file = "numpy-2.3.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa5bc7c5d59d831d9773d1170acac7893ce3a5e130540605770ade83280e7188"},
+ {file = "numpy-2.3.5-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:ccc933afd4d20aad3c00bcef049cb40049f7f196e0397f1109dba6fed63267b0"},
+ {file = "numpy-2.3.5-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:afaffc4393205524af9dfa400fa250143a6c3bc646c08c9f5e25a9f4b4d6a903"},
+ {file = "numpy-2.3.5-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c75442b2209b8470d6d5d8b1c25714270686f14c749028d2199c54e29f20b4d"},
+ {file = "numpy-2.3.5-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11e06aa0af8c0f05104d56450d6093ee639e15f24ecf62d417329d06e522e017"},
+ {file = "numpy-2.3.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ed89927b86296067b4f81f108a2271d8926467a8868e554eaf370fc27fa3ccaf"},
+ {file = "numpy-2.3.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51c55fe3451421f3a6ef9a9c1439e82101c57a2c9eab9feb196a62b1a10b58ce"},
+ {file = "numpy-2.3.5-cp313-cp313-win32.whl", hash = "sha256:1978155dd49972084bd6ef388d66ab70f0c323ddee6f693d539376498720fb7e"},
+ {file = "numpy-2.3.5-cp313-cp313-win_amd64.whl", hash = "sha256:00dc4e846108a382c5869e77c6ed514394bdeb3403461d25a829711041217d5b"},
+ {file = "numpy-2.3.5-cp313-cp313-win_arm64.whl", hash = "sha256:0472f11f6ec23a74a906a00b48a4dcf3849209696dff7c189714511268d103ae"},
+ {file = "numpy-2.3.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:414802f3b97f3c1eef41e530aaba3b3c1620649871d8cb38c6eaff034c2e16bd"},
+ {file = "numpy-2.3.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5ee6609ac3604fa7780e30a03e5e241a7956f8e2fcfe547d51e3afa5247ac47f"},
+ {file = "numpy-2.3.5-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:86d835afea1eaa143012a2d7a3f45a3adce2d7adc8b4961f0b362214d800846a"},
+ {file = "numpy-2.3.5-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:30bc11310e8153ca664b14c5f1b73e94bd0503681fcf136a163de856f3a50139"},
+ {file = "numpy-2.3.5-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1062fde1dcf469571705945b0f221b73928f34a20c904ffb45db101907c3454e"},
+ {file = "numpy-2.3.5-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce581db493ea1a96c0556360ede6607496e8bf9b3a8efa66e06477267bc831e9"},
+ {file = "numpy-2.3.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:cc8920d2ec5fa99875b670bb86ddeb21e295cb07aa331810d9e486e0b969d946"},
+ {file = "numpy-2.3.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9ee2197ef8c4f0dfe405d835f3b6a14f5fee7782b5de51ba06fb65fc9b36e9f1"},
+ {file = "numpy-2.3.5-cp313-cp313t-win32.whl", hash = "sha256:70b37199913c1bd300ff6e2693316c6f869c7ee16378faf10e4f5e3275b299c3"},
+ {file = "numpy-2.3.5-cp313-cp313t-win_amd64.whl", hash = "sha256:b501b5fa195cc9e24fe102f21ec0a44dffc231d2af79950b451e0d99cea02234"},
+ {file = "numpy-2.3.5-cp313-cp313t-win_arm64.whl", hash = "sha256:a80afd79f45f3c4a7d341f13acbe058d1ca8ac017c165d3fa0d3de6bc1a079d7"},
+ {file = "numpy-2.3.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:bf06bc2af43fa8d32d30fae16ad965663e966b1a3202ed407b84c989c3221e82"},
+ {file = "numpy-2.3.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:052e8c42e0c49d2575621c158934920524f6c5da05a1d3b9bab5d8e259e045f0"},
+ {file = "numpy-2.3.5-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:1ed1ec893cff7040a02c8aa1c8611b94d395590d553f6b53629a4461dc7f7b63"},
+ {file = "numpy-2.3.5-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:2dcd0808a421a482a080f89859a18beb0b3d1e905b81e617a188bd80422d62e9"},
+ {file = "numpy-2.3.5-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:727fd05b57df37dc0bcf1a27767a3d9a78cbbc92822445f32cc3436ba797337b"},
+ {file = "numpy-2.3.5-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fffe29a1ef00883599d1dc2c51aa2e5d80afe49523c261a74933df395c15c520"},
+ {file = "numpy-2.3.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8f7f0e05112916223d3f438f293abf0727e1181b5983f413dfa2fefc4098245c"},
+ {file = "numpy-2.3.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2e2eb32ddb9ccb817d620ac1d8dae7c3f641c1e5f55f531a33e8ab97960a75b8"},
+ {file = "numpy-2.3.5-cp314-cp314-win32.whl", hash = "sha256:66f85ce62c70b843bab1fb14a05d5737741e74e28c7b8b5a064de10142fad248"},
+ {file = "numpy-2.3.5-cp314-cp314-win_amd64.whl", hash = "sha256:e6a0bc88393d65807d751a614207b7129a310ca4fe76a74e5c7da5fa5671417e"},
+ {file = "numpy-2.3.5-cp314-cp314-win_arm64.whl", hash = "sha256:aeffcab3d4b43712bb7a60b65f6044d444e75e563ff6180af8f98dd4b905dfd2"},
+ {file = "numpy-2.3.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:17531366a2e3a9e30762c000f2c43a9aaa05728712e25c11ce1dbe700c53ad41"},
+ {file = "numpy-2.3.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d21644de1b609825ede2f48be98dfde4656aefc713654eeee280e37cadc4e0ad"},
+ {file = "numpy-2.3.5-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:c804e3a5aba5460c73955c955bdbd5c08c354954e9270a2c1565f62e866bdc39"},
+ {file = "numpy-2.3.5-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:cc0a57f895b96ec78969c34f682c602bf8da1a0270b09bc65673df2e7638ec20"},
+ {file = "numpy-2.3.5-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:900218e456384ea676e24ea6a0417f030a3b07306d29d7ad843957b40a9d8d52"},
+ {file = "numpy-2.3.5-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:09a1bea522b25109bf8e6f3027bd810f7c1085c64a0c7ce050c1676ad0ba010b"},
+ {file = "numpy-2.3.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:04822c00b5fd0323c8166d66c701dc31b7fbd252c100acd708c48f763968d6a3"},
+ {file = "numpy-2.3.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d6889ec4ec662a1a37eb4b4fb26b6100841804dac55bd9df579e326cdc146227"},
+ {file = "numpy-2.3.5-cp314-cp314t-win32.whl", hash = "sha256:93eebbcf1aafdf7e2ddd44c2923e2672e1010bddc014138b229e49725b4d6be5"},
+ {file = "numpy-2.3.5-cp314-cp314t-win_amd64.whl", hash = "sha256:c8a9958e88b65c3b27e22ca2a076311636850b612d6bbfb76e8d156aacde2aaf"},
+ {file = "numpy-2.3.5-cp314-cp314t-win_arm64.whl", hash = "sha256:6203fdf9f3dc5bdaed7319ad8698e685c7a3be10819f41d32a0723e611733b42"},
+ {file = "numpy-2.3.5-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f0963b55cdd70fad460fa4c1341f12f976bb26cb66021a5580329bd498988310"},
+ {file = "numpy-2.3.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f4255143f5160d0de972d28c8f9665d882b5f61309d8362fdd3e103cf7bf010c"},
+ {file = "numpy-2.3.5-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:a4b9159734b326535f4dd01d947f919c6eefd2d9827466a696c44ced82dfbc18"},
+ {file = "numpy-2.3.5-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:2feae0d2c91d46e59fcd62784a3a83b3fb677fead592ce51b5a6fbb4f95965ff"},
+ {file = "numpy-2.3.5-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffac52f28a7849ad7576293c0cb7b9f08304e8f7d738a8cb8a90ec4c55a998eb"},
+ {file = "numpy-2.3.5-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63c0e9e7eea69588479ebf4a8a270d5ac22763cc5854e9a7eae952a3908103f7"},
+ {file = "numpy-2.3.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f16417ec91f12f814b10bafe79ef77e70113a2f5f7018640e7425ff979253425"},
+ {file = "numpy-2.3.5.tar.gz", hash = "sha256:784db1dcdab56bf0517743e746dfb0f885fc68d948aba86eeec2cba234bdf1c0"},
+]
+
[[package]]
name = "opentelemetry-api"
-version = "1.21.0"
+version = "1.38.0"
description = "OpenTelemetry Python API"
optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.9"
+groups = ["main"]
files = [
- {file = "opentelemetry_api-1.21.0-py3-none-any.whl", hash = "sha256:4bb86b28627b7e41098f0e93280fe4892a1abed1b79a19aec6f928f39b17dffb"},
- {file = "opentelemetry_api-1.21.0.tar.gz", hash = "sha256:d6185fd5043e000075d921822fd2d26b953eba8ca21b1e2fa360dd46a7686316"},
+ {file = "opentelemetry_api-1.38.0-py3-none-any.whl", hash = "sha256:2891b0197f47124454ab9f0cf58f3be33faca394457ac3e09daba13ff50aa582"},
+ {file = "opentelemetry_api-1.38.0.tar.gz", hash = "sha256:f4c193b5e8acb0912b06ac5b16321908dd0843d75049c091487322284a3eea12"},
]
[package.dependencies]
-deprecated = ">=1.2.6"
-importlib-metadata = ">=6.0,<7.0"
+importlib-metadata = ">=6.0,<8.8.0"
+typing-extensions = ">=4.5.0"
[[package]]
name = "opentelemetry-exporter-otlp-proto-common"
-version = "1.21.0"
+version = "1.38.0"
description = "OpenTelemetry Protobuf encoding"
optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.9"
+groups = ["main"]
files = [
- {file = "opentelemetry_exporter_otlp_proto_common-1.21.0-py3-none-any.whl", hash = "sha256:97b1022b38270ec65d11fbfa348e0cd49d12006485c2321ea3b1b7037d42b6ec"},
- {file = "opentelemetry_exporter_otlp_proto_common-1.21.0.tar.gz", hash = "sha256:61db274d8a68d636fb2ec2a0f281922949361cdd8236e25ff5539edf942b3226"},
+ {file = "opentelemetry_exporter_otlp_proto_common-1.38.0-py3-none-any.whl", hash = "sha256:03cb76ab213300fe4f4c62b7d8f17d97fcfd21b89f0b5ce38ea156327ddda74a"},
+ {file = "opentelemetry_exporter_otlp_proto_common-1.38.0.tar.gz", hash = "sha256:e333278afab4695aa8114eeb7bf4e44e65c6607d54968271a249c180b2cb605c"},
]
[package.dependencies]
-backoff = {version = ">=1.10.0,<3.0.0", markers = "python_version >= \"3.7\""}
-opentelemetry-proto = "1.21.0"
+opentelemetry-proto = "1.38.0"
[[package]]
name = "opentelemetry-exporter-otlp-proto-http"
-version = "1.21.0"
+version = "1.38.0"
description = "OpenTelemetry Collector Protobuf over HTTP Exporter"
optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.9"
+groups = ["main"]
files = [
- {file = "opentelemetry_exporter_otlp_proto_http-1.21.0-py3-none-any.whl", hash = "sha256:56837773de6fb2714c01fc4895caebe876f6397bbc4d16afddf89e1299a55ee2"},
- {file = "opentelemetry_exporter_otlp_proto_http-1.21.0.tar.gz", hash = "sha256:19d60afa4ae8597f7ef61ad75c8b6c6b7ef8cb73a33fb4aed4dbc86d5c8d3301"},
+ {file = "opentelemetry_exporter_otlp_proto_http-1.38.0-py3-none-any.whl", hash = "sha256:84b937305edfc563f08ec69b9cb2298be8188371217e867c1854d77198d0825b"},
+ {file = "opentelemetry_exporter_otlp_proto_http-1.38.0.tar.gz", hash = "sha256:f16bd44baf15cbe07633c5112ffc68229d0edbeac7b37610be0b2def4e21e90b"},
]
[package.dependencies]
-backoff = {version = ">=1.10.0,<3.0.0", markers = "python_version >= \"3.7\""}
-deprecated = ">=1.2.6"
googleapis-common-protos = ">=1.52,<2.0"
opentelemetry-api = ">=1.15,<2.0"
-opentelemetry-exporter-otlp-proto-common = "1.21.0"
-opentelemetry-proto = "1.21.0"
-opentelemetry-sdk = ">=1.21.0,<1.22.0"
+opentelemetry-exporter-otlp-proto-common = "1.38.0"
+opentelemetry-proto = "1.38.0"
+opentelemetry-sdk = ">=1.38.0,<1.39.0"
requests = ">=2.7,<3.0"
+typing-extensions = ">=4.5.0"
+
+[[package]]
+name = "opentelemetry-instrumentation"
+version = "0.59b0"
+description = "Instrumentation Tools & Auto Instrumentation for OpenTelemetry Python"
+optional = false
+python-versions = ">=3.9"
+groups = ["main"]
+files = [
+ {file = "opentelemetry_instrumentation-0.59b0-py3-none-any.whl", hash = "sha256:44082cc8fe56b0186e87ee8f7c17c327c4c2ce93bdbe86496e600985d74368ee"},
+ {file = "opentelemetry_instrumentation-0.59b0.tar.gz", hash = "sha256:6010f0faaacdaf7c4dff8aac84e226d23437b331dcda7e70367f6d73a7db1adc"},
+]
+
+[package.dependencies]
+opentelemetry-api = ">=1.4,<2.0"
+opentelemetry-semantic-conventions = "0.59b0"
+packaging = ">=18.0"
+wrapt = ">=1.0.0,<2.0.0"
+
+[[package]]
+name = "opentelemetry-instrumentation-httpx"
+version = "0.59b0"
+description = "OpenTelemetry HTTPX Instrumentation"
+optional = false
+python-versions = ">=3.9"
+groups = ["main"]
+files = [
+ {file = "opentelemetry_instrumentation_httpx-0.59b0-py3-none-any.whl", hash = "sha256:7dc9f66aef4ca3904d877f459a70c78eafd06131dc64d713b9b1b5a7d0a48f05"},
+ {file = "opentelemetry_instrumentation_httpx-0.59b0.tar.gz", hash = "sha256:a1cb9b89d9f05a82701cc9ab9cfa3db54fd76932489449778b350bc1b9f0e872"},
+]
+
+[package.dependencies]
+opentelemetry-api = ">=1.12,<2.0"
+opentelemetry-instrumentation = "0.59b0"
+opentelemetry-semantic-conventions = "0.59b0"
+opentelemetry-util-http = "0.59b0"
+wrapt = ">=1.0.0,<2.0.0"
[package.extras]
-test = ["responses (==0.22.0)"]
+instruments = ["httpx (>=0.18.0)"]
+
+[[package]]
+name = "opentelemetry-instrumentation-requests"
+version = "0.59b0"
+description = "OpenTelemetry requests instrumentation"
+optional = false
+python-versions = ">=3.9"
+groups = ["main"]
+files = [
+ {file = "opentelemetry_instrumentation_requests-0.59b0-py3-none-any.whl", hash = "sha256:d43121532877e31a46c48649279cec2504ee1e0ceb3c87b80fe5ccd7eafc14c1"},
+ {file = "opentelemetry_instrumentation_requests-0.59b0.tar.gz", hash = "sha256:9af2ffe3317f03074d7f865919139e89170b6763a0251b68c25e8e64e04b3400"},
+]
+
+[package.dependencies]
+opentelemetry-api = ">=1.12,<2.0"
+opentelemetry-instrumentation = "0.59b0"
+opentelemetry-semantic-conventions = "0.59b0"
+opentelemetry-util-http = "0.59b0"
+
+[package.extras]
+instruments = ["requests (>=2.0,<3.0)"]
+
+[[package]]
+name = "opentelemetry-instrumentation-threading"
+version = "0.59b0"
+description = "Thread context propagation support for OpenTelemetry"
+optional = false
+python-versions = ">=3.9"
+groups = ["main"]
+files = [
+ {file = "opentelemetry_instrumentation_threading-0.59b0-py3-none-any.whl", hash = "sha256:76da2fc01fe1dccebff6581080cff9e42ac7b27cc61eb563f3c4435c727e8eca"},
+ {file = "opentelemetry_instrumentation_threading-0.59b0.tar.gz", hash = "sha256:ce5658730b697dcbc0e0d6d13643a69fd8aeb1b32fa8db3bade8ce114c7975f3"},
+]
+
+[package.dependencies]
+opentelemetry-api = ">=1.12,<2.0"
+opentelemetry-instrumentation = "0.59b0"
+wrapt = ">=1.0.0,<2.0.0"
+
+[[package]]
+name = "opentelemetry-instrumentation-urllib"
+version = "0.59b0"
+description = "OpenTelemetry urllib instrumentation"
+optional = false
+python-versions = ">=3.9"
+groups = ["main"]
+files = [
+ {file = "opentelemetry_instrumentation_urllib-0.59b0-py3-none-any.whl", hash = "sha256:ed2bd1a02e4334c13c13033681ff8cf10d5dfcd5b0e6d7514f94a00e7f7bd671"},
+ {file = "opentelemetry_instrumentation_urllib-0.59b0.tar.gz", hash = "sha256:1e2bb3427ce13854453777d8dccf3b0144640b03846f00fc302bdb6e1f2f8c7a"},
+]
+
+[package.dependencies]
+opentelemetry-api = ">=1.12,<2.0"
+opentelemetry-instrumentation = "0.59b0"
+opentelemetry-semantic-conventions = "0.59b0"
+opentelemetry-util-http = "0.59b0"
[[package]]
name = "opentelemetry-proto"
-version = "1.21.0"
+version = "1.38.0"
description = "OpenTelemetry Python Proto"
optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.9"
+groups = ["main"]
files = [
- {file = "opentelemetry_proto-1.21.0-py3-none-any.whl", hash = "sha256:32fc4248e83eebd80994e13963e683f25f3b443226336bb12b5b6d53638f50ba"},
- {file = "opentelemetry_proto-1.21.0.tar.gz", hash = "sha256:7d5172c29ed1b525b5ecf4ebe758c7138a9224441b3cfe683d0a237c33b1941f"},
+ {file = "opentelemetry_proto-1.38.0-py3-none-any.whl", hash = "sha256:b6ebe54d3217c42e45462e2a1ae28c3e2bf2ec5a5645236a490f55f45f1a0a18"},
+ {file = "opentelemetry_proto-1.38.0.tar.gz", hash = "sha256:88b161e89d9d372ce723da289b7da74c3a8354a8e5359992be813942969ed468"},
]
[package.dependencies]
-protobuf = ">=3.19,<5.0"
+protobuf = ">=5.0,<7.0"
[[package]]
name = "opentelemetry-sdk"
-version = "1.21.0"
+version = "1.38.0"
description = "OpenTelemetry Python SDK"
optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.9"
+groups = ["main"]
files = [
- {file = "opentelemetry_sdk-1.21.0-py3-none-any.whl", hash = "sha256:9fe633243a8c655fedace3a0b89ccdfc654c0290ea2d8e839bd5db3131186f73"},
- {file = "opentelemetry_sdk-1.21.0.tar.gz", hash = "sha256:3ec8cd3020328d6bc5c9991ccaf9ae820ccb6395a5648d9a95d3ec88275b8879"},
+ {file = "opentelemetry_sdk-1.38.0-py3-none-any.whl", hash = "sha256:1c66af6564ecc1553d72d811a01df063ff097cdc82ce188da9951f93b8d10f6b"},
+ {file = "opentelemetry_sdk-1.38.0.tar.gz", hash = "sha256:93df5d4d871ed09cb4272305be4d996236eedb232253e3ab864c8620f051cebe"},
]
[package.dependencies]
-opentelemetry-api = "1.21.0"
-opentelemetry-semantic-conventions = "0.42b0"
-typing-extensions = ">=3.7.4"
+opentelemetry-api = "1.38.0"
+opentelemetry-semantic-conventions = "0.59b0"
+typing-extensions = ">=4.5.0"
[[package]]
name = "opentelemetry-semantic-conventions"
-version = "0.42b0"
+version = "0.59b0"
description = "OpenTelemetry Semantic Conventions"
optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.9"
+groups = ["main"]
files = [
- {file = "opentelemetry_semantic_conventions-0.42b0-py3-none-any.whl", hash = "sha256:5cd719cbfec448af658860796c5d0fcea2fdf0945a2bed2363f42cb1ee39f526"},
- {file = "opentelemetry_semantic_conventions-0.42b0.tar.gz", hash = "sha256:44ae67a0a3252a05072877857e5cc1242c98d4cf12870159f1a94bec800d38ec"},
+ {file = "opentelemetry_semantic_conventions-0.59b0-py3-none-any.whl", hash = "sha256:35d3b8833ef97d614136e253c1da9342b4c3c083bbaf29ce31d572a1c3825eed"},
+ {file = "opentelemetry_semantic_conventions-0.59b0.tar.gz", hash = "sha256:7a6db3f30d70202d5bf9fa4b69bc866ca6a30437287de6c510fb594878aed6b0"},
+]
+
+[package.dependencies]
+opentelemetry-api = "1.38.0"
+typing-extensions = ">=4.5.0"
+
+[[package]]
+name = "opentelemetry-util-http"
+version = "0.59b0"
+description = "Web util for OpenTelemetry"
+optional = false
+python-versions = ">=3.9"
+groups = ["main"]
+files = [
+ {file = "opentelemetry_util_http-0.59b0-py3-none-any.whl", hash = "sha256:6d036a07563bce87bf521839c0671b507a02a0d39d7ea61b88efa14c6e25355d"},
+ {file = "opentelemetry_util_http-0.59b0.tar.gz", hash = "sha256:ae66ee91be31938d832f3b4bc4eb8a911f6eddd38969c4a871b1230db2a0a560"},
]
[[package]]
@@ -820,6 +1076,7 @@ version = "24.0"
description = "Core utilities for Python packages"
optional = false
python-versions = ">=3.7"
+groups = ["main", "docs", "test"]
files = [
{file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"},
{file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"},
@@ -831,16 +1088,118 @@ version = "0.5.6"
description = "Divides large result sets into pages for easier browsing"
optional = false
python-versions = "*"
+groups = ["docs"]
files = [
{file = "paginate-0.5.6.tar.gz", hash = "sha256:5e6007b6a9398177a7e1648d04fdd9f8c9766a1a945bceac82f1929e8c78af2d"},
]
+[[package]]
+name = "pandas"
+version = "2.3.3"
+description = "Powerful data structures for data analysis, time series, and statistics"
+optional = false
+python-versions = ">=3.9"
+groups = ["main"]
+files = [
+ {file = "pandas-2.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:376c6446ae31770764215a6c937f72d917f214b43560603cd60da6408f183b6c"},
+ {file = "pandas-2.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e19d192383eab2f4ceb30b412b22ea30690c9e618f78870357ae1d682912015a"},
+ {file = "pandas-2.3.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5caf26f64126b6c7aec964f74266f435afef1c1b13da3b0636c7518a1fa3e2b1"},
+ {file = "pandas-2.3.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd7478f1463441ae4ca7308a70e90b33470fa593429f9d4c578dd00d1fa78838"},
+ {file = "pandas-2.3.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4793891684806ae50d1288c9bae9330293ab4e083ccd1c5e383c34549c6e4250"},
+ {file = "pandas-2.3.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:28083c648d9a99a5dd035ec125d42439c6c1c525098c58af0fc38dd1a7a1b3d4"},
+ {file = "pandas-2.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:503cf027cf9940d2ceaa1a93cfb5f8c8c7e6e90720a2850378f0b3f3b1e06826"},
+ {file = "pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523"},
+ {file = "pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45"},
+ {file = "pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66"},
+ {file = "pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b"},
+ {file = "pandas-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791"},
+ {file = "pandas-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151"},
+ {file = "pandas-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c"},
+ {file = "pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53"},
+ {file = "pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35"},
+ {file = "pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908"},
+ {file = "pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89"},
+ {file = "pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98"},
+ {file = "pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084"},
+ {file = "pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b"},
+ {file = "pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713"},
+ {file = "pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8"},
+ {file = "pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d"},
+ {file = "pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac"},
+ {file = "pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c"},
+ {file = "pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493"},
+ {file = "pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee"},
+ {file = "pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5"},
+ {file = "pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21"},
+ {file = "pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78"},
+ {file = "pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110"},
+ {file = "pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86"},
+ {file = "pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc"},
+ {file = "pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0"},
+ {file = "pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593"},
+ {file = "pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c"},
+ {file = "pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b"},
+ {file = "pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6"},
+ {file = "pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3"},
+ {file = "pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5"},
+ {file = "pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec"},
+ {file = "pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7"},
+ {file = "pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450"},
+ {file = "pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5"},
+ {file = "pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788"},
+ {file = "pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87"},
+ {file = "pandas-2.3.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c503ba5216814e295f40711470446bc3fd00f0faea8a086cbc688808e26f92a2"},
+ {file = "pandas-2.3.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a637c5cdfa04b6d6e2ecedcb81fc52ffb0fd78ce2ebccc9ea964df9f658de8c8"},
+ {file = "pandas-2.3.3-cp39-cp39-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:854d00d556406bffe66a4c0802f334c9ad5a96b4f1f868adf036a21b11ef13ff"},
+ {file = "pandas-2.3.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf1f8a81d04ca90e32a0aceb819d34dbd378a98bf923b6398b9a3ec0bf44de29"},
+ {file = "pandas-2.3.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:23ebd657a4d38268c7dfbdf089fbc31ea709d82e4923c5ffd4fbd5747133ce73"},
+ {file = "pandas-2.3.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5554c929ccc317d41a5e3d1234f3be588248e61f08a74dd17c9eabb535777dc9"},
+ {file = "pandas-2.3.3-cp39-cp39-win_amd64.whl", hash = "sha256:d3e28b3e83862ccf4d85ff19cf8c20b2ae7e503881711ff2d534dc8f761131aa"},
+ {file = "pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b"},
+]
+
+[package.dependencies]
+numpy = [
+ {version = ">=1.22.4", markers = "python_version < \"3.11\""},
+ {version = ">=1.23.2", markers = "python_version == \"3.11\""},
+ {version = ">=1.26.0", markers = "python_version >= \"3.12\""},
+]
+python-dateutil = ">=2.8.2"
+pytz = ">=2020.1"
+tzdata = ">=2022.7"
+
+[package.extras]
+all = ["PyQt5 (>=5.15.9)", "SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)", "beautifulsoup4 (>=4.11.2)", "bottleneck (>=1.3.6)", "dataframe-api-compat (>=0.1.7)", "fastparquet (>=2022.12.0)", "fsspec (>=2022.11.0)", "gcsfs (>=2022.11.0)", "html5lib (>=1.1)", "hypothesis (>=6.46.1)", "jinja2 (>=3.1.2)", "lxml (>=4.9.2)", "matplotlib (>=3.6.3)", "numba (>=0.56.4)", "numexpr (>=2.8.4)", "odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "pandas-gbq (>=0.19.0)", "psycopg2 (>=2.9.6)", "pyarrow (>=10.0.1)", "pymysql (>=1.0.2)", "pyreadstat (>=1.2.0)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "qtpy (>=2.3.0)", "s3fs (>=2022.11.0)", "scipy (>=1.10.0)", "tables (>=3.8.0)", "tabulate (>=0.9.0)", "xarray (>=2022.12.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)", "zstandard (>=0.19.0)"]
+aws = ["s3fs (>=2022.11.0)"]
+clipboard = ["PyQt5 (>=5.15.9)", "qtpy (>=2.3.0)"]
+compression = ["zstandard (>=0.19.0)"]
+computation = ["scipy (>=1.10.0)", "xarray (>=2022.12.0)"]
+consortium-standard = ["dataframe-api-compat (>=0.1.7)"]
+excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)"]
+feather = ["pyarrow (>=10.0.1)"]
+fss = ["fsspec (>=2022.11.0)"]
+gcp = ["gcsfs (>=2022.11.0)", "pandas-gbq (>=0.19.0)"]
+hdf5 = ["tables (>=3.8.0)"]
+html = ["beautifulsoup4 (>=4.11.2)", "html5lib (>=1.1)", "lxml (>=4.9.2)"]
+mysql = ["SQLAlchemy (>=2.0.0)", "pymysql (>=1.0.2)"]
+output-formatting = ["jinja2 (>=3.1.2)", "tabulate (>=0.9.0)"]
+parquet = ["pyarrow (>=10.0.1)"]
+performance = ["bottleneck (>=1.3.6)", "numba (>=0.56.4)", "numexpr (>=2.8.4)"]
+plot = ["matplotlib (>=3.6.3)"]
+postgresql = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "psycopg2 (>=2.9.6)"]
+pyarrow = ["pyarrow (>=10.0.1)"]
+spss = ["pyreadstat (>=1.2.0)"]
+sql-other = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)"]
+test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"]
+xml = ["lxml (>=4.9.2)"]
+
[[package]]
name = "pathspec"
version = "0.12.1"
description = "Utility library for gitignore style pattern matching of file paths."
optional = false
python-versions = ">=3.8"
+groups = ["docs"]
files = [
{file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"},
{file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
@@ -852,6 +1211,7 @@ version = "4.2.2"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
optional = false
python-versions = ">=3.8"
+groups = ["docs"]
files = [
{file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"},
{file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"},
@@ -868,6 +1228,7 @@ version = "1.5.0"
description = "plugin and hook calling mechanisms for python"
optional = false
python-versions = ">=3.8"
+groups = ["test"]
files = [
{file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
{file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
@@ -879,22 +1240,22 @@ testing = ["pytest", "pytest-benchmark"]
[[package]]
name = "protobuf"
-version = "4.25.3"
+version = "6.33.1"
description = ""
optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.9"
+groups = ["main"]
files = [
- {file = "protobuf-4.25.3-cp310-abi3-win32.whl", hash = "sha256:d4198877797a83cbfe9bffa3803602bbe1625dc30d8a097365dbc762e5790faa"},
- {file = "protobuf-4.25.3-cp310-abi3-win_amd64.whl", hash = "sha256:209ba4cc916bab46f64e56b85b090607a676f66b473e6b762e6f1d9d591eb2e8"},
- {file = "protobuf-4.25.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:f1279ab38ecbfae7e456a108c5c0681e4956d5b1090027c1de0f934dfdb4b35c"},
- {file = "protobuf-4.25.3-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:e7cb0ae90dd83727f0c0718634ed56837bfeeee29a5f82a7514c03ee1364c019"},
- {file = "protobuf-4.25.3-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:7c8daa26095f82482307bc717364e7c13f4f1c99659be82890dcfc215194554d"},
- {file = "protobuf-4.25.3-cp38-cp38-win32.whl", hash = "sha256:f4f118245c4a087776e0a8408be33cf09f6c547442c00395fbfb116fac2f8ac2"},
- {file = "protobuf-4.25.3-cp38-cp38-win_amd64.whl", hash = "sha256:c053062984e61144385022e53678fbded7aea14ebb3e0305ae3592fb219ccfa4"},
- {file = "protobuf-4.25.3-cp39-cp39-win32.whl", hash = "sha256:19b270aeaa0099f16d3ca02628546b8baefe2955bbe23224aaf856134eccf1e4"},
- {file = "protobuf-4.25.3-cp39-cp39-win_amd64.whl", hash = "sha256:e3c97a1555fd6388f857770ff8b9703083de6bf1f9274a002a332d65fbb56c8c"},
- {file = "protobuf-4.25.3-py3-none-any.whl", hash = "sha256:f0700d54bcf45424477e46a9f0944155b46fb0639d69728739c0e47bab83f2b9"},
- {file = "protobuf-4.25.3.tar.gz", hash = "sha256:25b5d0b42fd000320bd7830b349e3b696435f3b329810427a6bcce6a5492cc5c"},
+ {file = "protobuf-6.33.1-cp310-abi3-win32.whl", hash = "sha256:f8d3fdbc966aaab1d05046d0240dd94d40f2a8c62856d41eaa141ff64a79de6b"},
+ {file = "protobuf-6.33.1-cp310-abi3-win_amd64.whl", hash = "sha256:923aa6d27a92bf44394f6abf7ea0500f38769d4b07f4be41cb52bd8b1123b9ed"},
+ {file = "protobuf-6.33.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:fe34575f2bdde76ac429ec7b570235bf0c788883e70aee90068e9981806f2490"},
+ {file = "protobuf-6.33.1-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:f8adba2e44cde2d7618996b3fc02341f03f5bc3f2748be72dc7b063319276178"},
+ {file = "protobuf-6.33.1-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:0f4cf01222c0d959c2b399142deb526de420be8236f22c71356e2a544e153c53"},
+ {file = "protobuf-6.33.1-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:8fd7d5e0eb08cd5b87fd3df49bc193f5cfd778701f47e11d127d0afc6c39f1d1"},
+ {file = "protobuf-6.33.1-cp39-cp39-win32.whl", hash = "sha256:023af8449482fa884d88b4563d85e83accab54138ae098924a985bcbb734a213"},
+ {file = "protobuf-6.33.1-cp39-cp39-win_amd64.whl", hash = "sha256:df051de4fd7e5e4371334e234c62ba43763f15ab605579e04c7008c05735cd82"},
+ {file = "protobuf-6.33.1-py3-none-any.whl", hash = "sha256:d595a9fd694fdeb061a62fbe10eb039cc1e444df81ec9bb70c7fc59ebcb1eafa"},
+ {file = "protobuf-6.33.1.tar.gz", hash = "sha256:97f65757e8d09870de6fd973aeddb92f85435607235d20b2dfed93405d00c85b"},
]
[[package]]
@@ -903,6 +1264,7 @@ version = "5.9.8"
description = "Cross-platform lib for process and system monitoring in Python."
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
+groups = ["main"]
files = [
{file = "psutil-5.9.8-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:26bd09967ae00920df88e0352a91cff1a78f8d69b3ecabbfe733610c0af486c8"},
{file = "psutil-5.9.8-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:05806de88103b25903dff19bb6692bd2e714ccf9e668d050d144012055cbca73"},
@@ -923,7 +1285,7 @@ files = [
]
[package.extras]
-test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"]
+test = ["enum34 ; python_version <= \"3.4\"", "ipaddress ; python_version < \"3.0\"", "mock ; python_version < \"3.0\"", "pywin32 ; sys_platform == \"win32\"", "wmi ; sys_platform == \"win32\""]
[[package]]
name = "pygments"
@@ -931,6 +1293,7 @@ version = "2.18.0"
description = "Pygments is a syntax highlighting package written in Python."
optional = false
python-versions = ">=3.8"
+groups = ["main", "docs"]
files = [
{file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"},
{file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"},
@@ -945,6 +1308,7 @@ version = "10.8.1"
description = "Extension pack for Python Markdown."
optional = false
python-versions = ">=3.8"
+groups = ["docs"]
files = [
{file = "pymdown_extensions-10.8.1-py3-none-any.whl", hash = "sha256:f938326115884f48c6059c67377c46cf631c733ef3629b6eed1349989d1b30cb"},
{file = "pymdown_extensions-10.8.1.tar.gz", hash = "sha256:3ab1db5c9e21728dabf75192d71471f8e50f216627e9a1fa9535ecb0231b9940"},
@@ -963,6 +1327,7 @@ version = "7.4.4"
description = "pytest: simple powerful testing with Python"
optional = false
python-versions = ">=3.7"
+groups = ["test"]
files = [
{file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"},
{file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"},
@@ -985,6 +1350,7 @@ version = "2.9.0.post0"
description = "Extensions to the standard Python datetime module"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
+groups = ["main", "docs"]
files = [
{file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"},
{file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"},
@@ -993,12 +1359,25 @@ files = [
[package.dependencies]
six = ">=1.5"
+[[package]]
+name = "pytz"
+version = "2025.2"
+description = "World timezone definitions, modern and historical"
+optional = false
+python-versions = "*"
+groups = ["main"]
+files = [
+ {file = "pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00"},
+ {file = "pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3"},
+]
+
[[package]]
name = "pyyaml"
version = "6.0.1"
description = "YAML parser and emitter for Python"
optional = false
python-versions = ">=3.6"
+groups = ["docs"]
files = [
{file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"},
{file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"},
@@ -1018,6 +1397,7 @@ files = [
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
{file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
{file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
+ {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"},
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
{file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
{file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
@@ -1058,6 +1438,7 @@ version = "0.1"
description = "A custom YAML tag for referencing environment variables in YAML files. "
optional = false
python-versions = ">=3.6"
+groups = ["docs"]
files = [
{file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"},
{file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"},
@@ -1072,6 +1453,7 @@ version = "2024.5.15"
description = "Alternative regular expression module, to replace re."
optional = false
python-versions = ">=3.8"
+groups = ["docs"]
files = [
{file = "regex-2024.5.15-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a81e3cfbae20378d75185171587cbf756015ccb14840702944f014e0d93ea09f"},
{file = "regex-2024.5.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7b59138b219ffa8979013be7bc85bb60c6f7b7575df3d56dc1e403a438c7a3f6"},
@@ -1160,6 +1542,7 @@ version = "2.31.0"
description = "Python HTTP for Humans."
optional = false
python-versions = ">=3.7"
+groups = ["main", "docs"]
files = [
{file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"},
{file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"},
@@ -1181,6 +1564,7 @@ version = "13.7.1"
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
optional = false
python-versions = ">=3.7.0"
+groups = ["main"]
files = [
{file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"},
{file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"},
@@ -1199,6 +1583,7 @@ version = "1.5.4"
description = "Tool to Detect Surrounding Shell"
optional = false
python-versions = ">=3.7"
+groups = ["main"]
files = [
{file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"},
{file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"},
@@ -1210,6 +1595,7 @@ version = "1.16.0"
description = "Python 2 and 3 compatibility utilities"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
+groups = ["main", "docs"]
files = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
@@ -1221,6 +1607,7 @@ version = "1.3.1"
description = "Sniff out which async library your code is running under"
optional = false
python-versions = ">=3.7"
+groups = ["main"]
files = [
{file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"},
{file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"},
@@ -1228,25 +1615,30 @@ files = [
[[package]]
name = "synapseclient"
-version = "4.2.0"
+version = "4.10.0"
description = "A client for Synapse, a collaborative, open-source research platform that allows teams to share data, track analyses, and collaborate."
optional = false
-python-versions = ">=3.8"
+python-versions = "<3.14,>=3.9"
+groups = ["main"]
files = [
- {file = "synapseclient-4.2.0-py3-none-any.whl", hash = "sha256:ab5bc9c2bf5b90f271f1a9478eff7e9fca3e573578401ac706383ddb984d7a13"},
- {file = "synapseclient-4.2.0.tar.gz", hash = "sha256:89222661125de1795b1a096cf8c58b8115c19d6b0fa5846ed2a41cdb394ef773"},
+ {file = "synapseclient-4.10.0-py3-none-any.whl", hash = "sha256:ce1e588258132d923dcbb84cc35491b5952e182e4c40bd90f97b8734c2603c25"},
+ {file = "synapseclient-4.10.0.tar.gz", hash = "sha256:667db3cad5afd541604b0a4b53fdad6a6cab15ccc00956c365356b59777aba00"},
]
[package.dependencies]
async-lru = ">=2.0.4,<2.1.0"
asyncio-atexit = ">=1.0.1,<1.1.0"
deprecated = ">=1.2.4,<2.0"
-httpx = ">=0.27.0,<0.28.0"
-loky = ">=3.0.0,<3.1.0"
+httpcore = ">=1.0.9"
+httpx = ">=0.27.0"
nest-asyncio = ">=1.6.0,<1.7.0"
-opentelemetry-api = ">=1.21.0,<1.22.0"
-opentelemetry-exporter-otlp-proto-http = ">=1.21.0,<1.22.0"
-opentelemetry-sdk = ">=1.21.0,<1.22.0"
+opentelemetry-api = ">=1.21.0"
+opentelemetry-exporter-otlp-proto-http = ">=1.21.0"
+opentelemetry-instrumentation-httpx = ">=0.48b0"
+opentelemetry-instrumentation-requests = ">=0.48b0"
+opentelemetry-instrumentation-threading = ">=0.48b0"
+opentelemetry-instrumentation-urllib = ">=0.48b0"
+opentelemetry-sdk = ">=1.21.0"
psutil = ">=5.9.8,<5.10.0"
requests = ">=2.22.0,<3.0"
tqdm = ">=4.66.2,<5.0"
@@ -1254,11 +1646,11 @@ urllib3 = ">=1.26.18,<2"
[package.extras]
boto3 = ["boto3 (>=1.7.0,<2.0)"]
-dev = ["black", "flake8 (>=3.7.0,<4.0)", "func-timeout (>=4.3,<5.0)", "pre-commit", "pytest (>=6.0.0,<7.0)", "pytest-asyncio (>=0.19,<1.0)", "pytest-cov (>=4.1.0,<4.2.0)", "pytest-mock (>=3.0,<4.0)", "pytest-rerunfailures (>=12.0,<13.0)", "pytest-socket (>=0.6.0,<0.7.0)", "pytest-xdist[psutil] (>=2.2,<3.0.0)"]
-docs = ["markdown-include (>=0.8.1,<0.9.0)", "mkdocs (>=1.5.3)", "mkdocs-material (>=9.4.14)", "mkdocs-open-in-new-tab (>=1.0.3,<1.1.0)", "mkdocstrings (>=0.24.0)", "mkdocstrings-python (>=1.7.5)", "termynal (>=0.11.1)"]
+dev = ["black", "flake8 (>=3.7.0,<4.0)", "func-timeout (>=4.3,<5.0)", "pandas (>=1.5,<3.0)", "pre-commit", "pytest (>=8.2.0,<8.3.0)", "pytest-asyncio (>=1.2.0,<2.0)", "pytest-cov (>=4.1.0,<4.2.0)", "pytest-mock (>=3.0,<4.0)", "pytest-rerunfailures (>=12.0,<13.0)", "pytest-socket (>=0.6.0,<0.7.0)", "pytest-xdist[psutil] (>=2.2,<3.0.0)"]
+docs = ["markdown-include (>=0.8.1,<0.9.0)", "mkdocs (>=1.5.3)", "mkdocs-material (>=9.4.14)", "mkdocs-open-in-new-tab (>=1.0.3,<1.1.0)", "mkdocstrings (>=0.24.0)", "mkdocstrings-python (>=1.8.0)", "termynal (>=0.11.1)"]
pandas = ["pandas (>=1.5,<3.0)"]
-pysftp = ["pysftp (>=0.2.8,<0.3)"]
-tests = ["flake8 (>=3.7.0,<4.0)", "func-timeout (>=4.3,<5.0)", "pytest (>=6.0.0,<7.0)", "pytest-asyncio (>=0.19,<1.0)", "pytest-cov (>=4.1.0,<4.2.0)", "pytest-mock (>=3.0,<4.0)", "pytest-rerunfailures (>=12.0,<13.0)", "pytest-socket (>=0.6.0,<0.7.0)", "pytest-xdist[psutil] (>=2.2,<3.0.0)"]
+pysftp = ["paramiko (<4.0.0)", "pysftp (>=0.2.8,<0.3)"]
+tests = ["flake8 (>=3.7.0,<4.0)", "func-timeout (>=4.3,<5.0)", "pandas (>=1.5,<3.0)", "pytest (>=8.2.0,<8.3.0)", "pytest-asyncio (>=1.2.0,<2.0)", "pytest-cov (>=4.1.0,<4.2.0)", "pytest-mock (>=3.0,<4.0)", "pytest-rerunfailures (>=12.0,<13.0)", "pytest-socket (>=0.6.0,<0.7.0)", "pytest-xdist[psutil] (>=2.2,<3.0.0)"]
[[package]]
name = "termynal"
@@ -1266,6 +1658,7 @@ version = "0.11.1"
description = "A lightweight and modern animated terminal window"
optional = false
python-versions = ">=3.8.1,<4.0.0"
+groups = ["docs"]
files = [
{file = "termynal-0.11.1-py3-none-any.whl", hash = "sha256:553fc58b126a0482b71a442263d33a9b47fb69868a7fdd6cc36dc9f006aab7a2"},
{file = "termynal-0.11.1.tar.gz", hash = "sha256:ce70d264a649d26365bf72b8cfc5aa2bf903adafebc1027347343779a8695ff0"},
@@ -1283,6 +1676,8 @@ version = "2.0.1"
description = "A lil' TOML parser"
optional = false
python-versions = ">=3.7"
+groups = ["test"]
+markers = "python_version == \"3.10\""
files = [
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
@@ -1294,6 +1689,7 @@ version = "4.66.4"
description = "Fast, Extensible Progress Meter"
optional = false
python-versions = ">=3.7"
+groups = ["main"]
files = [
{file = "tqdm-4.66.4-py3-none-any.whl", hash = "sha256:b75ca56b413b030bc3f00af51fd2c1a1a5eac6a0c1cca83cbb37a5c52abce644"},
{file = "tqdm-4.66.4.tar.gz", hash = "sha256:e4d936c9de8727928f3be6079590e97d9abfe8d39a590be678eb5919ffc186bb"},
@@ -1314,6 +1710,7 @@ version = "0.9.4"
description = "Typer, build great CLIs. Easy to code. Based on Python type hints."
optional = false
python-versions = ">=3.6"
+groups = ["main"]
files = [
{file = "typer-0.9.4-py3-none-any.whl", hash = "sha256:aa6c4a4e2329d868b80ecbaf16f807f2b54e192209d7ac9dd42691d63f7a54eb"},
{file = "typer-0.9.4.tar.gz", hash = "sha256:f714c2d90afae3a7929fcd72a3abb08df305e1ff61719381384211c4070af57f"},
@@ -1338,25 +1735,39 @@ version = "4.11.0"
description = "Backported and Experimental Type Hints for Python 3.8+"
optional = false
python-versions = ">=3.8"
+groups = ["main"]
files = [
{file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"},
{file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"},
]
+[[package]]
+name = "tzdata"
+version = "2025.2"
+description = "Provider of IANA time zone data"
+optional = false
+python-versions = ">=2"
+groups = ["main"]
+files = [
+ {file = "tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8"},
+ {file = "tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9"},
+]
+
[[package]]
name = "urllib3"
version = "1.26.18"
description = "HTTP library with thread-safe connection pooling, file post, and more."
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
+groups = ["main", "docs"]
files = [
{file = "urllib3-1.26.18-py2.py3-none-any.whl", hash = "sha256:34b97092d7e0a3a8cf7cd10e386f401b3737364026c45e622aa02903dffe0f07"},
{file = "urllib3-1.26.18.tar.gz", hash = "sha256:f8ecc1bba5667413457c529ab955bf8c67b45db799d159066261719e328580a0"},
]
[package.extras]
-brotli = ["brotli (==1.0.9)", "brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"]
-secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"]
+brotli = ["brotli (==1.0.9) ; os_name != \"nt\" and python_version < \"3\" and platform_python_implementation == \"CPython\"", "brotli (>=1.0.9) ; python_version >= \"3\" and platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; (os_name != \"nt\" or python_version >= \"3\") and platform_python_implementation != \"CPython\"", "brotlipy (>=0.6.0) ; os_name == \"nt\" and python_version < \"3\""]
+secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress ; python_version == \"2.7\"", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"]
socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
[[package]]
@@ -1365,6 +1776,7 @@ version = "4.0.0"
description = "Filesystem events monitoring"
optional = false
python-versions = ">=3.8"
+groups = ["docs"]
files = [
{file = "watchdog-4.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:39cb34b1f1afbf23e9562501673e7146777efe95da24fab5707b88f7fb11649b"},
{file = "watchdog-4.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c522392acc5e962bcac3b22b9592493ffd06d1fc5d755954e6be9f4990de932b"},
@@ -1406,6 +1818,7 @@ version = "1.16.0"
description = "Module for decorators, wrappers and monkey patching."
optional = false
python-versions = ">=3.6"
+groups = ["main"]
files = [
{file = "wrapt-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4"},
{file = "wrapt-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020"},
@@ -1485,6 +1898,7 @@ version = "3.18.2"
description = "Backport of pathlib-compatible object wrapper for zip files"
optional = false
python-versions = ">=3.8"
+groups = ["main"]
files = [
{file = "zipp-3.18.2-py3-none-any.whl", hash = "sha256:dce197b859eb796242b0622af1b8beb0a722d52aa2f57133ead08edd5bf5374e"},
{file = "zipp-3.18.2.tar.gz", hash = "sha256:6278d9ddbcfb1f1089a88fde84481528b07b0e10474e09dcfe53dad4069fa059"},
@@ -1495,6 +1909,6 @@ docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.link
testing = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"]
[metadata]
-lock-version = "2.0"
-python-versions = "^3.9"
-content-hash = "173cc8bb0b09db652af4c46e683c3bafdf365328c34eccc6b0089909d92b0e7f"
+lock-version = "2.1"
+python-versions = ">=3.10,<3.14"
+content-hash = "dffc126d383906d864314633e479606d4dc927bfc97db6c58f115e950b54a102"
diff --git a/pyproject.toml b/pyproject.toml
index 4b86505..68aeec6 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "cnb-tools"
-version = "0.3.2"
+version = "0.3.3"
description = "Convenience tools/functions for challenges and benchmarking on Synapse.org"
license = "Apache-2.0"
authors = ["Sage CNB Team "]
@@ -11,14 +11,13 @@ readme = "README.md"
repository = "https://github.com/Sage-Bionetworks-Challenges/cnb-tools"
documentation = "https://sage-bionetworks-challenges.github.io/cnb-tools"
classifiers = [
- "Development Status :: 3 - Alpha",
+ "Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"Intended Audience :: Science/Research",
"License :: OSI Approved :: Apache Software License",
"Natural Language :: English",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
- "Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
@@ -28,9 +27,11 @@ classifiers = [
]
[tool.poetry.dependencies]
-python = "^3.9"
+python = ">=3.10,<3.14"
typer = {extras = ["all"], version = "^0.9.0"}
-synapseclient = "^4.2.0"
+synapseclient = "^4.7.0"
+click = ">=8.0.0,<8.2.0"
+pandas = ">=2.0.3"
[tool.poetry.scripts]
cnb-tools = "cnb_tools.main_cli:app"
@@ -39,14 +40,14 @@ cnb-tools = "cnb_tools.main_cli:app"
"Bug Tracker" = "https://github.com/Sage-Bionetworks-Challenges/cnb-tools/issues"
[tool.poetry.group.test.dependencies]
-pytest = "^7.4.3"
+pytest = ">=7.4.3"
[tool.poetry.group.docs.dependencies]
-mkdocs = "^1.5.3"
-mkdocs-material = "^9.4.14"
-mkdocstrings = "^0.24.0"
-mkdocstrings-python = "^1.7.5"
-termynal = "^0.11.1"
+mkdocs = ">=1.5.3"
+mkdocs-material = ">=9.4.14"
+mkdocstrings = ">=0.24.0"
+mkdocstrings-python = ">=1.7.5"
+termynal = ">=0.11.1"
[build-system]
requires = ["poetry-core"]
diff --git a/tests/__init__.py b/tests/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..480bfd8
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,107 @@
+"""Shared pytest fixtures for cnb-tools tests."""
+
+import os
+import sys
+from unittest.mock import MagicMock
+
+import pandas as pd
+import pytest
+from synapseclient import Evaluation, SubmissionStatus
+
+
+def pytest_configure(config):
+ """Allow test scripts to import scripts from parent folder."""
+ src_path = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
+ sys.path.insert(0, src_path)
+
+
+@pytest.fixture
+def mock_syn():
+ """Fixture for mocked Synapse client."""
+ return MagicMock()
+
+
+@pytest.fixture
+def mock_submission_status():
+ """Fixture for mocked SubmissionStatus."""
+ status = MagicMock(spec=SubmissionStatus)
+ status.status = "SCORED"
+ status.submissionAnnotations = MagicMock()
+ status.submissionAnnotations.__getitem__ = lambda self, key: {
+ "score": 0.95,
+ "passed": True,
+ }.get(key)
+ return status
+
+
+@pytest.fixture
+def mock_submission_file():
+ """Fixture for mocked file submission."""
+ sub = MagicMock()
+ sub.id = "12345"
+ sub.evaluationId = "98765"
+ sub.createdOn = "2025-11-26T10:30:00.000Z"
+ sub.get.side_effect = lambda key: {"teamId": 111, "userId": 222}.get(key)
+ return sub
+
+
+@pytest.fixture
+def mock_submission_docker():
+ """Fixture for mocked Docker submission."""
+ sub = MagicMock()
+ sub.id = "12345"
+ sub.evaluationId = "98765"
+ sub.dockerRepositoryName = "docker.synapse.org/syn12345/model"
+ sub.dockerDigest = "sha256:abc123"
+ sub.__contains__ = lambda self, key: key == "dockerDigest"
+ return sub
+
+
+@pytest.fixture
+def mock_evaluation():
+ """Fixture for mocked Evaluation."""
+ eval_obj = MagicMock(spec=Evaluation)
+ eval_obj.id = "98765"
+ eval_obj.name = "Test Queue"
+ eval_obj.contentSource = "syn12345"
+ return eval_obj
+
+
+@pytest.fixture
+def truth_ids():
+ """Fixture for truth IDs."""
+ return pd.Series(["id1", "id2", "id3"], name="ids")
+
+
+@pytest.fixture
+def pred_ids_valid():
+ """Fixture for valid prediction IDs."""
+ return pd.Series(["id1", "id2", "id3"], name="ids")
+
+
+@pytest.fixture
+def pred_ids_invalid():
+ """Fixture for invalid prediction IDs."""
+ return pd.Series(["id1", "id1", "id4"], name="ids")
+
+
+@pytest.fixture
+def pred_values_valid():
+ """Fixture for valid prediction values."""
+ return pd.DataFrame(
+ {
+ "predictions": [0, 1, 1],
+ "probabilities": [0.25, 0.6, 0.83],
+ }
+ )
+
+
+@pytest.fixture
+def pred_values_invalid():
+ """Fixture for invalid prediction values."""
+ return pd.DataFrame(
+ {
+ "predictions": [0, 1, None],
+ "probabilities": [-0.5, 1.0, 1.5],
+ }
+ )
diff --git a/tests/test_annotation.py b/tests/test_annotation.py
new file mode 100644
index 0000000..769241a
--- /dev/null
+++ b/tests/test_annotation.py
@@ -0,0 +1,137 @@
+"""Unit tests for cnb_tools.modules.annotation"""
+
+from unittest.mock import MagicMock, Mock, mock_open, patch
+
+import pytest
+from synapseclient.core.exceptions import SynapseHTTPError
+
+from cnb_tools.modules import annotation
+from cnb_tools.modules.base import UnknownSynapseID
+
+
+class TestGetSubmissionStatus:
+ """Tests for get_submission_status function"""
+
+ @patch("cnb_tools.modules.annotation.get_synapse_client")
+ def test_get_submission_status_success(
+ self, mock_get_client, mock_syn, mock_submission_status
+ ):
+ """Test successfully getting submission status"""
+ mock_get_client.return_value = mock_syn
+ mock_syn.getSubmissionStatus.return_value = mock_submission_status
+
+ result = annotation.get_submission_status(12345)
+
+ mock_syn.getSubmissionStatus.assert_called_once_with(12345)
+ assert result == mock_submission_status
+
+ @patch("cnb_tools.modules.annotation.get_synapse_client")
+ def test_get_submission_status_invalid_id(self, mock_get_client, mock_syn):
+ """Test error handling for invalid submission ID"""
+ mock_get_client.return_value = mock_syn
+ mock_response = Mock()
+ mock_response.json.return_value = {"reason": "Submission not found"}
+ mock_syn.getSubmissionStatus.side_effect = SynapseHTTPError(
+ response=mock_response
+ )
+
+ with pytest.raises(UnknownSynapseID) as exc_info:
+ annotation.get_submission_status(99999)
+ assert "Submission not found" in str(exc_info.value)
+
+
+class TestUpdateAnnotations:
+ """Tests for update_annotations function"""
+
+ @patch("cnb_tools.modules.annotation.get_synapse_client")
+ @patch("cnb_tools.modules.annotation.get_submission_status")
+ def test_update_annotations_success(
+ self, mock_get_status, mock_get_client, mock_syn, mock_submission_status, capsys
+ ):
+ """Test successfully updating annotations"""
+ mock_get_client.return_value = mock_syn
+ mock_get_status.return_value = mock_submission_status
+ mock_syn.store.return_value = mock_submission_status
+
+ # Mock the submissionAnnotations attribute as a dictionary
+ mock_submission_status.submissionAnnotations = MagicMock()
+
+ new_annots = {"new_field": "value"}
+ result = annotation.update_annotations(12345, new_annots, verbose=False)
+
+ mock_submission_status.submissionAnnotations.update.assert_called_once_with(
+ new_annots
+ )
+ mock_syn.store.assert_called_once_with(mock_submission_status)
+
+ captured = capsys.readouterr()
+ assert "Submission ID 12345 annotations updated" in captured.out
+ assert result == mock_submission_status
+
+ @patch("cnb_tools.modules.annotation.get_synapse_client")
+ @patch("cnb_tools.modules.annotation.get_submission_status")
+ def test_update_annotations_verbose(
+ self, mock_get_status, mock_get_client, mock_syn, mock_submission_status, capsys
+ ):
+ """Test updating annotations with verbose output"""
+ mock_get_client.return_value = mock_syn
+ mock_get_status.return_value = mock_submission_status
+ mock_syn.store.return_value = mock_submission_status
+
+ # Mock submissionAnnotations as a dict for JSON serialization
+ mock_submission_status.submissionAnnotations = {"score": 0.95, "passed": True}
+
+ annotation.update_annotations(12345, {"test": "value"}, verbose=True)
+
+ captured = capsys.readouterr()
+ assert "Annotations:" in captured.out
+ assert "score" in captured.out
+
+
+class TestUpdateAnnotationsFromFile:
+ """Tests for update_annotations_from_file function"""
+
+ @patch("cnb_tools.modules.annotation.with_retry")
+ @patch(
+ "builtins.open",
+ new_callable=mock_open,
+ read_data='{"score": 0.85, "status": "pass", "null_field": null, "empty_list": []}',
+ )
+ def test_update_annotations_from_file(self, mock_file, mock_retry):
+ """Test updating annotations from JSON file"""
+ mock_retry.return_value = MagicMock()
+
+ annotation.update_annotations_from_file(12345, "test.json", verbose=False)
+
+ mock_file.assert_called_once_with("test.json", encoding="utf-8")
+
+ # Verify with_retry was called
+ assert mock_retry.called
+
+ # Verify null and empty list values were filtered
+ call_args = mock_retry.call_args
+ assert call_args[1]["wait"] == 3
+ assert call_args[1]["retries"] == 10
+
+
+class TestUpdateSubmissionStatus:
+ """Tests for update_submission_status function"""
+
+ @patch("cnb_tools.modules.annotation.get_synapse_client")
+ @patch("cnb_tools.modules.annotation.get_submission_status")
+ def test_update_submission_status(
+ self, mock_get_status, mock_get_client, mock_syn, mock_submission_status, capsys
+ ):
+ """Test updating submission status"""
+ mock_get_client.return_value = mock_syn
+ mock_get_status.return_value = mock_submission_status
+ mock_syn.store.return_value = mock_submission_status
+
+ result = annotation.update_submission_status(12345, "ACCEPTED")
+
+ assert mock_submission_status.status == "ACCEPTED"
+ mock_syn.store.assert_called_once_with(mock_submission_status)
+
+ captured = capsys.readouterr()
+ assert "Updated submission ID 12345 to status: ACCEPTED" in captured.out
+ assert result == mock_submission_status
diff --git a/tests/test_participant.py b/tests/test_participant.py
new file mode 100644
index 0000000..181ee39
--- /dev/null
+++ b/tests/test_participant.py
@@ -0,0 +1,91 @@
+"""Unit tests for cnb_tools.modules.participant"""
+
+from unittest.mock import MagicMock, patch
+
+import pytest
+from synapseclient import Team
+from synapseclient.core.exceptions import SynapseHTTPError
+
+from cnb_tools.modules import participant
+
+
+class TestGetParticipantName:
+ """Tests for get_participant_name function"""
+
+ @patch("cnb_tools.modules.participant.get_synapse_client")
+ def test_get_participant_name_team(self, mock_get_client, mock_syn):
+ """Test getting team name"""
+ mock_get_client.return_value = mock_syn
+ mock_syn.getTeam.return_value = {"name": "Dream Team"}
+
+ result = participant.get_participant_name(12345)
+
+ assert result == "Dream Team"
+ mock_syn.getTeam.assert_called_once_with(12345)
+
+ @patch("cnb_tools.modules.participant.get_synapse_client")
+ def test_get_participant_name_user(self, mock_get_client, mock_syn):
+ """Test getting username when team lookup fails"""
+ mock_get_client.return_value = mock_syn
+ mock_syn.getTeam.side_effect = SynapseHTTPError(response=MagicMock())
+ mock_syn.getUserProfile.return_value = {"userName": "john_doe"}
+
+ result = participant.get_participant_name(67890)
+
+ assert result == "john_doe"
+ mock_syn.getUserProfile.assert_called_once_with(67890)
+
+
+class TestCreateTeam:
+ """Tests for create_team function"""
+
+ @patch("cnb_tools.modules.participant.typer.confirm")
+ @patch("cnb_tools.modules.participant.get_synapse_client")
+ def test_create_team_new(self, mock_get_client, mock_confirm, mock_syn):
+ """Test creating a new team"""
+ mock_get_client.return_value = mock_syn
+ mock_syn.getTeam.side_effect = ValueError("Team not found")
+
+ result = participant.create_team(
+ name="New Team", description="Test description", can_public_join=True
+ )
+
+ assert isinstance(result, Team)
+ assert result.name == "New Team"
+ assert result.description == "Test description"
+ assert result.canPublicJoin is True
+
+ @patch("cnb_tools.modules.participant.typer.confirm")
+ @patch("cnb_tools.modules.participant.get_synapse_client")
+ def test_create_team_existing_confirmed(
+ self, mock_get_client, mock_confirm, mock_syn
+ ):
+ """Test using an existing team when user confirms"""
+ mock_get_client.return_value = mock_syn
+ existing_team = MagicMock(spec=Team)
+ existing_team.name = "Dream Team"
+ mock_syn.getTeam.return_value = existing_team
+ mock_confirm.return_value = True
+
+ result = participant.create_team(name="Dream Team")
+
+ assert result == existing_team
+ mock_confirm.assert_called_once_with(
+ "Team 'Dream Team' already exists. Use this team?"
+ )
+
+ @patch("cnb_tools.modules.participant.typer.confirm")
+ @patch("cnb_tools.modules.participant.get_synapse_client")
+ def test_create_team_existing_declined(
+ self, mock_get_client, mock_confirm, mock_syn
+ ):
+ """Test declining to use existing team exits"""
+ mock_get_client.return_value = mock_syn
+ existing_team = MagicMock(spec=Team)
+ mock_syn.getTeam.return_value = existing_team
+ mock_confirm.return_value = False
+
+ with pytest.raises(SystemExit) as exc_info:
+ participant.create_team(name="Existing Team")
+
+ assert "Try again with a new challenge name" in str(exc_info.value)
diff --git a/tests/test_queue.py b/tests/test_queue.py
new file mode 100644
index 0000000..6a58d1b
--- /dev/null
+++ b/tests/test_queue.py
@@ -0,0 +1,73 @@
+"""Unit tests for cnb_tools.modules.queue"""
+
+from unittest.mock import MagicMock, Mock, patch
+
+import pytest
+from synapseclient.core.exceptions import SynapseHTTPError
+
+from cnb_tools.modules import queue
+from cnb_tools.modules.base import UnknownSynapseID
+
+
+class TestGetEvaluation:
+ """Tests for get_evaluation function"""
+
+ @patch("cnb_tools.modules.queue.get_synapse_client")
+ def test_get_evaluation_success(self, mock_get_client, mock_syn, mock_evaluation):
+ """Test successfully getting an evaluation"""
+ mock_get_client.return_value = mock_syn
+ mock_syn.getEvaluation.return_value = mock_evaluation
+
+ result = queue.get_evaluation(98765)
+
+ mock_syn.getEvaluation.assert_called_once_with(98765)
+ assert result == mock_evaluation
+
+ @patch("cnb_tools.modules.queue.get_synapse_client")
+ def test_get_evaluation_invalid_id(self, mock_get_client, mock_syn):
+ """Test error handling for invalid evaluation ID"""
+ mock_get_client.return_value = mock_syn
+ mock_response = Mock()
+ mock_response.json.return_value = {"reason": "Evaluation not found"}
+ mock_syn.getEvaluation.side_effect = SynapseHTTPError(response=mock_response)
+
+ with pytest.raises(UnknownSynapseID) as exc_info:
+ queue.get_evaluation(99999)
+
+ assert "Evaluation not found" in str(exc_info.value)
+
+
+class TestGetEvaluationName:
+ """Tests for get_evaluation_name function"""
+
+ @patch("cnb_tools.modules.queue.get_evaluation")
+ def test_get_evaluation_name(self, mock_get_eval, mock_evaluation):
+ """Test getting evaluation name"""
+ mock_get_eval.return_value = mock_evaluation
+
+ result = queue.get_evaluation_name(98765)
+
+ assert result == "Test Queue"
+ mock_get_eval.assert_called_once_with(98765)
+
+
+class TestGetChallengeNameFromEvaluation:
+ """Tests for get_challenge_name_from_evaluation function"""
+
+ @patch("cnb_tools.modules.queue.get_synapse_client")
+ @patch("cnb_tools.modules.queue.get_evaluation")
+ def test_get_challenge_name_from_evaluation(
+ self, mock_get_eval, mock_get_client, mock_syn, mock_evaluation
+ ):
+ """Test getting challenge name from evaluation"""
+ mock_get_eval.return_value = mock_evaluation
+ mock_get_client.return_value = mock_syn
+
+ mock_challenge = MagicMock()
+ mock_challenge.name = "Amazing Challenge"
+ mock_syn.get.return_value = mock_challenge
+
+ result = queue.get_challenge_name_from_evaluation(98765)
+
+ assert result == "Amazing Challenge"
+ mock_syn.get.assert_called_once_with("syn12345")
diff --git a/tests/test_submission.py b/tests/test_submission.py
new file mode 100644
index 0000000..76f778c
--- /dev/null
+++ b/tests/test_submission.py
@@ -0,0 +1,196 @@
+"""Unit tests for cnb_tools.modules.submission"""
+
+from unittest.mock import MagicMock, Mock, patch
+
+import pytest
+from synapseclient.core.exceptions import SynapseHTTPError
+
+from cnb_tools.modules import submission
+from cnb_tools.modules.base import UnknownSynapseID
+
+
+class TestGetSubmission:
+ """Tests for get_submission function"""
+
+ @patch("cnb_tools.modules.submission.get_synapse_client")
+ def test_get_submission_success(
+ self, mock_get_client, mock_syn, mock_submission_file
+ ):
+ """Test successfully getting a submission"""
+ mock_get_client.return_value = mock_syn
+ mock_syn.getSubmission.return_value = mock_submission_file
+
+ result = submission.get_submission(12345, download_file=False)
+
+ mock_syn.getSubmission.assert_called_once_with(12345, downloadFile=False)
+ assert result == mock_submission_file
+
+ @patch("cnb_tools.modules.submission.get_synapse_client")
+ def test_get_submission_invalid_id(self, mock_get_client, mock_syn):
+ """Test error handling for invalid submission ID"""
+ mock_get_client.return_value = mock_syn
+ mock_response = Mock()
+ mock_response.json.return_value = {"reason": "Submission not found"}
+ mock_syn.getSubmission.side_effect = SynapseHTTPError(response=mock_response)
+
+ with pytest.raises(UnknownSynapseID) as exc_info:
+ submission.get_submission(99999)
+
+ assert "Submission not found" in str(exc_info.value)
+
+
+class TestDeleteSubmission:
+ """Tests for delete_submission function"""
+
+ @patch("cnb_tools.modules.submission.get_synapse_client")
+ @patch("cnb_tools.modules.submission.get_submission")
+ def test_delete_submission(
+ self, mock_get_sub, mock_get_client, mock_syn, mock_submission_file, capsys
+ ):
+ """Test deleting a submission"""
+ mock_get_client.return_value = mock_syn
+ mock_get_sub.return_value = mock_submission_file
+
+ submission.delete_submission(12345)
+
+ mock_get_sub.assert_called_once_with(12345)
+ mock_syn.delete.assert_called_once_with(mock_submission_file)
+
+ captured = capsys.readouterr()
+ assert "Submission deleted: 12345" in captured.out
+
+
+class TestDownloadSubmission:
+ """Tests for download_submission function"""
+
+ @patch("cnb_tools.modules.submission.get_synapse_client")
+ @patch("cnb_tools.modules.submission.get_submission")
+ def test_download_submission_file(
+ self, mock_get_sub, mock_get_client, mock_syn, mock_submission_file, capsys
+ ):
+ """Test downloading a file submission"""
+ mock_get_client.return_value = mock_syn
+ mock_get_sub.return_value = mock_submission_file
+
+ submission.download_submission(12345, dest="/tmp")
+
+ mock_syn.getSubmission.assert_called_once_with(12345, downloadLocation="/tmp")
+
+ captured = capsys.readouterr()
+ assert "Submission ID 12345 downloaded to:" in captured.out
+
+ @patch("cnb_tools.modules.submission.get_synapse_client")
+ @patch("cnb_tools.modules.submission.get_submission")
+ def test_download_submission_docker(
+ self, mock_get_sub, mock_get_client, mock_syn, mock_submission_docker, capsys
+ ):
+ """Test 'downloading' a Docker submission (displays pull command)"""
+ mock_get_client.return_value = mock_syn
+ mock_get_sub.return_value = mock_submission_docker
+
+ submission.download_submission(12345)
+
+ captured = capsys.readouterr()
+ assert "Docker image" in captured.out
+ assert "docker pull" in captured.out
+ assert "docker.synapse.org/syn12345/model@sha256:abc123" in captured.out
+
+
+class TestGetSubmitterName:
+ """Tests for get_submitter_name function"""
+
+ @patch("cnb_tools.modules.submission.get_synapse_client")
+ def test_get_submitter_name_team(self, mock_get_client, mock_syn):
+ """Test getting team name"""
+ mock_get_client.return_value = mock_syn
+ mock_syn.getTeam.return_value = {"name": "Test Team"}
+
+ result = submission.get_submitter_name(111)
+
+ assert result == "Test Team"
+
+ @patch("cnb_tools.modules.submission.get_synapse_client")
+ def test_get_submitter_name_user(self, mock_get_client, mock_syn):
+ """Test getting username when team lookup fails"""
+ mock_get_client.return_value = mock_syn
+ mock_syn.getTeam.side_effect = SynapseHTTPError(response=Mock())
+ mock_syn.getUserProfile.return_value = {"userName": "testuser"}
+
+ result = submission.get_submitter_name(222)
+
+ assert result == "testuser"
+
+
+class TestGetChallengeName:
+ """Tests for get_challenge_name function"""
+
+ @patch("cnb_tools.modules.submission.get_synapse_client")
+ def test_get_challenge_name(self, mock_get_client, mock_syn):
+ """Test getting challenge name from evaluation"""
+ mock_get_client.return_value = mock_syn
+ mock_eval = MagicMock()
+ mock_eval.contentSource = "syn98765"
+ mock_syn.getEvaluation.return_value = mock_eval
+
+ mock_challenge = MagicMock()
+ mock_challenge.name = "Test Challenge"
+ mock_syn.get.return_value = mock_challenge
+
+ result = submission.get_challenge_name(12345)
+
+ assert result == "Test Challenge"
+ mock_syn.get.assert_called_once_with("syn98765")
+
+
+class TestPrintSubmissionInfo:
+ """Tests for print_submission_info function"""
+
+ @patch("cnb_tools.modules.submission.get_submission")
+ @patch("cnb_tools.modules.submission.get_challenge_name")
+ @patch("cnb_tools.modules.submission.get_submitter_name")
+ def test_print_submission_info_basic(
+ self,
+ mock_get_submitter,
+ mock_get_challenge,
+ mock_get_sub,
+ mock_submission_file,
+ capsys,
+ ):
+ """Test printing basic submission info"""
+ mock_get_sub.return_value = mock_submission_file
+ mock_get_challenge.return_value = "Test Challenge"
+ mock_get_submitter.return_value = "Test Team"
+
+ submission.print_submission_info(12345, verbose=False)
+
+ captured = capsys.readouterr()
+ assert "ID: 12345" in captured.out
+ assert "Challenge: Test Challenge" in captured.out
+ assert "Date: 2025-11-26" in captured.out
+ assert "Submitter: Test Team" in captured.out
+
+ @patch("cnb_tools.modules.submission.annotation")
+ @patch("cnb_tools.modules.submission.get_submission")
+ @patch("cnb_tools.modules.submission.get_challenge_name")
+ @patch("cnb_tools.modules.submission.get_submitter_name")
+ def test_print_submission_info_verbose(
+ self,
+ mock_get_submitter,
+ mock_get_challenge,
+ mock_get_sub,
+ mock_annotation,
+ mock_submission_file,
+ ):
+ """Test printing submission info with annotations"""
+ mock_get_sub.return_value = mock_submission_file
+ mock_get_challenge.return_value = "Test Challenge"
+ mock_get_submitter.return_value = "Test Team"
+
+ mock_status = MagicMock()
+ mock_annotation.get_submission_status.return_value = mock_status
+ mock_annotation.format_annotations.return_value = "Formatted annotations"
+
+ submission.print_submission_info(12345, verbose=True)
+
+ mock_annotation.get_submission_status.assert_called_once_with(12345)
+ mock_annotation.format_annotations.assert_called_once_with(mock_status)
diff --git a/tests/test_validation_toolkit.py b/tests/test_validation_toolkit.py
new file mode 100644
index 0000000..35ce01d
--- /dev/null
+++ b/tests/test_validation_toolkit.py
@@ -0,0 +1,125 @@
+"""Unit tests for cnb_tools.validation_toolkit"""
+
+from cnb_tools import validation_toolkit
+
+
+class TestCheckMissingKeys:
+ """Tests for check_missing_keys function"""
+
+ def test_no_missing_keys(self, truth_ids, pred_ids_valid):
+ """Test when all keys are present"""
+ result = validation_toolkit.check_missing_keys(truth_ids, pred_ids_valid)
+ assert result == ""
+
+ def test_missing_keys(self, truth_ids, pred_ids_invalid):
+ """Test when keys are missing"""
+ result = validation_toolkit.check_missing_keys(truth_ids, pred_ids_invalid)
+ assert "Found 2 missing ID(s)" in result
+
+ def test_missing_keys_verbose(self, truth_ids, pred_ids_invalid):
+ """Test missing keys with verbose output"""
+ result = validation_toolkit.check_missing_keys(
+ truth_ids, pred_ids_invalid, verbose=True
+ )
+ assert "Found 2 missing ID(s)" in result
+ assert "id2" in result
+ assert "id3" in result
+
+
+class TestCheckUnknownKeys:
+ """Tests for check_unknown_keys function"""
+
+ def test_no_unknown_keys(self, truth_ids, pred_ids_valid):
+ """Test when no unknown keys are present"""
+ result = validation_toolkit.check_unknown_keys(truth_ids, pred_ids_valid)
+ assert result == ""
+
+ def test_unknown_keys(self, truth_ids, pred_ids_invalid):
+ """Test when unknown keys are present"""
+ result = validation_toolkit.check_unknown_keys(truth_ids, pred_ids_invalid)
+ assert "Found 1 unknown ID(s)" in result
+
+ def test_unknown_keys_verbose(self, truth_ids, pred_ids_invalid):
+ """Test unknown keys with verbose output"""
+ result = validation_toolkit.check_unknown_keys(
+ truth_ids, pred_ids_invalid, verbose=True
+ )
+ assert "Found 1 unknown ID(s)" in result
+ assert "id4" in result
+
+
+class TestCheckDuplicateKeys:
+ """Tests for check_duplicate_keys function"""
+
+ def test_no_duplicates(self, pred_ids_valid):
+ """Test when no duplicate keys are present"""
+ result = validation_toolkit.check_duplicate_keys(pred_ids_valid)
+ assert result == ""
+
+ def test_duplicates(self, pred_ids_invalid):
+ """Test when duplicate keys are present"""
+ result = validation_toolkit.check_duplicate_keys(pred_ids_invalid)
+ assert "Found 1 duplicate ID(s)" in result
+
+ def test_duplicates_verbose(self, pred_ids_invalid):
+ """Test duplicate keys with verbose output"""
+ result = validation_toolkit.check_duplicate_keys(pred_ids_invalid, verbose=True)
+ assert "Found 1 duplicate ID(s)" in result
+ assert "id1" in result
+
+
+class TestCheckNanValues:
+ """Tests for check_nan_values function"""
+
+ def test_no_nan_values(self, pred_values_valid):
+ """Test when no NaN values are present"""
+ result = validation_toolkit.check_nan_values(pred_values_valid["predictions"])
+ assert result == ""
+
+ def test_with_nan_values(self, pred_values_invalid):
+ """Test when NaN values are present"""
+ result = validation_toolkit.check_nan_values(pred_values_invalid["predictions"])
+ assert "'predictions' column contains 1 NaN value(s)" in result
+
+
+class TestCheckBinaryValues:
+ """Tests for check_binary_values function"""
+
+ def test_valid_binary_values_default(self, pred_values_valid):
+ """Test valid binary values with default labels (0, 1)"""
+ result = validation_toolkit.check_binary_values(
+ pred_values_valid["predictions"]
+ )
+ assert result == ""
+
+ def test_invalid_binary_values(self, pred_values_invalid):
+ """Test invalid binary values"""
+ result = validation_toolkit.check_binary_values(
+ pred_values_invalid["predictions"]
+ )
+ assert "'predictions' values should only be 0 or 1" in result
+
+
+class TestCheckValuesRange:
+ """Tests for check_values_range function"""
+
+ def test_valid_range_default(self, pred_values_valid):
+ """Test values within default range [0, 1]"""
+ result = validation_toolkit.check_values_range(
+ pred_values_valid["probabilities"]
+ )
+ assert result == ""
+
+ def test_invalid_range_below_min(self, pred_values_invalid):
+ """Test values below minimum"""
+ result = validation_toolkit.check_values_range(
+ pred_values_invalid["probabilities"]
+ )
+ assert "'probabilities' values should be between [0, 1]" in result
+
+ def test_invalid_range_above_max(self, pred_values_invalid):
+ """Test values above maximum"""
+ result = validation_toolkit.check_values_range(
+ pred_values_invalid["probabilities"]
+ )
+ assert "'probabilities' values should be between [0, 1]" in result