diff --git a/Makefile b/Makefile index a7c7376..40e4d2d 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -VERSION ?= 0.5.0 +VERSION ?= 0.5.2 SHELL := /bin/bash .PHONY: releasehere diff --git a/anaconda_build/meta.yaml b/anaconda_build/meta.yaml index 56806e0..ea07e3d 100644 --- a/anaconda_build/meta.yaml +++ b/anaconda_build/meta.yaml @@ -1,6 +1,6 @@ package: name: openprotein-python - version: "0.5.0" + version: "0.5.2" source: path: ../ @@ -12,19 +12,19 @@ build: requirements: build: - - python >=3.10,<3.11 + - python >=3.10 - hatchling >=1.25.0,<2 host: - - python >=3.10,<3.11 + - python >=3.10 - pip - hatchling >=1.25.0,<2 run: - - python >=3.10,<3.11 + - python >=3.10 - requests >=2.32.3,<3 - pydantic >=2.5,<3 - tqdm >=4.66.5,<5 - - pandas >=2.1.4,<3 - - numpy >=2.1.1,<3 + - pandas >=2.2.2,<3 + - numpy >=1.9,<3 about: home: https://www.openprotein.ai/ diff --git a/openprotein/__init__.py b/openprotein/__init__.py index b74045c..f97d2cb 100644 --- a/openprotein/__init__.py +++ b/openprotein/__init__.py @@ -1,14 +1,24 @@ +""" +OpenProtein Python client. + +A pythonic interface for interacting with our cutting-edge protein engineering platform. + +isort:skip_file +""" + from openprotein._version import __version__ from openprotein.app import ( - SVDAPI, - AlignAPI, AssayDataAPI, + JobsAPI, + TrainingAPI, DesignAPI, + AlignAPI, EmbeddingsAPI, FoldAPI, - JobsAPI, + SVDAPI, + UMAPAPI, PredictorAPI, - TrainingAPI, + DesignerAPI, ) from openprotein.app.models import Future from openprotein.base import APISession @@ -19,15 +29,17 @@ class OpenProtein(APISession): The base class for accessing OpenProtein API functionality. """ - _embedding = None - _svd = None - _fold = None - _align = None - _jobs = None _data = None + _jobs = None _train = None _design = None + _align = None + _embedding = None + _svd = None + _umap = None + _fold = None _predictor = None + _designer = None def wait(self, future: Future, *args, **kwargs): return future.wait(*args, **kwargs) @@ -37,15 +49,6 @@ def wait(self, future: Future, *args, **kwargs): def load_job(self, job_id): return self.jobs.__load(job_id=job_id) - @property - def jobs(self) -> JobsAPI: - """ - The jobs submodule gives access to functionality for listing jobs and checking their status. - """ - if self._jobs is None: - self._jobs = JobsAPI(self) - return self._jobs - @property def data(self) -> AssayDataAPI: """ @@ -55,6 +58,15 @@ def data(self) -> AssayDataAPI: self._data = AssayDataAPI(self) return self._data + @property + def jobs(self) -> JobsAPI: + """ + The jobs submodule gives access to functionality for listing jobs and checking their status. + """ + if self._jobs is None: + self._jobs = JobsAPI(self) + return self._jobs + @property def train(self) -> TrainingAPI: """ @@ -64,6 +76,15 @@ def train(self) -> TrainingAPI: self._train = TrainingAPI(self) return self._train + @property + def design(self) -> DesignAPI: + """ + The design submodule gives access to functionality for designing new sequences using models from train. + """ + if self._design is None: + self._design = DesignAPI(self) + return self._design + @property def align(self) -> AlignAPI: """ @@ -91,6 +112,15 @@ def svd(self) -> SVDAPI: self._svd = SVDAPI(self, self.embedding) return self._svd + @property + def umap(self) -> UMAPAPI: + """ + The embedding submodule gives access to protein embedding models and their inference endpoints. + """ + if self._umap is None: + self._umap = UMAPAPI(self) + return self._umap + @property def predictor(self) -> PredictorAPI: """ @@ -101,13 +131,13 @@ def predictor(self) -> PredictorAPI: return self._predictor @property - def design(self) -> DesignAPI: + def designer(self) -> DesignerAPI: """ - The design submodule gives access to functionality for designing new sequences using models from train. + The designer submodule gives access to functionality for designing new sequences using models from predictor train. """ - if self._design is None: - self._design = DesignAPI(self) - return self._design + if self._designer is None: + self._designer = DesignerAPI(self) + return self._designer @property def fold(self) -> FoldAPI: diff --git a/openprotein/api/__init__.py b/openprotein/api/__init__.py index c53bffd..a1a3762 100644 --- a/openprotein/api/__init__.py +++ b/openprotein/api/__init__.py @@ -1,2 +1 @@ from . import align, assaydata, design, embedding, fold, predict, predictor, train -from .deprecated import poet diff --git a/openprotein/api/align.py b/openprotein/api/align.py index 14f467e..cf34ec3 100644 --- a/openprotein/api/align.py +++ b/openprotein/api/align.py @@ -1,5 +1,3 @@ -import codecs -import csv import io import random from typing import BinaryIO, Iterator @@ -7,31 +5,11 @@ import openprotein.config as config import requests from openprotein.base import APISession +from openprotein.csv import csv_stream from openprotein.errors import APIError, InvalidParameterError, MissingParameterError from openprotein.schemas import Job, MSASamplingMethod, PoetInputType -def csv_stream(response: requests.Response) -> Iterator[list[str]]: - """ - Returns a CSV reader from a requests.Response object. - - Parameters - ---------- - response : requests.Response - The response object to parse. - - Returns - ------- - csv.reader - A csv reader object for the response. - """ - raw_content = response.raw # the raw bytes stream - content = codecs.getreader("utf-8")( - raw_content - ) # force the response to be encoded as utf-8 - return csv.reader(content) - - def get_align_job_inputs( session: APISession, job_id: str, diff --git a/openprotein/api/deprecated/poet.py b/openprotein/api/deprecated/poet.py index c792cb5..7c2fdf3 100644 --- a/openprotein/api/deprecated/poet.py +++ b/openprotein/api/deprecated/poet.py @@ -5,7 +5,11 @@ from openprotein import config from openprotein.base import APISession from openprotein.errors import APIError, InvalidParameterError, MissingParameterError -from openprotein.schemas import PoetGenerateJob, PoetScoreJob, PoetSSPJob +from openprotein.schemas.deprecated.poet import ( + PoetGenerateJob, + PoetScoreJob, + PoetSSPJob, +) def poet_score_post( diff --git a/openprotein/api/design.py b/openprotein/api/design.py index 9d49a11..0475882 100644 --- a/openprotein/api/design.py +++ b/openprotein/api/design.py @@ -1,8 +1,12 @@ from openprotein.base import APISession -from openprotein.schemas import DesignJobCreate, DesignResults, Job +from openprotein.schemas import ( + WorkflowDesign, + WorkflowDesignJob, + WorkflowDesignJobCreate, +) -def create_design_job(session: APISession, design_job: DesignJobCreate): +def create_design_job(session: APISession, design_job: WorkflowDesignJobCreate): """ Send a POST request for protein design job. @@ -13,7 +17,7 @@ def create_design_job(session: APISession, design_job: DesignJobCreate): design_job : DesignJobCreate The details of the design job to be created, with the following parameters: - assay_id: The ID for the assay. - - criteria: A list of CriterionItem lists for evaluating the design. + - criteria: Criteria for evaluating the design. - num_steps: The number of steps in the genetic algo. Default is 8. - pop_size: The population size for the genetic algo. Default is None. - n_offsprings: The number of offspring for the genetic algo. Default is None. @@ -24,13 +28,13 @@ def create_design_job(session: APISession, design_job: DesignJobCreate): Returns ------- - Job + WorkflowDesignJob The created job as a Job instance. """ params = design_job.model_dump(exclude_none=True) # print(f"sending design: {params}") response = session.post("v1/workflow/design/genetic-algorithm", json=params) - return Job.model_validate(response.json()) + return WorkflowDesignJob.model_validate(response.json()) def get_design_results( @@ -39,7 +43,7 @@ def get_design_results( step: int | None = None, page_size: int | None = None, page_offset: int | None = None, -) -> DesignResults: +) -> WorkflowDesign: """ Retrieves the results of a Design job. @@ -60,7 +64,7 @@ def get_design_results( Returns ------- - DesignJob + WorkflowDesignJob The job object representing the Design job. Raises @@ -79,4 +83,4 @@ def get_design_results( response = session.get(endpoint, params=params) - return DesignResults.model_validate(response.json()) + return WorkflowDesign.model_validate(response.json()) diff --git a/openprotein/api/designer.py b/openprotein/api/designer.py new file mode 100644 index 0000000..a1c07e1 --- /dev/null +++ b/openprotein/api/designer.py @@ -0,0 +1,262 @@ +"""API for interacting with the designer service.""" + +from typing import Generator, Iterator, NamedTuple + +import numpy as np +import pandas as pd +from openprotein.base import APISession +from openprotein.csv import csv_stream +from openprotein.schemas import ( + Criteria, + Criterion, + Design, + DesignConstraint, + DesignJob, + Job, + Subcriterion, +) +from pydantic import TypeAdapter + +PATH_PREFIX = "v1/designer/design" + + +def designs_list(session: APISession) -> list[Design]: + """ + List designs. + + Parameters + ---------- + session : APISession + Session object for API communication. + + Returns + ------- + list[Design] + List of designs. + """ + endpoint = PATH_PREFIX + response = session.get(endpoint) + return TypeAdapter(list[Design]).validate_python(response.json()) + + +def design_get(session: APISession, design_id: str) -> Design: + """ + Get design. + + Parameters + ---------- + session : APISession + Session object for API communication. + design_id: str + ID of design to get. + + Returns + ------- + Design + Design metadata. + """ + endpoint = PATH_PREFIX + f"/{design_id}" + response = session.get(endpoint) + return TypeAdapter(Design).validate_python(response.json()) + + +def designer_create_genetic_algorithm( + session: APISession, + assay_id: str, + criteria: Criteria | Subcriterion | Criterion, + num_steps: int = 25, + pop_size: int = 1024, # TODO - rename to library_size + n_offsprings: int = 5120, + crossover_prob: float = 1.0, + crossover_prob_pointwise: float = 0.2, + mutation_average_mutations_per_seq: int = 1, + allowed_tokens: DesignConstraint | dict[int, list[str]] = {}, +) -> Job: + """ + Create design using genetic algorithm. + + Parameters + ---------- + session : APISession + Session object for API communication. + assay_id : str + Assay ID to fit GP on. + criteria: list[list[DesignCriterion]] + List of list of design criteria, logically grouping by OR then AND. + num_steps: int, optional + The number of steps in the genetic algorithm. Default is 8. + pop_size: int, optional + The population size for the genetic algorithm. Default is 256. + n_offsprings: int, optional + The number of offspring for the genetic algorithm. Default is 5120. + crossover_prob: float, optional + The crossover probability for the genetic algorithm. Default is 1. + crossover_prob_pointwise: float, optional + The pointwise crossover probability for the genetic algorithm. Default is 0.2. + mutation_average_mutations_per_seq: int, optional + The average number of mutations per sequence. Default is 1. + allowed_tokens: DesignConstraint | dict[int, list[str]] + A dict of positions and allows tokens (e.g. *{1:['G','L']})* ) designating how mutations may occur. Defaults to empty dict. + + Returns + ------- + DesignJob + """ + if isinstance(criteria, Subcriterion): + criteria = Criteria([Criterion([criteria])]) + elif isinstance(criteria, Criterion): + criteria = Criteria([criteria]) + + endpoint = PATH_PREFIX + "/genetic-algorithm" + + body = { + "assay_id": assay_id, + "criteria": criteria.model_dump(), + "num_steps": num_steps, + "pop_size": pop_size, + "n_offsprings": n_offsprings, + "crossover_prob": crossover_prob, + "crossover_prob_pointwise": crossover_prob_pointwise, + "mutation_average_mutations_per_seq": mutation_average_mutations_per_seq, + "allowed_tokens": allowed_tokens, + } + response = session.post(endpoint, json=body) + return DesignJob.model_validate(response.json()) + + +def design_delete(session: APISession, design_id: str): + raise NotImplementedError() + + +def designer_get_design_results( + session: APISession, + design_id: str, + step: int | None = -1, +) -> Iterator[list[str]]: + """ + Get csv encoded results for a design ID. + + Parameters + ---------- + session : APISession + Session object for API communication. + design_id : str + Design ID to retrieve results from. + step: int | None, optional + Step of the design whose results to fetch. Defaults to -1, which refers to the last step. + + Returns + ------- + bytes + """ + params = {} + if step is not None: + params["step"] = step + endpoint = PATH_PREFIX + f"/{design_id}/results" + response = session.get(endpoint, params=params, stream=True) + return csv_stream(response) + + +class DesignResult(NamedTuple): + step: int + sample_index: int + sequence: str + scores: np.ndarray + subscores: np.ndarray + means: np.ndarray + vars: np.ndarray + + +def decode_design_result( + row: list[str], + score_start_index: int, + subscore_start_index: int, + pred_start_index: int, +) -> DesignResult: + """ + Decode prediction scores. + + Args: + data (bytes): raw bytes encoding the array received over the API + batched (bool): whether or not the result was batched. affects the retrieved csv format whether they contain additional columns and header rows. + + Returns: + mus (np.ndarray): decoded array of means + vars (np.ndarray): decoded array of variances + """ + scores = np.array( + [float(score) for score in row[score_start_index:subscore_start_index]] + ) + subscores = np.array( + [float(subscore) for subscore in row[subscore_start_index:pred_start_index]] + ) + preds = np.array([float(pred) for pred in row[pred_start_index:]]) + result = DesignResult( + step=int(row[0]), + sample_index=int(row[1]), + sequence=row[2], + scores=scores, + subscores=subscores, + means=preds[::2], + vars=preds[1::2], + ) + return result + + +def decode_design_results_stream( + data: Iterator[list[str]], header: list[str] | None = None +) -> Generator[DesignResult, None, None]: + """ + Decode design results. + + Args: + data: Iterator[list[str]] + Data in the form of an iterator of list of string-encoded values + header: list[str] | None, optional + Headers describing the data. Should be same length as each row returned from the data iterator. + Defaults to None, which means the first row in the iterator should be header. + + Returns: + step: int + Step index of the design. + sample_index: int + Index of the sample in the overall design. + sequence: str + Output designed sequence. + scores: np.ndarray[float] + M array of scores based on provided criteria (M groups of subcriteria). + subscores: np.ndarray[float] + N array of subscores based on provided criteria (flattened N subcriteria). + means: np.ndarray[float] + K array of means for each model subscriterion. + vars: np.ndarray[float] + K array of variances for each model subscriterion. + vars (np.ndarray): decoded array of variances + """ + if header is None: + header = next(data) + if header[0].isnumeric(): + raise ValueError( + "Expected first row in data to be header of 'step','sample_index',..." + ) + score_start_index = subscore_start_index = pred_start_index = len(header) + # first start indices + for i, col_name in enumerate(header): + if col_name.startswith("score"): + score_start_index = i + break + for i, col_name in enumerate(header[score_start_index:]): + if col_name.endswith("score"): + subscore_start_index = score_start_index + i + break + for i, col_name in enumerate(header[subscore_start_index:]): + if col_name.endswith("y_mu"): + pred_start_index = subscore_start_index + i + break + for row in data: + yield decode_design_result( + row=row, + score_start_index=score_start_index, + subscore_start_index=subscore_start_index, + pred_start_index=pred_start_index, + ) diff --git a/openprotein/api/embedding.py b/openprotein/api/embedding.py index c7a2ede..852844c 100644 --- a/openprotein/api/embedding.py +++ b/openprotein/api/embedding.py @@ -3,13 +3,14 @@ from typing import Iterator import numpy as np -from openprotein.api.align import csv_stream from openprotein.base import APISession +from openprotein.csv import csv_stream from openprotein.errors import InvalidParameterError from openprotein.schemas import ( AttnJob, EmbeddingsJob, GenerateJob, + JobType, LogitsJob, ModelMetadata, ScoreJob, @@ -44,7 +45,9 @@ def get_model(session: APISession, model_id: str) -> ModelMetadata: return ModelMetadata(**result) -def get_request_sequences(session: APISession, job_id: str) -> list[bytes]: +def get_request_sequences( + session: APISession, job_id: str, job_type: JobType = JobType.embeddings_embed +) -> list[bytes]: """ Get results associated with the given request ID. @@ -59,13 +62,18 @@ def get_request_sequences(session: APISession, job_id: str) -> list[bytes]: ------- sequences : List[bytes] """ - endpoint = PATH_PREFIX + f"/{job_id}/sequences" + # NOTE - allow to handle svd/embed and umap/embed directly too instead of redirect + path = "v1" + job_type.value + endpoint = path + f"/{job_id}/sequences" response = session.get(endpoint) return TypeAdapter(list[bytes]).validate_python(response.json()) def request_get_sequence_result( - session: APISession, job_id: str, sequence: str | bytes + session: APISession, + job_id: str, + sequence: str | bytes, + job_type: JobType = JobType.embeddings_embed, ) -> bytes: """ Get encoded result for a sequence from the request ID. @@ -83,9 +91,11 @@ def request_get_sequence_result( ------- result : bytes """ + # NOTE - allow to handle svd/embed and umap/embed directly too instead of redirect + path = "v1" + job_type.value if isinstance(sequence, bytes): sequence = sequence.decode() - endpoint = PATH_PREFIX + f"/{job_id}/{sequence}" + endpoint = path + f"/{job_id}/{sequence}" response = session.get(endpoint) return response.content diff --git a/openprotein/api/job.py b/openprotein/api/job.py index a92854c..8f61fd1 100644 --- a/openprotein/api/job.py +++ b/openprotein/api/job.py @@ -52,6 +52,7 @@ def jobs_list( job_type: str | None = None, assay_id: str | None = None, more_recent_than: str | None = None, + limit: int | None = None, ) -> List[Job]: """ Retrieve a list of jobs filtered by specific criteria. @@ -85,6 +86,8 @@ def jobs_list( params["assay_id"] = assay_id if more_recent_than is not None: params["more_recent_than"] = more_recent_than + if limit is not None: + params["limit"] = limit response = session.get(endpoint, params=params) # return jobs, not futures diff --git a/openprotein/api/predict.py b/openprotein/api/predict.py index be03636..50f3e47 100644 --- a/openprotein/api/predict.py +++ b/openprotein/api/predict.py @@ -1,6 +1,7 @@ from openprotein.base import APISession from openprotein.errors import InvalidParameterError from openprotein.schemas import WorkflowPredictJob, WorkflowPredictSingleSiteJob +from pydantic import TypeAdapter def _create_predict_job( @@ -9,7 +10,9 @@ def _create_predict_job( payload: dict, model_ids: list[str] | None = None, train_job_id: str | None = None, -) -> WorkflowPredictJob: + page_size: int | None = None, + page_offset: int | None = None, +) -> WorkflowPredictJob | WorkflowPredictSingleSiteJob: """ Creates a Predict request and returns the job object. @@ -60,9 +63,16 @@ def _create_predict_job( payload["model_id"] = model_ids else: payload["train_job_id"] = train_job_id + params = {} + if page_size is not None: + params["page_size"] = page_size + if page_offset is not None: + params["page_offset"] = page_offset - response = session.post(endpoint, json=payload) - return WorkflowPredictJob.model_validate(response.json()) + response = session.post(endpoint, json=payload, params=params) + return TypeAdapter( + WorkflowPredictJob | WorkflowPredictSingleSiteJob + ).validate_python(response.json()) def create_predict_job( @@ -70,6 +80,8 @@ def create_predict_job( sequences: list[str], train_job_id: str | None = None, model_ids: list[str] | None = None, + page_size: int | None = None, + page_offset: int | None = None, ) -> WorkflowPredictJob: """ Creates a predict job with a given set of sequences and a train job. @@ -107,9 +119,19 @@ def create_predict_job( model_ids = [model_ids] endpoint = "v1/workflow/predict" payload = {"sequences": sequences} - return _create_predict_job( - session, endpoint, payload, model_ids=model_ids, train_job_id=train_job_id + pj = _create_predict_job( + session=session, + endpoint=endpoint, + payload=payload, + model_ids=model_ids, + train_job_id=train_job_id, + page_size=page_size, + page_offset=page_offset, ) + assert isinstance( + pj, WorkflowPredictJob + ), "Expected WorkflowPredictJob to be returned" + return pj def create_predict_single_site( @@ -117,7 +139,9 @@ def create_predict_single_site( sequence: str, train_job_id: str | None = None, model_ids: list[str] | None = None, -) -> WorkflowPredictJob: + page_size: int | None = None, + page_offset: int | None = None, +) -> WorkflowPredictSingleSiteJob: """ Creates a predict job for single site mutants with a given sequence and a train job. @@ -150,9 +174,19 @@ def create_predict_single_site( """ endpoint = "v1/workflow/predict/single_site" payload = {"sequence": sequence} - return _create_predict_job( - session, endpoint, payload, model_ids=model_ids, train_job_id=train_job_id + pj = _create_predict_job( + session=session, + endpoint=endpoint, + payload=payload, + model_ids=model_ids, + train_job_id=train_job_id, + page_size=page_size, + page_offset=page_offset, ) + assert isinstance( + pj, WorkflowPredictSingleSiteJob + ), "Expected WorkflowPredictSingleSiteJob to be returned" + return pj def get_prediction_results( @@ -236,4 +270,4 @@ def get_single_site_prediction_results( response = session.get(endpoint, params=params) # get results to assemble into list - return WorkflowPredictSingleSiteJob.model_validate(response) + return WorkflowPredictSingleSiteJob.model_validate(response.json()) diff --git a/openprotein/api/predictor.py b/openprotein/api/predictor.py index 364159a..53e2459 100644 --- a/openprotein/api/predictor.py +++ b/openprotein/api/predictor.py @@ -3,6 +3,7 @@ import numpy as np import pandas as pd from openprotein.base import APISession +from openprotein.csv import csv_stream from openprotein.schemas import ( CVJob, Job, @@ -149,6 +150,21 @@ def predictor_predict_post( return PredictJob.model_validate(response.json()) +def predictor_predict_multi_post( + session: APISession, predictor_ids: list[str], sequences: list[bytes] | list[str] +): + endpoint = PATH_PREFIX + f"/predict" + + sequences_unicode = [(s if isinstance(s, str) else s.decode()) for s in sequences] + body = { + "model_ids": predictor_ids, + "sequences": sequences_unicode, + } + response = session.post(endpoint, json=body) + + return PredictMultiJob.model_validate(response.json()) + + def predictor_predict_single_site_post( session: APISession, predictor_id: str, @@ -167,6 +183,25 @@ def predictor_predict_single_site_post( return PredictSingleSiteJob.model_validate(response.json()) +def predictor_predict_multi_single_site_post( + session: APISession, + predictor_ids: list[str], + base_sequence: bytes | str, +): + endpoint = PATH_PREFIX + f"/predict_single_site" + + base_sequence = ( + base_sequence.decode() if isinstance(base_sequence, bytes) else base_sequence + ) + body = { + "model_ids": predictor_ids, + "base_sequence": base_sequence, + } + response = session.post(endpoint, json=body) + + return PredictMultiSingleSiteJob.model_validate(response.json()) + + def predictor_predict_get_sequences( session: APISession, prediction_job_id: str ) -> list[bytes]: @@ -189,7 +224,7 @@ def predictor_predict_get_sequence_result( job_id : str job ID to retrieve results from sequence from: bytes - sequence to retrieve results for + sequence to retrieve predictions for Returns ------- diff --git a/openprotein/api/svd.py b/openprotein/api/svd.py index f739c38..1d64b02 100644 --- a/openprotein/api/svd.py +++ b/openprotein/api/svd.py @@ -3,7 +3,7 @@ import numpy as np from openprotein.base import APISession from openprotein.errors import InvalidParameterError -from openprotein.schemas import FitJob, SVDEmbeddingsJob, SVDMetadata +from openprotein.schemas import SVDEmbeddingsJob, SVDFitJob, SVDMetadata from pydantic import TypeAdapter PATH_PREFIX = "v1/embeddings/svd" @@ -112,7 +112,7 @@ def svd_fit_post( n_components: int = 1024, reduction: str | None = None, prompt_id: str | None = None, -) -> FitJob: +) -> SVDFitJob: """ Create SVD fit job. @@ -160,7 +160,7 @@ def svd_fit_post( response = session.post(endpoint, json=body) # return job for metadata - return FitJob.model_validate(response.json()) + return SVDFitJob.model_validate(response.json()) def svd_embed_post( diff --git a/openprotein/api/umap.py b/openprotein/api/umap.py new file mode 100644 index 0000000..3aef3d5 --- /dev/null +++ b/openprotein/api/umap.py @@ -0,0 +1,247 @@ +import io + +import numpy as np +import pandas as pd +from openprotein.base import APISession +from openprotein.errors import InvalidParameterError +from openprotein.schemas import FeatureType, UMAPEmbeddingsJob, UMAPFitJob, UMAPMetadata +from pydantic import TypeAdapter + +PATH_PREFIX = "v1/umap" + + +def umap_list_get(session: APISession) -> list[UMAPMetadata]: + """Get UMAP job metadata for all UMAPs. Including UMAP dimension and sequence lengths.""" + endpoint = PATH_PREFIX + response = session.get(endpoint) + return TypeAdapter(list[UMAPMetadata]).validate_python(response.json()) + + +def umap_get(session: APISession, umap_id: str) -> UMAPMetadata: + """Get UMAP job metadata. Including UMAP dimension and sequence lengths.""" + endpoint = PATH_PREFIX + f"/{umap_id}" + response = session.get(endpoint) + return UMAPMetadata(**response.json()) + + +def umap_get_sequences(session: APISession, umap_id: str) -> list[bytes]: + """ + Get sequences used to fit an UMAP. + + Parameters + ---------- + session : APISession + Session object for API communication. + umap_id : str + UMAP ID whose sequences to fetch + + Returns + ------- + sequences : List[bytes] + """ + endpoint = PATH_PREFIX + f"/{umap_id}/sequences" + response = session.get(endpoint) + return TypeAdapter(list[bytes]).validate_python(response.json()) + + +def embed_get_sequence_result( + session: APISession, job_id: str, sequence: str | bytes +) -> bytes: + """ + Get encoded umap embeddings result for a sequence from the request ID. + + Parameters + ---------- + session : APISession + Session object for API communication. + job_id : str + job ID to retrieve results from + sequence : bytes + sequence to retrieve results for + + Returns + ------- + result : bytes + """ + if isinstance(sequence, bytes): + sequence = sequence.decode() + endpoint = PATH_PREFIX + f"/embed/{job_id}/{sequence}" + response = session.get(endpoint) + return response.content + + +def embed_get_batch_result(session: APISession, job_id: str) -> bytes: + """ + Get encoded umap embeddings batched result from the request ID. + + Parameters + ---------- + session : APISession + Session object for API communication. + job_id : str + Job ID to retrieve results from + + Returns + ------- + result : bytes + """ + endpoint = PATH_PREFIX + f"/embed/{job_id}/csv" + response = session.get(endpoint) + return response.content + + +def embed_decode(data: bytes) -> np.ndarray: + """ + Decode embedding. + + Parameters + ---------- + data (bytes): raw bytes encoding the array received over the API + + Returns + ------- + np.ndarray: decoded array + """ + s = io.BytesIO(data) + return np.load(s, allow_pickle=False) + + +def embed_batch_decode(data: bytes) -> np.ndarray: + """ + Decode prediction scores. + + Args: + data (bytes): raw bytes encoding the array received over the API + batched (bool): whether or not the result was batched. affects the retrieved csv format whether they contain additional columns and header rows. + + Returns: + mus (np.ndarray): decoded array of means + vars (np.ndarray): decoded array of variances + """ + s = io.BytesIO(data) + # should contain header and sequence column + df = pd.read_csv(s) + umaps = df.iloc[:, 1:].values + return umaps + + +def umap_delete(session: APISession, umap_id: str) -> bool: + """ + Delete and UMAP model. + + Parameters + ---------- + session : APISession + Session object for API communication. + umap_id : str + UMAP model to delete + + Returns + ------- + bool + """ + + endpoint = PATH_PREFIX + f"/{umap_id}" + session.delete(endpoint) + return True + + +def umap_fit_post( + session: APISession, + model_id: str, + feature_type: FeatureType, + sequences: list[bytes] | list[str] | None = None, + assay_id: str | None = None, + n_components: int = 2, + n_neighbors: int = 15, + min_dist: float = 0.1, + reduction: str | None = None, + prompt_id: str | None = None, +) -> UMAPFitJob: + """ + Create UMAP fit job. + + Parameters + ---------- + session : APISession + Session object for API communication. + model_id : str + Model to use. Can be either svd_id or id of a foundational model. + feature_type: FeatureType + Type of feature to use for fitting UMAP. Either PLM or SVD. + sequences : list[bytes] | None, optional + Optional sequences to fit UMAP with. Either use sequences or assay_id. sequences is preferred. + assay_id: str | None, optional + Optional ID of assay containing sequences to fit UMAP with. Either use sequences or assay_id. Ignored if sequences are provided. + n_components: int + Number of UMAP components to fit. Defaults to 2. + n_neighbors: int + Number of neighbors to use for fitting. Defaults to 15. + min_dist: float + Minimum distance in UMAP fitting. Defaults to 0.1. + reduction : str | None + Embedding reduction to use for fitting the UMAP. Defaults to None. + + Returns + ------- + UMAPFitJob + """ + + endpoint = PATH_PREFIX + + body = { + "model_id": model_id, + "feature_type": feature_type.value, + "n_components": n_components, + "n_neighbors": n_neighbors, + "min_dist": min_dist, + } + if reduction is not None: + body["reduction"] = reduction + if prompt_id is not None: + body["prompt_id"] = prompt_id + if sequences is not None: + # both provided + if assay_id is not None: + raise InvalidParameterError("Expected only either sequences or assay_id") + sequences = [(s if isinstance(s, str) else s.decode()) for s in sequences] + body["sequences"] = sequences + else: + # both are none + if assay_id is None: + raise InvalidParameterError("Expected either sequences or assay_id") + body["assay_id"] = assay_id + + response = session.post(endpoint, json=body) + # return job for metadata + return UMAPFitJob.model_validate(response.json()) + + +def umap_embed_post( + session: APISession, umap_id: str, sequences: list[bytes] | list[str] +) -> UMAPEmbeddingsJob: + """ + POST a request for embeddings from the given UMAP model. + + Parameters + ---------- + session : APISession + Session object for API communication. + umap_id : str + UMAP model to use + sequences : List[bytes] + sequences to UMAP + + Returns + ------- + UMAPEmbeddingsJob + """ + endpoint = PATH_PREFIX + f"/{umap_id}/embed" + + sequences_unicode = [(s if isinstance(s, str) else s.decode()) for s in sequences] + body = { + "sequences": sequences_unicode, + } + response = session.post(endpoint, json=body) + + return UMAPEmbeddingsJob.model_validate(response.json()) diff --git a/openprotein/app/__init__.py b/openprotein/app/__init__.py index a27c5f6..de2aaf3 100644 --- a/openprotein/app/__init__.py +++ b/openprotein/app/__init__.py @@ -1,11 +1,20 @@ +""" +OpenProtein APIs. + +A collection of the available APIs available on the pythonic interface. +isort:skip_file +""" + from .services import ( - SVDAPI, - AlignAPI, + JobsAPI, AssayDataAPI, + TrainingAPI, DesignAPI, + AlignAPI, EmbeddingsAPI, FoldAPI, - JobsAPI, + SVDAPI, + UMAPAPI, PredictorAPI, - TrainingAPI, + DesignerAPI, ) diff --git a/openprotein/app/models/__init__.py b/openprotein/app/models/__init__.py index 11dfc4c..cc814a2 100644 --- a/openprotein/app/models/__init__.py +++ b/openprotein/app/models/__init__.py @@ -1,9 +1,22 @@ -"""OpenProtein app-level models providing service-level functionality.""" +""" +OpenProtein app-level models providing service-level functionality. + +isort:skip_file +""" -from .align import MSAFuture, PromptFuture from .assaydata import AssayDataPage, AssayDataset, AssayMetadata + +# workflow system +from .futures import Future, MappedFuture, StreamingFuture +from .train import CVFuture, TrainFuture +from .predict import PredictionResultFuture as WorkflowPredictionResultFuture +from .design import DesignFuture as WorkflowDesignFuture + +# poet system +from .align import MSAFuture, PromptFuture from .deprecated.poet import PoetGenerateFuture, PoetScoreFuture, PoetSingleSiteFuture -from .design import DesignFuture + +# distributed system from .embeddings import ( EmbeddingModel, EmbeddingResultFuture, @@ -12,9 +25,8 @@ OpenProteinModel, PoETModel, ) +from .svd import SVDModel +from .umap import UMAPModel from .fold import AlphaFold2Model, ESMFoldModel, FoldModel, FoldResultFuture -from .futures import Future, MappedFuture, StreamingFuture -from .predict import PredictFuture from .predictor import PredictionResultFuture, PredictorModel -from .svd import SVDModel -from .train import CVFuture, TrainFuture +from .designer import DesignFuture diff --git a/openprotein/app/models/deprecated/poet.py b/openprotein/app/models/deprecated/poet.py index 8c73c24..0924ec7 100644 --- a/openprotein/app/models/deprecated/poet.py +++ b/openprotein/app/models/deprecated/poet.py @@ -2,10 +2,11 @@ import numpy as np from openprotein import config -from openprotein.api import align, poet +from openprotein.api.deprecated import poet from openprotein.base import APISession +from openprotein.csv import csv_stream from openprotein.errors import APIError -from openprotein.schemas import ( +from openprotein.schemas.deprecated.poet import ( PoetGenerateJob, PoetScoreJob, PoetScoreResult, @@ -167,7 +168,7 @@ def stream(self) -> Iterator[PoetScoreResult]: """ try: response = poet.poet_generate_get(self.session, self.job.job_id) - for tokens in align.csv_stream(response): + for tokens in csv_stream(response): try: name, sequence = tokens[:2] score = [float(s) for s in tokens[2:]] diff --git a/openprotein/app/models/design.py b/openprotein/app/models/design.py index 0b8bacd..62ef5d8 100644 --- a/openprotein/app/models/design.py +++ b/openprotein/app/models/design.py @@ -1,6 +1,6 @@ from openprotein.api import design from openprotein.base import APISession -from openprotein.schemas import DesignJob, DesignResults, DesignStep +from openprotein.schemas import WorkflowDesign, WorkflowDesignJob from .futures import Future, PagedFuture @@ -8,9 +8,11 @@ class DesignFuture(PagedFuture, Future): """Future Job for manipulating results""" - job: DesignJob + job: WorkflowDesignJob - def __init__(self, session: APISession, job: DesignJob, page_size: int = 1000): + def __init__( + self, session: APISession, job: WorkflowDesignJob, page_size: int = 1000 + ): super().__init__(session, job) self.page_size = page_size @@ -20,12 +22,7 @@ def __str__(self) -> str: def __repr__(self) -> str: return repr(self.job) - def _fmt_results( - self, results: DesignResults - ) -> ( - # list[dict] - list[DesignStep] - ): + def _fmt_results(self, results: WorkflowDesign): # return [i.model_dump() for i in results.result] return results.result @@ -76,7 +73,7 @@ def get_results( step: int | None = None, page_size: int | None = None, page_offset: int | None = None, - ) -> DesignResults: + ) -> WorkflowDesign: """ Retrieves the results of a Design job. @@ -91,7 +88,7 @@ def get_results( Returns ------- - DesignJob + WorkflowDesignJob The job object representing the Design job. Raises diff --git a/openprotein/app/models/designer.py b/openprotein/app/models/designer.py new file mode 100644 index 0000000..9cc6d2b --- /dev/null +++ b/openprotein/app/models/designer.py @@ -0,0 +1,121 @@ +from typing import Generator + +from openprotein.api import assaydata, designer +from openprotein.api import job as job_api +from openprotein.base import APISession +from openprotein.schemas import Criteria, Design, DesignJob +from openprotein.schemas.designer import DesignAlgorithm + +from .assaydata import AssayDataset +from .futures import Future, StreamingFuture + + +class DesignFuture(StreamingFuture, Future): + """Future Job for manipulating results""" + + job: DesignJob + + def __init__( + self, + session: APISession, + job: DesignJob | None = None, + metadata: Design | None = None, + ): + """Initializes with either job get or design get.""" + self._design_assay = None + if metadata is None: + if job is None: + raise ValueError("Expected design metadata or job") + metadata = designer.design_get(session=session, design_id=job.job_id) + self._metadata = metadata + if job is None: + job = DesignJob.create(job_api.job_get(session=session, job_id=metadata.id)) + super().__init__(session, job) + + @property + def id(self): + return self._metadata.id + + @property + def assay(self) -> AssayDataset: + if self._design_assay is None: + self._design_assay = self.get_assay() + return self._design_assay + + @property + def algorithm(self) -> DesignAlgorithm: + return self._metadata.algorithm + + @property + def criteria(self) -> Criteria: + return self._metadata.criteria + + @property + def num_steps(self): + return self._metadata.num_steps + + @property + def num_rows(self): + return self._metadata.num_rows + + @property + def allowed_tokens(self) -> dict[str, list[str]] | None: + return self._metadata.allowed_tokens + + @property + def pop_size(self) -> int: + return self._metadata.pop_size + + @property + def n_offsprings(self) -> int: + return self._metadata.n_offsprings + + @property + def crossover_prob(self) -> float: + return self._metadata.crossover_prob + + @property + def crossover_prob_pointwise(self) -> float: + return self._metadata.crossover_prob_pointwise + + @property + def mutation_average_mutations_per_seq(self) -> int: + return self._metadata.mutation_average_mutations_per_seq + + @property + def metadata(self): + self._refresh_metadata() + return self._metadata + + def _refresh_metadata(self): + if not self._metadata.is_done(): + self._metadata = designer.design_get( + session=self.session, design_id=self._metadata.id + ) + + def delete(self) -> bool: + """ + Delete this design. + """ + return designer.design_delete(session=self.session, design_id=self.id) + + def stream(self) -> Generator: + stream = designer.designer_get_design_results( + session=self.session, design_id=self.id + ) + return designer.decode_design_results_stream(data=stream) + + def get_assay(self) -> AssayDataset: + """ + Get assay used for design job. + + Returns + ------- + AssayDataset: Assay dataset used for design. + """ + return AssayDataset( + session=self.session, + metadata=assaydata.get_assay_metadata( + self.session, self._metadata.assay_id + ), + ) diff --git a/openprotein/app/models/embeddings/base.py b/openprotein/app/models/embeddings/base.py index 65121e7..81cd9fa 100644 --- a/openprotein/app/models/embeddings/base.py +++ b/openprotein/app/models/embeddings/base.py @@ -1,6 +1,6 @@ from typing import TYPE_CHECKING -from openprotein.api import assaydata, embedding, predictor, svd +from openprotein.api import assaydata, embedding, predictor, svd, umap from openprotein.base import APISession from openprotein.errors import InvalidParameterError from openprotein.schemas import FeatureType, ModelMetadata, ReductionType @@ -11,6 +11,7 @@ if TYPE_CHECKING: from ..predictor import PredictorModel from ..svd import SVDModel + from ..umap import UMAPModel class EmbeddingModel: @@ -222,6 +223,58 @@ def fit_svd( ) return SVDModel.create(session=self.session, job=job) + def fit_umap( + self, + sequences: list[bytes] | list[str] | None = None, + assay: AssayDataset | None = None, + n_components: int = 2, + reduction: ReductionType | None = ReductionType.MEAN, + **kwargs, + ) -> "UMAPModel": + """ + Fit an UMAP on the embedding results of this model. + + This function will create an UMAPModel based on the embeddings from this model \ + as well as the hyperparameters specified in the args. + + Parameters + ---------- + sequences : list[bytes] | None + Optional sequences to fit UMAP with. Either use sequences or assay. sequences is preferred. + assay: AssayDataset | None + Optional assay containing sequences to fit UMAP with. Either use sequences or assay. Ignored if sequences are provided. + n_components: int + Number of components in UMAP fit. Will determine output shapes. Defaults to 2. + reduction: ReductionType | None + Embeddings reduction to use (e.g. mean). Defaults to MEAN. + + Returns + ------- + UMAPModel + """ + # local import for cyclic dep + from ..umap import UMAPModel + + # Ensure either or + if (assay is None and sequences is None) or ( + assay is not None and sequences is not None + ): + raise InvalidParameterError( + "Expected either assay or sequences to fit UMAP on!" + ) + model_id = self.id + job = umap.umap_fit_post( + session=self.session, + model_id=model_id, + feature_type=FeatureType.PLM, + sequences=sequences, + assay_id=assay.id if assay is not None else None, + n_components=n_components, + reduction=reduction, + **kwargs, + ) + return UMAPModel.create(session=self.session, job=job) + def fit_gp( self, assay: AssayMetadata | AssayDataset | str, diff --git a/openprotein/app/models/embeddings/future.py b/openprotein/app/models/embeddings/future.py index f032ab9..ae60f05 100644 --- a/openprotein/app/models/embeddings/future.py +++ b/openprotein/app/models/embeddings/future.py @@ -34,7 +34,12 @@ def __init__( max_workers: int = config.MAX_CONCURRENT_WORKERS, ): super().__init__(session=session, job=job, max_workers=max_workers) - self._sequences = sequences + # ensure all list[bytes] + self._sequences = ( + [seq.encode() if isinstance(seq, str) else seq for seq in sequences] + if sequences is not None + else sequences + ) def get(self, verbose=False) -> list: return super().get(verbose=verbose) @@ -43,7 +48,7 @@ def get(self, verbose=False) -> list: def sequences(self) -> list[bytes] | list[str]: if self._sequences is None: self._sequences = embedding.get_request_sequences( - self.session, self.job.job_id + session=self.session, job_id=self.job.job_id, job_type=self.job.job_type ) return self._sequences @@ -65,7 +70,10 @@ def get_item(self, sequence: bytes) -> np.ndarray: np.ndarray: embeddings """ data = embedding.request_get_sequence_result( - self.session, self.job.job_id, sequence + session=self.session, + job_id=self.job.job_id, + sequence=sequence, + job_type=self.job.job_type, ) return embedding.result_decode(data) diff --git a/openprotein/app/models/embeddings/poet.py b/openprotein/app/models/embeddings/poet.py index 43a68ad..e16c383 100644 --- a/openprotein/app/models/embeddings/poet.py +++ b/openprotein/app/models/embeddings/poet.py @@ -1,25 +1,25 @@ from typing import TYPE_CHECKING -from openprotein.api import embedding, poet +from openprotein.api import embedding +from openprotein.api.deprecated import poet from openprotein.base import APISession -from openprotein.schemas import ( - ModelMetadata, +from openprotein.schemas import ModelMetadata, ReductionType +from openprotein.schemas.deprecated.poet import ( PoetGenerateJob, PoetScoreJob, PoetSSPJob, - ReductionType, ) from ..align import PromptFuture from ..assaydata import AssayDataset, AssayMetadata from ..deprecated.poet import PoetGenerateFuture, PoetScoreFuture, PoetSingleSiteFuture -from ..futures import Future from .base import EmbeddingModel from .future import EmbeddingResultFuture, EmbeddingsScoreResultFuture if TYPE_CHECKING: from ..predictor import PredictorModel from ..svd import SVDModel + from ..umap import UMAPModel class PoETModel(EmbeddingModel): @@ -226,7 +226,7 @@ def fit_svd( reduction: ReductionType | None = None, ) -> "SVDModel": """ - Fit an SVD on the embedding results of this model. + Fit an SVD on the embedding results of PoET. This function will create an SVDModel based on the embeddings from this model \ as well as the hyperparameters specified in the args. @@ -256,6 +256,46 @@ def fit_svd( prompt_id=prompt_id, ) + def fit_umap( + self, + prompt: str | PromptFuture, + sequences: list[bytes] | list[str] | None = None, + assay: AssayDataset | None = None, + n_components: int = 2, + reduction: ReductionType | None = ReductionType.MEAN, + ) -> "UMAPModel": + """ + Fit a UMAP on assay using PoET and hyperparameters. + + This function will create a UMAP based on the embeddings from this PoET model \ + as well as the hyperparameters specified in the args. + + Parameters + ---------- + prompt: Union[str, PromptFuture] + prompt from an align workflow to condition Poet model + sequences : list[bytes] | None + Optional sequences to fit UMAP with. Either use sequences or assay. sequences is preferred. + assay: AssayDataset | None + Optional assay containing sequences to fit UMAP with. Either use sequences or assay. Ignored if sequences are provided. + n_components: int + Number of components in UMAP fit. Will determine output shapes. Defaults to 2. + reduction: ReductionType | None + Embeddings reduction to use (e.g. mean). Defaults to MEAN. + + Returns + ------- + PredictorModel + """ + prompt_id = prompt.id if isinstance(prompt, PromptFuture) else prompt + return super().fit_umap( + sequences=sequences, + assay=assay, + n_components=n_components, + reduction=reduction, + prompt_id=prompt_id, + ) + def fit_gp( self, prompt: str | PromptFuture, diff --git a/openprotein/app/models/predict.py b/openprotein/app/models/predict.py index e4411e7..1e79e7a 100644 --- a/openprotein/app/models/predict.py +++ b/openprotein/app/models/predict.py @@ -1,7 +1,9 @@ import logging +from typing import TYPE_CHECKING from openprotein.api import predict from openprotein.base import APISession +from openprotein.errors import MissingParameterError from openprotein.schemas import ( JobType, WorkflowPredictJob, @@ -10,10 +12,13 @@ from .futures import Future, PagedFuture +if TYPE_CHECKING: + from .train import TrainFuture + logger = logging.getLogger(__name__) -class PredictFuture(PagedFuture, Future): +class PredictionResultFuture(PagedFuture, Future): """Future Job for manipulating results""" job: WorkflowPredictJob | WorkflowPredictSingleSiteJob @@ -22,8 +27,23 @@ def __init__( self, session: APISession, job: WorkflowPredictJob | WorkflowPredictSingleSiteJob, + sequences: list[str] | None = None, + sequence: str | None = None, + train_job: "TrainFuture | None" = None, + model_ids: list[str] | None = None, page_size: int = 1000, ): + if job.job_id is None: + if (sequences is None and sequence is None) or ( + train_job is None and model_ids is None + ): + raise MissingParameterError( + "Expected job_id from predict job or predict params" + ) + self.sequences = sequences + self.sequence = sequence + self.train_job = train_job + self.model_ids = model_ids super().__init__(session=session, job=job, page_size=page_size) def __str__(self) -> str: @@ -166,7 +186,32 @@ def get_results( HTTPError If the GET request does not succeed. """ - assert self.id is not None + if self.id is None: + # use old caching method of resubmitting POSTs + if self.job.job_type is JobType.workflow_predict_single_site: + assert self.sequence is not None + assert self.train_job is not None or self.model_ids is not None + train_job_id = self.train_job.id if self.train_job is not None else None + return predict.create_predict_single_site( + session=self.session, + sequence=self.sequence, + train_job_id=train_job_id, + model_ids=self.model_ids, + page_size=page_size, + page_offset=page_offset, + ) + else: + assert self.sequences is not None + assert self.train_job is not None or self.model_ids is not None + train_job_id = self.train_job.id if self.train_job is not None else None + return predict.create_predict_job( + session=self.session, + sequences=self.sequences, + train_job_id=train_job_id, + model_ids=self.model_ids, + page_size=page_size, + page_offset=page_offset, + ) if self.job.job_type is JobType.workflow_predict_single_site: return predict.get_single_site_prediction_results( session=self.session, diff --git a/openprotein/app/models/predictor/predictor.py b/openprotein/app/models/predictor/predictor.py index e919e94..a390d81 100644 --- a/openprotein/app/models/predictor/predictor.py +++ b/openprotein/app/models/predictor/predictor.py @@ -1,47 +1,16 @@ -import numpy as np from openprotein.api import assaydata from openprotein.api import job as job_api from openprotein.api import predictor, svd from openprotein.base import APISession from openprotein.errors import InvalidParameterError -from openprotein.schemas import CVJob, PredictorMetadata, TrainJob +from openprotein.schemas import Criterion, ModelCriterion, PredictorMetadata, TrainJob from ..assaydata import AssayDataset from ..embeddings import EmbeddingModel from ..futures import Future from ..svd import SVDModel from .predict import PredictionResultFuture - - -class CVResultFuture(Future): - """Future Job for manipulating results""" - - job: CVJob - - def __init__( - self, - session: APISession, - job: CVJob, - ): - super().__init__(session, job) - - @property - def id(self): - return self.job.job_id - - def get(self, verbose: bool = False) -> tuple[np.ndarray, np.ndarray, np.ndarray]: - """ - Get embedding results for specified sequence. - - Args: - sequence (bytes): sequence to fetch results for - - Returns: - mu (np.ndarray): means of predictions - var (np.ndarray): variances of predictions - """ - data = predictor.predictor_crossvalidate_get(self.session, self.job.job_id) - return predictor.decode_crossvalidate(data) +from .validate import CVResultFuture class PredictorModel(Future): @@ -59,7 +28,7 @@ def __init__( job: TrainJob | None = None, metadata: PredictorMetadata | None = None, ): - """Initializes with either job get or svd metadata get.""" + """Initializes with either job get or predictor get.""" self._training_assay = None # initialize the metadata if metadata is None: @@ -69,7 +38,6 @@ def __init__( self._metadata = metadata if job is None: job = TrainJob.create(job_api.job_get(session=session, job_id=metadata.id)) - # getter initializes job if not provided super().__init__(session, job) def __str__(self) -> str: @@ -78,6 +46,54 @@ def __str__(self) -> str: def __repr__(self) -> str: return repr(self.metadata) + def __or__(self, model: "PredictorModel") -> "PredictorModelGroup": + if self.sequence_length is not None: + if model.sequence_length != self.sequence_length: + raise ValueError( + "Expected sequence lengths to either match or be None." + ) + return PredictorModelGroup( + session=self.session, + models=[self, model], + sequence_length=self.sequence_length or model.sequence_length, + check_sequence_length=False, + ) + + def __lt__(self, target: float) -> ModelCriterion: + if len(self.training_properties) == 1: + return ModelCriterion( + model_id=self.id, + measurement_name=self.training_properties[0], + criterion=ModelCriterion.Criterion( + target=target, direction=ModelCriterion.Criterion.DirectionEnum.lt + ), + ) + raise self.InvalidMultitaskModelToCriterion() + + def __gt__(self, target: float) -> ModelCriterion: + if len(self.training_properties) == 1: + return ModelCriterion( + model_id=self.id, + measurement_name=self.training_properties[0], + criterion=ModelCriterion.Criterion( + target=target, direction=ModelCriterion.Criterion.DirectionEnum.gt + ), + ) + raise self.InvalidMultitaskModelToCriterion() + + def __eq__(self, target: float) -> ModelCriterion: + if len(self.training_properties) == 1: + return ModelCriterion( + model_id=self.id, + measurement_name=self.training_properties[0], + criterion=ModelCriterion.Criterion( + target=target, direction=ModelCriterion.Criterion.DirectionEnum.eq + ), + ) + raise self.InvalidMultitaskModelToCriterion() + + class InvalidMultitaskModelToCriterion(Exception): ... + @property def id(self): return self._metadata.id @@ -134,7 +150,7 @@ def model(self) -> EmbeddingModel | SVDModel | None: def delete(self) -> bool: """ - Delete this SVD model. + Delete this predictor model. """ return predictor.predictor_delete(self.session, self.id) @@ -148,7 +164,7 @@ def get_assay(self) -> AssayDataset: Returns ------- - list[bytes]: list of sequences + AssayDataset: Assay dataset used for train job. """ return AssayDataset( session=self.session, @@ -170,10 +186,11 @@ def crossvalidate(self, n_splits: int | None = None) -> CVResultFuture: def predict(self, sequences: list[bytes] | list[str]) -> PredictionResultFuture: if self.sequence_length is not None: for sequence in sequences: + # convert to string to check token length sequence = sequence if isinstance(sequence, str) else sequence.decode() if len(sequence) != self.sequence_length: raise InvalidParameterError( - f"Expected sequence to predict to be of length {self.sequence_length}" + f"Expected sequences to predict to be of length {self.sequence_length}" ) return PredictionResultFuture.create( session=self.session, @@ -184,7 +201,94 @@ def predict(self, sequences: list[bytes] | list[str]) -> PredictionResultFuture: def single_site(self, sequence: bytes | str) -> PredictionResultFuture: if self.sequence_length is not None: - if len(sequence) != self.sequence_length: + # convert to string to check token length + seq = sequence if isinstance(sequence, str) else sequence.decode() + if len(seq) != self.sequence_length: + raise InvalidParameterError( + f"Expected sequence to predict to be of length {self.sequence_length}" + ) + return PredictionResultFuture.create( + session=self.session, + job=predictor.predictor_predict_single_site_post( + session=self.session, predictor_id=self.id, base_sequence=sequence + ), + ) + + +class PredictorModelGroup(Future): + """ + Class providing predict endpoint for fitted predictor models. + + Also implements a Future that waits for train job. + """ + + __models__: list[PredictorModel] + + def __init__( + self, + session: APISession, + models: list[PredictorModel], + sequence_length: int | None = None, + check_sequence_length: bool = True, # turn off checking - prevent n^2 operation when chaining many + ): + if len(models) == 0: + raise ValueError("Expected at least one model to group") + # calculate and check sequence length compatibility + if check_sequence_length: + for m in models: + if m.sequence_length is not None: + if sequence_length is None: + sequence_length = m.sequence_length + elif sequence_length != m.sequence_length: + raise ValueError( + "Expected sequence lengths of all models to either match or be None." + ) + self.sequence_length = sequence_length + self.session = session + self.__models__ = models + + def __str__(self) -> str: + return repr(self.__models__) + + def __repr__(self) -> str: + return repr(self.__models__) + + def __or__(self, model: PredictorModel) -> "PredictorModelGroup": + if self.sequence_length is not None: + if model.sequence_length != self.sequence_length: + raise ValueError( + "Expected sequence lengths to either match or be None." + ) + return PredictorModelGroup( + session=self.session, + models=self.__models__ + [model], + sequence_length=self.sequence_length or model.sequence_length, + check_sequence_length=False, + ) + + def predict(self, sequences: list[bytes] | list[str]) -> PredictionResultFuture: + if self.sequence_length is not None: + for sequence in sequences: + # convert to string to check token length + sequence = sequence if isinstance(sequence, str) else sequence.decode() + if len(sequence) != self.sequence_length: + raise InvalidParameterError( + f"Expected sequences to predict to be of length {self.sequence_length}" + ) + return PredictionResultFuture.create( + session=self.session, + job=predictor.predictor_predict_multi_post( + session=self.session, + predictor_ids=[m.id for m in self.__models__], + sequences=sequences, + ), + ) + + def single_site(self, sequence: bytes | str) -> PredictionResultFuture: + if self.sequence_length is not None: + # convert to string to check token length + seq = sequence if isinstance(sequence, str) else sequence.decode() + if len(seq) != self.sequence_length: raise InvalidParameterError( f"Expected sequence to predict to be of length {self.sequence_length}" ) @@ -194,3 +298,7 @@ def single_site(self, sequence: bytes | str) -> PredictionResultFuture: session=self.session, predictor_id=self.id, base_sequence=sequence ), ) + + def get(self, verbose: bool = False): + # overload for Future + return self diff --git a/openprotein/app/models/predictor/validate.py b/openprotein/app/models/predictor/validate.py new file mode 100644 index 0000000..c463dd0 --- /dev/null +++ b/openprotein/app/models/predictor/validate.py @@ -0,0 +1,37 @@ +import numpy as np +from openprotein.api import predictor +from openprotein.base import APISession +from openprotein.schemas import CVJob + +from ..futures import Future + + +class CVResultFuture(Future): + """Future Job for manipulating results""" + + job: CVJob + + def __init__( + self, + session: APISession, + job: CVJob, + ): + super().__init__(session, job) + + @property + def id(self): + return self.job.job_id + + def get(self, verbose: bool = False) -> tuple[np.ndarray, np.ndarray, np.ndarray]: + """ + Get embedding results for specified sequence. + + Args: + sequence (bytes): sequence to fetch results for + + Returns: + mu (np.ndarray): means of predictions + var (np.ndarray): variances of predictions + """ + data = predictor.predictor_crossvalidate_get(self.session, self.job.job_id) + return predictor.decode_crossvalidate(data) diff --git a/openprotein/app/models/svd.py b/openprotein/app/models/svd.py index 70ce442..60f7bb1 100644 --- a/openprotein/app/models/svd.py +++ b/openprotein/app/models/svd.py @@ -3,10 +3,10 @@ import numpy as np from openprotein.api import assaydata from openprotein.api import job as job_api -from openprotein.api import predictor, svd +from openprotein.api import predictor, svd, umap from openprotein.base import APISession from openprotein.errors import InvalidParameterError -from openprotein.schemas import FeatureType, FitJob, SVDEmbeddingsJob, SVDMetadata +from openprotein.schemas import FeatureType, SVDEmbeddingsJob, SVDFitJob, SVDMetadata from .assaydata import AssayDataset, AssayMetadata from .embeddings import EmbeddingModel, EmbeddingResultFuture @@ -14,6 +14,7 @@ if TYPE_CHECKING: from .predictor import PredictorModel + from .umap import UMAPModel class SVDModel(Future): @@ -23,12 +24,12 @@ class SVDModel(Future): Implements a Future to allow waiting for a fit job. """ - job: FitJob + job: SVDFitJob def __init__( self, session: APISession, - job: FitJob | None = None, + job: SVDFitJob | None = None, metadata: SVDMetadata | None = None, ): """Initializes with either job get or svd metadata get.""" @@ -39,7 +40,7 @@ def __init__( metadata = svd.svd_get(session, job.job_id) self._metadata = metadata if job is None: - job = FitJob.create(job_api.job_get(session=session, job_id=metadata.id)) + job = SVDFitJob.create(job_api.job_get(session=session, job_id=metadata.id)) # getter initializes job if not provided super().__init__(session, job) @@ -127,6 +128,54 @@ def embed( sequences=sequences, ) + def fit_umap( + self, + sequences: list[bytes] | list[str] | None = None, + assay: AssayDataset | None = None, + n_components: int = 2, + **kwargs, + ) -> "UMAPModel": + """ + Fit an UMAP on the embedding results of this model. + + This function will create an UMAPModel based on the embeddings from this model \ + as well as the hyperparameters specified in the args. + + Parameters + ---------- + sequences : List[bytes] + sequences to UMAP + n_components: int + number of components in UMAP. Will determine output shapes + reduction: ReductionType | None + embeddings reduction to use (e.g. mean) + + Returns + ------- + UMAPModel + """ + # local import for cyclic dep + from .umap import UMAPModel + + # Ensure either or + if (assay is None and sequences is None) or ( + assay is not None and sequences is not None + ): + raise InvalidParameterError( + "Expected either assay or sequences to fit UMAP on!" + ) + model_id = self.id + job = umap.umap_fit_post( + session=self.session, + model_id=model_id, + feature_type=FeatureType.SVD, + sequences=sequences, + assay_id=assay.id if assay is not None else None, + n_components=n_components, + **kwargs, + ) + return UMAPModel.create(session=self.session, job=job) + def fit_gp( self, assay: AssayMetadata | AssayDataset | str, diff --git a/openprotein/app/models/train.py b/openprotein/app/models/train.py index fa5f867..da3c598 100644 --- a/openprotein/app/models/train.py +++ b/openprotein/app/models/train.py @@ -12,7 +12,7 @@ from .assaydata import AssayDataset from .futures import Future, PagedFuture -from .predict import PredictFuture +from .predict import PredictionResultFuture class TrainFuture(Future): @@ -159,7 +159,7 @@ def list_models(self): def predict( self, sequences: list[str], model_ids: list[str] | None = None - ) -> PredictFuture: + ) -> PredictionResultFuture: """ Creates a predict job based on the training job. @@ -183,7 +183,7 @@ def predict_single_site( self, sequence: str, model_ids: list[str] | None = None, - ) -> PredictFuture: + ) -> PredictionResultFuture: """ Creates a new Predict job for single site mutation analysis with a trained model. diff --git a/openprotein/app/models/umap.py b/openprotein/app/models/umap.py new file mode 100644 index 0000000..678dc30 --- /dev/null +++ b/openprotein/app/models/umap.py @@ -0,0 +1,156 @@ +import numpy as np +from openprotein.api import job as job_api +from openprotein.api import umap +from openprotein.base import APISession +from openprotein.schemas import UMAPEmbeddingsJob, UMAPFitJob, UMAPMetadata + +from .embeddings import EmbeddingModel, EmbeddingResultFuture +from .futures import Future + + +class UMAPModel(Future): + """ + Class providing embedding endpoint for UMAP models. \ + Also allows retrieving embeddings of sequences used to fit the UMAP with `get`. + Implements a Future to allow waiting for a fit job. + """ + + job: UMAPFitJob + + def __init__( + self, + session: APISession, + job: UMAPFitJob | None = None, + metadata: UMAPMetadata | None = None, + ): + """Initializes with either job get or umap metadata get.""" + if metadata is None: + # use job to fetch metadata + if job is None: + raise ValueError("Expected umap metadata or job") + metadata = umap.umap_get(session, job.job_id) + self._metadata = metadata + if job is None: + job = UMAPFitJob.create( + job_api.job_get(session=session, job_id=metadata.id) + ) + self._sequences = None + self._embeddings = None + # getter initializes job if not provided + super().__init__(session, job) + + def __str__(self) -> str: + return str(self.metadata) + + def __repr__(self) -> str: + return repr(self.metadata) + + @property + def id(self): + return self._metadata.id + + @property + def n_components(self): + return self._metadata.n_components + + @property + def n_neighbors(self): + return self._metadata.n_neighbors + + @property + def min_dist(self): + return self._metadata.min_dist + + @property + def sequence_length(self): + return self._metadata.sequence_length + + @property + def reduction(self): + return self._metadata.reduction + + @property + def metadata(self): + self._refresh_metadata() + return self._metadata + + @property + def sequences(self): + if self._sequences is not None: + return self._sequences + self._sequences = self.get_inputs() + return self._sequences + + @property + def embeddings(self): + if self._embeddings is not None: + return self._embeddings + data = umap.embed_get_batch_result(session=self.session, job_id=self.id) + embeddings = [ + (seq, umap) + for seq, umap in zip(self.sequences, umap.embed_batch_decode(data)) + ] + self._embeddings = embeddings + return self._embeddings + + def _refresh_metadata(self): + if not self._metadata.is_done(): + self._metadata = umap.umap_get(self.session, self._metadata.id) + + def get_model(self) -> EmbeddingModel: + """Fetch embeddings model""" + model = EmbeddingModel.create(session=self.session, model_id=self._metadata.id) + return model + + @property + def model(self) -> EmbeddingModel: + return self.get_model() + + def delete(self) -> bool: + """ + Delete this UMAP model. + """ + return umap.umap_delete(self.session, self.id) + + def get(self, verbose: bool = False): + return self.embeddings + + def get_inputs(self) -> list[bytes]: + """ + Get sequences used for umap job. + + Returns + ------- + List[bytes]: list of sequences + """ + return umap.umap_get_sequences(session=self.session, umap_id=self.id) + + def embed( + self, sequences: list[bytes] | list[str], **kwargs + ) -> EmbeddingResultFuture: + """ + Use this UMAP model to get reduced embeddings from input sequences. + + Parameters + ---------- + sequences : List[bytes] + List of protein sequences. + + Returns + ------- + EmbeddingResultFuture + Class for further job manipulation. + """ + return EmbeddingResultFuture.create( + session=self.session, + job=umap.umap_embed_post( + session=self.session, umap_id=self.id, sequences=sequences, **kwargs + ), + sequences=sequences, + ) + + +class UMAPEmbeddingResultFuture(EmbeddingResultFuture, Future): + """Future for manipulating results for embeddings-related requests.""" + + job: UMAPEmbeddingsJob diff --git a/openprotein/app/services/__init__.py b/openprotein/app/services/__init__.py index 87548af..cc473b8 100644 --- a/openprotein/app/services/__init__.py +++ b/openprotein/app/services/__init__.py @@ -1,11 +1,17 @@ -"""Application services for OpenProtein.""" +""" +Application services for OpenProtein. + +isort:skip_file +""" -from .align import AlignAPI from .assaydata import AssayDataAPI +from .train import TrainingAPI from .design import DesignAPI +from .align import AlignAPI from .embeddings import EmbeddingsAPI from .fold import FoldAPI from .job import JobsAPI -from .predictor import PredictorAPI from .svd import SVDAPI -from .train import TrainingAPI +from .umap import UMAPAPI +from .predictor import PredictorAPI +from .designer import DesignerAPI diff --git a/openprotein/app/services/design.py b/openprotein/app/services/design.py index da3a35f..de17e10 100644 --- a/openprotein/app/services/design.py +++ b/openprotein/app/services/design.py @@ -1,7 +1,7 @@ from openprotein.api import design -from openprotein.app.models import DesignFuture +from openprotein.app.models import WorkflowDesignFuture from openprotein.base import APISession -from openprotein.schemas import DesignJobCreate, DesignResults +from openprotein.schemas import WorkflowDesign, WorkflowDesignJobCreate class DesignAPI: @@ -10,7 +10,9 @@ class DesignAPI: def __init__(self, session: APISession): self.session = session - def create_design_job(self, design_job: DesignJobCreate) -> DesignFuture: + def create_design_job( + self, design_job: WorkflowDesignJobCreate + ) -> WorkflowDesignFuture: """ Start a protein design job based on your assaydata, a trained ML model and Criteria (specified here). @@ -19,7 +21,7 @@ def create_design_job(self, design_job: DesignJobCreate) -> DesignFuture: design_job : DesignJobCreate The details of the design job to be created, with the following parameters: - assay_id: The ID for the assay. - - criteria: A list of CriterionItem lists for evaluating the design. + - criteria: Criteria for evaluating the design. - num_steps: The number of steps in the genetic algo. Default is 8. - pop_size: The population size for the genetic algo. Default is None. - n_offsprings: The number of offspring for the genetic algo. Default is None. @@ -33,7 +35,7 @@ def create_design_job(self, design_job: DesignJobCreate) -> DesignFuture: DesignFuture The created job as a DesignFuture instance. """ - return DesignFuture.create( + return WorkflowDesignFuture.create( session=self.session, job=design.create_design_job(self.session, design_job) ) @@ -43,7 +45,7 @@ def get_design_results( step: int | None = None, page_size: int | None = None, page_offset: int | None = None, - ) -> DesignResults: + ) -> WorkflowDesign: """ Retrieves the results of a Design job. @@ -60,7 +62,7 @@ def get_design_results( Returns ------- - DesignJob + WorkflowDesignJob The job object representing the Design job. Raises diff --git a/openprotein/app/services/designer.py b/openprotein/app/services/designer.py new file mode 100644 index 0000000..9f7a8be --- /dev/null +++ b/openprotein/app/services/designer.py @@ -0,0 +1,73 @@ +from openprotein.api import designer +from openprotein.app.models import AssayDataset, DesignFuture +from openprotein.base import APISession +from openprotein.schemas import Criteria, Criterion, DesignConstraint, Subcriterion + + +class DesignerAPI: + """interface for calling Designer endpoints""" + + def __init__(self, session: APISession): + self.session = session + + def list_designs(self) -> list[DesignFuture]: + return [ + DesignFuture(session=self.session, metadata=m) + for m in designer.designs_list(session=self.session) + ] + + def get_design(self, design_id: str) -> DesignFuture: + return DesignFuture( + session=self.session, + metadata=designer.design_get(session=self.session, design_id=design_id), + ) + + def create_genetic_algorithm_design( + self, + assay: AssayDataset, + criteria: Criteria | Subcriterion | Criterion, + num_steps: int = 25, + pop_size: int = 1024, + n_offsprings: int = 5120, + crossover_prob: float = 1.0, + crossover_prob_pointwise: float = 0.2, + mutation_average_mutations_per_seq: int = 1, + allowed_tokens: DesignConstraint | dict[int, list[str]] = {}, + ) -> DesignFuture: + """ + Start a protein design job based on your assaydata, a trained ML model and Criteria (specified here). + + Parameters + ---------- + design_job : DesignJobCreate + The details of the design job to be created, with the following parameters: + - assay_id: The ID for the assay. + - criteria: Criteria for evaluating the design. + - num_steps: The number of steps in the genetic algo. Default is 8. + - pop_size: The population size for the genetic algo. Default is None. + - n_offsprings: The number of offspring for the genetic algo. Default is None. + - crossover_prob: The crossover probability for the genetic algo. Default is None. + - crossover_prob_pointwise: The pointwise crossover probability for the genetic algo. Default is None. + - mutation_average_mutations_per_seq: The average number of mutations per sequence. Default is None. + - allowed_tokens: A dict of positions and allows tokens (e.g. *{1:['G','L']})* ) designating how mutations may occur. Default is None. + + Returns + ------- + DesignFuture + The created job as a DesignFuture instance. + """ + return DesignFuture.create( + session=self.session, + job=designer.designer_create_genetic_algorithm( + self.session, + assay_id=assay.id, + criteria=criteria, + num_steps=num_steps, + pop_size=pop_size, + n_offsprings=n_offsprings, + crossover_prob=crossover_prob, + crossover_prob_pointwise=crossover_prob_pointwise, + mutation_average_mutations_per_seq=mutation_average_mutations_per_seq, + allowed_tokens=allowed_tokens, + ), + ) diff --git a/openprotein/app/services/job.py b/openprotein/app/services/job.py index 4e431b7..07decf2 100644 --- a/openprotein/app/services/job.py +++ b/openprotein/app/services/job.py @@ -1,8 +1,10 @@ +from datetime import datetime + from openprotein import config from openprotein.api import job from openprotein.app.models import Future from openprotein.base import APISession -from openprotein.schemas import Job +from openprotein.schemas import Job, JobStatus, JobType class JobsAPI: @@ -14,9 +16,19 @@ def __init__(self, session: APISession): self.session = session def list( - self, status=None, job_type=None, assay_id=None, more_recent_than=None + self, + status: JobStatus | None = None, + job_type: JobType | None = None, + assay_id: str | None = None, + more_recent_than: datetime | str | None = None, + limit: int = 100, ) -> list[Job]: """List jobs.""" + more_recent_than_str = ( + more_recent_than.isoformat() + if isinstance(more_recent_than, datetime) + else more_recent_than + ) return [ Job.create(j) for j in job.jobs_list( @@ -24,7 +36,8 @@ def list( status=status, job_type=job_type, assay_id=assay_id, - more_recent_than=more_recent_than, + more_recent_than=more_recent_than_str, + limit=limit, ) ] diff --git a/openprotein/app/services/predict.py b/openprotein/app/services/predict.py index 1c0e908..ca88b00 100644 --- a/openprotein/app/services/predict.py +++ b/openprotein/app/services/predict.py @@ -1,7 +1,7 @@ import logging from openprotein.api import predict -from openprotein.app.models import PredictFuture, TrainFuture +from openprotein.app.models import TrainFuture, WorkflowPredictionResultFuture from openprotein.base import APISession from openprotein.errors import InvalidParameterError @@ -27,7 +27,7 @@ def create_predict_job( sequences: list[str], train_job: TrainFuture, model_ids: list[str] | None = None, - ) -> PredictFuture: + ) -> WorkflowPredictionResultFuture: """ Creates a new Predict job for a given list of sequences and a trained model. @@ -69,8 +69,11 @@ def create_predict_job( # f"train job has status {train_job.status.value}, Predict requires status SUCCESS" # ) - return PredictFuture.create( + return WorkflowPredictionResultFuture.create( session=self.session, + sequences=sequences, + train_job=train_job, + model_ids=model_ids, job=predict.create_predict_job( session=self.session, sequences=sequences, @@ -84,7 +87,7 @@ def create_predict_single_site( sequence: str, train_job: TrainFuture, model_ids: list[str] | None = None, - ) -> PredictFuture: + ) -> WorkflowPredictionResultFuture: """ Creates a new Predict job for single site mutation analysis with a trained model. @@ -128,8 +131,11 @@ def create_predict_single_site( # f"train job has status {train_job.status.value}, Predict requires status SUCCESS" # ) - return PredictFuture.create( + return WorkflowPredictionResultFuture.create( session=self.session, + sequence=sequence, + train_job=train_job, + model_ids=model_ids, job=predict.create_predict_single_site( session=self.session, sequence=sequence, diff --git a/openprotein/app/services/umap.py b/openprotein/app/services/umap.py new file mode 100644 index 0000000..d0ccd90 --- /dev/null +++ b/openprotein/app/services/umap.py @@ -0,0 +1,97 @@ +from openprotein.api import umap +from openprotein.app.models import AssayDataset, EmbeddingModel, SVDModel, UMAPModel +from openprotein.base import APISession +from openprotein.schemas import ReductionType + + +class UMAPAPI: + + def __init__(self, session: APISession): + self.session = session + + def fit_umap( + self, + model: EmbeddingModel | SVDModel, + sequences: list[bytes] | None = None, + assay: AssayDataset | None = None, + n_components: int = 2, + reduction: ReductionType | None = None, + **kwargs, + ) -> UMAPModel: + """ + Fit an UMAP on the sequences with the specified model_id and hyperparameters (n_components). + + Parameters + ---------- + model_id : str + The ID of the model to fit the UMAP on. + sequences : list[bytes] + The list of sequences to use for the UMAP fitting. + n_components : int, optional + The number of components for the UMAP, by default 1024. + reduction : str, optional + The reduction method to apply to the embeddings, by default None. + + Returns + ------- + UMAPModel + The model with the UMAP fit. + """ + return model.fit_umap( + sequences=sequences, + assay=assay, + n_components=n_components, + reduction=reduction, + **kwargs, + ) + + def get_umap(self, umap_id: str) -> UMAPModel: + """ + Get UMAP job results. Including UMAP dimension and sequence lengths. + + Requires a successful UMAP job from fit_umap + + Parameters + ---------- + umap_id : str + The ID of the UMAP job. + Returns + ------- + UMAPModel + The model with the UMAP fit. + """ + metadata = umap.umap_get(self.session, umap_id) + return UMAPModel(session=self.session, metadata=metadata) + + def __delete_umap(self, umap_id: str) -> bool: + """ + Delete UMAP model. + + Parameters + ---------- + umap_id : str + The ID of the UMAP job. + Returns + ------- + bool + True: successful deletion + + """ + return umap.umap_delete(self.session, umap_id) + + def list_umap(self) -> list[UMAPModel]: + """ + List UMAP models made by user. + + Takes no args. + + Returns + ------- + list[UMAPModel] + UMAPModels + + """ + return [ + UMAPModel(session=self.session, metadata=metadata) + for metadata in umap.umap_list_get(self.session) + ] diff --git a/openprotein/csv.py b/openprotein/csv.py new file mode 100644 index 0000000..1bf4747 --- /dev/null +++ b/openprotein/csv.py @@ -0,0 +1,26 @@ +import codecs +import csv +from typing import Iterator + +import requests + + +def csv_stream(response: requests.Response) -> Iterator[list[str]]: + """ + Returns a CSV reader from a requests.Response object. + + Parameters + ---------- + response : requests.Response + The response object to parse. + + Returns + ------- + csv.reader + A csv reader object for the response. + """ + # get raw bytes stream + raw_content = response.raw + # force the response to be encoded as utf-8 + content = codecs.getreader("utf-8")(raw_content) + return csv.reader(content) diff --git a/openprotein/schemas/__init__.py b/openprotein/schemas/__init__.py index dcdadc3..8045781 100644 --- a/openprotein/schemas/__init__.py +++ b/openprotein/schemas/__init__.py @@ -18,19 +18,14 @@ ) from .design import ( ModelCriterion, + NMutationCriterion, + n_mutations, + Subcriterion, Criterion, - DesignJobCreate, - DesignMetadata, - DesignResults, - DesignStep, - DesignJob, -) -from .deprecated.poet import ( - PoetScoreJob, - PoetSSPJob, - PoetScoreResult, - PoetSSPResult, - PoetGenerateJob, + Criteria, + DesignJobCreate as WorkflowDesignJobCreate, + DesignJob as WorkflowDesignJob, + Design as WorkflowDesign, ) from .align import MSAJob, MSASamplingMethod, PoetInputType, PromptJob, PromptPostParams from .embeddings import ( @@ -47,15 +42,20 @@ GenerateJob, ) from .fold import FoldJob +from .features import FeatureType from .svd import ( SVDMetadata, - FitJob, + FitJob as SVDFitJob, EmbeddingsJob as SVDEmbeddingsJob, ) +from .umap import ( + UMAPMetadata, + FitJob as UMAPFitJob, + EmbeddingsJob as UMAPEmbeddingsJob, +) from .predictor import ( Constraints, CVJob, - FeatureType, Kernel, PredictJob, PredictMultiJob, @@ -65,3 +65,9 @@ PredictSingleSiteJob, TrainJob, ) +from .designer import ( + Design, + DesignJob, + DesignAlgorithm, + DesignConstraint, +) diff --git a/openprotein/schemas/design.py b/openprotein/schemas/design.py index d36aeec..14aa0dd 100644 --- a/openprotein/schemas/design.py +++ b/openprotein/schemas/design.py @@ -1,24 +1,30 @@ import re -from datetime import datetime from enum import Enum -from typing import Literal +from typing import Any, Literal -from pydantic import BaseModel, Field, field_validator +from pydantic import ( + BaseModel, + ConfigDict, + Field, + RootModel, + field_validator, + model_serializer, +) from .job import Job, JobType -class DesignMetadata(BaseModel): +class SubscoreMetadata(BaseModel): y_mu: float | None = None y_var: float | None = None class DesignSubscore(BaseModel): score: float - metadata: DesignMetadata + metadata: SubscoreMetadata -class DesignStep(BaseModel): +class DesignResult(BaseModel): step: int sample_index: int sequence: str @@ -30,46 +36,155 @@ class DesignStep(BaseModel): # umap2: float -class DesignResults(BaseModel): - status: str - job_id: str - job_type: str - created_date: datetime - start_date: datetime - end_date: datetime | None +class DesignJob(Job): + job_type: Literal[JobType.workflow_design] + + +class Design(DesignJob): assay_id: str num_rows: int - result: list[DesignStep] + result: list[DesignResult] + +class CriterionType(str, Enum): + model = "model" + n_mutations = "n_mutations" -class DirectionEnum(str, Enum): - gt = ">" - lt = "<" - eq = "=" +class Subcriterion(BaseModel): + + criterion_type: CriterionType + + def __and__(self, other: "Subcriterion | Criterion | Any") -> "Criterion": + """Returns a Criterion with the two subcriteria AND-ed.""" + others = [] + if isinstance(other, Subcriterion): + others = [other] + elif isinstance(other, Criterion): + others = other.root + else: + raise ValueError( + f"Expected to chain only with criterion or subcriterion, got {type(other)}" + ) + return Criterion([self] + others) # type: ignore - doesnt like Self + + def __or__(self, other: "Subcriterion | Criterion | Any") -> "Criteria": + """Returns a Criteria with the two subcriteria OR-ed.""" + if isinstance(other, Criterion): + pass + elif isinstance(other, Subcriterion): + other = Criterion([other]) + else: + raise ValueError( + f"Expected to chain only with criterion or subcriterion, got {type(other)}" + ) + return Criteria([Criterion([self]), other]) -class Criterion(BaseModel): - target: float - weight: float - direction: str +class ModelCriterion(Subcriterion): -class ModelCriterion(BaseModel): - criterion_type: Literal["model"] + class Criterion(BaseModel): + class DirectionEnum(str, Enum): + gt = ">" + lt = "<" + eq = "=" + + weight: float = 1.0 + direction: DirectionEnum | None = None + target: float | None = None + + criterion_type: CriterionType = CriterionType.model model_id: str measurement_name: str - criterion: Criterion + criterion: Criterion = Criterion() + + model_config = ConfigDict(protected_namespaces=()) + + def __mul__(self, weight: float) -> "ModelCriterion": + self.criterion.weight = weight + return self + + def __lt__(self, other: float) -> "ModelCriterion": + self.criterion.target = other + self.criterion.direction = ModelCriterion.Criterion.DirectionEnum.lt + return self + + def __gt__(self, other: float) -> "ModelCriterion": + self.criterion.target = other + self.criterion.direction = ModelCriterion.Criterion.DirectionEnum.gt + return self + + def __eq__(self, other: float) -> "ModelCriterion": + self.criterion.target = other + self.criterion.direction = ModelCriterion.Criterion.DirectionEnum.eq + return self + + __rmul__ = __mul__ - class Config: - protected_namespaces = () + @model_serializer(mode="wrap") + def validate_criterion_before_serialize(self, handler): + if ( + self.criterion is None + or self.criterion.direction is None + or self.criterion.target is None + ): + raise ValueError("Expected direction and target to be set") + return handler(self) -class NMutationCriterion(BaseModel): - criterion_type: Literal["n_mutations"] - # sequences: list[str] | None +class NMutationCriterion(Subcriterion): + criterion_type: CriterionType = CriterionType.n_mutations + sequences: list[str] = Field(default_factory=list) + @model_serializer(mode="wrap") + def remove_empty_sequences(self, handler): + d = handler(self) + if not d["sequences"]: + del d["sequences"] + return d -CriterionItem = ModelCriterion | NMutationCriterion + +n_mutations = NMutationCriterion + + +class Criterion(RootModel): + root: list[ModelCriterion | NMutationCriterion | Subcriterion] + + def __and__(self, other: "Criterion | Subcriterion") -> "Criterion": + """Returns a Criteria with the other criterion OR-ed with itself.""" + others = [] + + if isinstance(other, Subcriterion): + others = [other] + elif isinstance(other, Criterion): + others = other.root + + return Criterion(self.root + others) + + def __or__(self, other: "Criterion | Subcriterion") -> "Criteria": + """Returns a Criteria with the other criterion OR-ed with itself.""" + + if isinstance(other, Criterion): + pass + elif isinstance(other, Subcriterion): + other = Criterion([other]) + + return Criteria([self, other]) + + +class Criteria(RootModel): + root: list[Criterion] + + def __or__(self, other: "Criterion | Subcriterion | Criteria") -> "Criteria": + """Returns a Criteria with the other criteria OR-ed with itself.""" + if isinstance(other, Criteria): + pass + if isinstance(other, Criterion): + other = Criteria([other]) + elif isinstance(other, Subcriterion): + other = Criteria([Criterion([other])]) + + return Criteria(self.root + other.root) class DesignConstraint: @@ -114,8 +229,8 @@ def as_dict(self) -> dict[int, list[str]]: class DesignJobCreate(BaseModel): assay_id: str - criteria: list[list[CriterionItem]] - num_steps: int | None = 8 + criteria: Criteria + num_steps: int = 8 pop_size: int | None = None n_offsprings: int | None = None crossover_prob: float | None = None @@ -123,8 +238,7 @@ class DesignJobCreate(BaseModel): mutation_average_mutations_per_seq: int | None = None allowed_tokens: DesignConstraint | dict[int, list[str]] | None = None - class Config: - arbitrary_types_allowed = True + model_config = ConfigDict(arbitrary_types_allowed=True) @field_validator("allowed_tokens", mode="before") def ensure_dict(cls, v): @@ -245,7 +359,3 @@ def no_change(sequence: str): def keep_cys(sequence: str): return {k + 1: [v] for k, v in enumerate(sequence) if v == "C"} - - -class DesignJob(Job): - job_type: Literal[JobType.workflow_design] diff --git a/openprotein/schemas/designer.py b/openprotein/schemas/designer.py new file mode 100644 index 0000000..64de8a5 --- /dev/null +++ b/openprotein/schemas/designer.py @@ -0,0 +1,38 @@ +from datetime import datetime +from enum import Enum +from typing import Literal + +from pydantic import BaseModel + +from .design import Criteria, DesignConstraint +from .job import Job, JobStatus, JobType + + +class DesignAlgorithm(str, Enum): + genetic_algorithm = "genetic-algorithm" + + +class Design(BaseModel): + id: str + status: JobStatus + progress_counter: int + created_date: datetime + algorithm: DesignAlgorithm + num_rows: int + num_steps: int + assay_id: str + criteria: Criteria + allowed_tokens: dict[str, list[str]] | None + pop_size: int + # ga params + n_offsprings: int + crossover_prob: float + crossover_prob_pointwise: float + mutation_average_mutations_per_seq: int + + def is_done(self): + return self.status.done() + + +class DesignJob(Job): + job_type: Literal[JobType.designer] diff --git a/openprotein/schemas/embeddings.py b/openprotein/schemas/embeddings.py index 26df243..06de726 100644 --- a/openprotein/schemas/embeddings.py +++ b/openprotein/schemas/embeddings.py @@ -4,7 +4,7 @@ from typing import Literal import numpy as np -from pydantic import BaseModel, Field +from pydantic import BaseModel, ConfigDict, Field from .job import BatchJob, Job, JobType @@ -51,12 +51,11 @@ class EmbeddedSequence(BaseModel): Represented as an iterable yielding the sequence followed by the embedding. """ - class Config: - arbitrary_types_allowed = True - sequence: bytes embedding: np.ndarray + model_config = ConfigDict(arbitrary_types_allowed=True) + def __iter__(self): yield self.sequence yield self.embedding diff --git a/openprotein/schemas/features.py b/openprotein/schemas/features.py new file mode 100644 index 0000000..4f01a84 --- /dev/null +++ b/openprotein/schemas/features.py @@ -0,0 +1,7 @@ +from enum import Enum + + +class FeatureType(str, Enum): + + PLM = "PLM" + SVD = "SVD" diff --git a/openprotein/schemas/job.py b/openprotein/schemas/job.py index 3e11d17..e50d46e 100644 --- a/openprotein/schemas/job.py +++ b/openprotein/schemas/job.py @@ -44,6 +44,9 @@ class JobType(str, Enum): svd_fit = "/svd/fit" svd_embed = "/svd/embed" + umap_fit = "/umap/fit" + umap_embed = "/umap/embed" + embeddings_fold = "/embeddings/fold" # predictor jobs @@ -54,6 +57,9 @@ class JobType(str, Enum): predictor_predict_multi = "/predictor/predict_multi" predictor_predict_multi_single_site = "/predictor/predict_multi_single_site" + # designer + designer = "/design" + class JobStatus(str, Enum): PENDING = "PENDING" diff --git a/openprotein/schemas/predict.py b/openprotein/schemas/predict.py index b4626f0..953419b 100644 --- a/openprotein/schemas/predict.py +++ b/openprotein/schemas/predict.py @@ -1,7 +1,7 @@ from datetime import datetime from typing import Literal -from pydantic import BaseModel, field_validator +from pydantic import BaseModel, ConfigDict from .job import Job, JobType @@ -44,8 +44,7 @@ class Prediction(BaseModel): model_name: str properties: dict[str, dict[str, float]] - class Config: - protected_namespaces = () + model_config = ConfigDict(protected_namespaces=()) class PredictJobBase(BaseModel): diff --git a/openprotein/schemas/predictor.py b/openprotein/schemas/predictor.py index c47d183..ab110b4 100644 --- a/openprotein/schemas/predictor.py +++ b/openprotein/schemas/predictor.py @@ -1,8 +1,8 @@ -from enum import Enum from typing import Literal -from pydantic import BaseModel +from pydantic import BaseModel, ConfigDict +from .features import FeatureType from .job import Job, JobStatus, JobType @@ -15,19 +15,12 @@ class Constraints(BaseModel): sequence_length: int | None = None -class FeatureType(str, Enum): - - PLM = "PLM" - SVD = "SVD" - - class Features(BaseModel): type: FeatureType model_id: str | None = None reduction: str | None = None - class Config: - protected_namespaces = () + model_config = ConfigDict(protected_namespaces=()) class PredictorArgs(BaseModel): @@ -55,8 +48,7 @@ class PredictorMetadata(BaseModel): def is_done(self): return self.status.done() - class Config: - protected_namespaces = () + model_config = ConfigDict(protected_namespaces=()) class TrainJob(Job): diff --git a/openprotein/schemas/svd.py b/openprotein/schemas/svd.py index 88e9cae..86c6d91 100644 --- a/openprotein/schemas/svd.py +++ b/openprotein/schemas/svd.py @@ -3,7 +3,7 @@ from datetime import datetime from typing import Literal -from pydantic import BaseModel, Field +from pydantic import BaseModel, ConfigDict from .job import BatchJob, Job, JobStatus, JobType @@ -20,8 +20,7 @@ class SVDMetadata(BaseModel): def is_done(self): return self.status.done() - class Config: - protected_namespaces = () + model_config = ConfigDict(protected_namespaces=()) class FitJob(Job): diff --git a/openprotein/schemas/umap.py b/openprotein/schemas/umap.py new file mode 100644 index 0000000..7909f75 --- /dev/null +++ b/openprotein/schemas/umap.py @@ -0,0 +1,35 @@ +"""Schemas for OpenProtein UMAP system.""" + +from datetime import datetime +from typing import Literal + +from pydantic import BaseModel, ConfigDict + +from .features import FeatureType +from .job import BatchJob, Job, JobStatus, JobType + + +class UMAPMetadata(BaseModel): + id: str + status: JobStatus + created_date: datetime | None = None + model_id: str + feature_type: FeatureType + n_components: int = 2 + n_neighbors: int = 15 + min_dist: float = 0.1 + reduction: str | None = None + sequence_length: int | None = None + + def is_done(self): + return self.status.done() + + model_config = ConfigDict(protected_namespaces=()) + + +class FitJob(Job): + job_type: Literal[JobType.umap_fit] + + +class EmbeddingsJob(Job, BatchJob): + job_type: Literal[JobType.umap_embed] diff --git a/pixi.lock b/pixi.lock index 8378130..fcfd081 100644 --- a/pixi.lock +++ b/pixi.lock @@ -121,8 +121,8 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ca-certificates-2024.8.30-hbcca054_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/editables-0.5-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.40-hf3520f5_7.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.6.4-h5888daf_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.2-h7f98852_5.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-14.1.0-h77fa898_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-14.1.0-h69a702a_1.conda @@ -135,7 +135,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-he02047a_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.3.2-hb9d3cd8_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pip-24.2-pyh8b19718_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.10.15-h4a871b0_0_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.12.7-hc5c86c4_0_cpython.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8228510_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-75.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h4845f30_101.conda @@ -145,16 +145,15 @@ environments: - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/45/86/4736ac618d82a20d87d2f92ae19441ebc7ac9e7a581d7e58bbe79233b24a/asttokens-2.4.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/da/f1/3702ba2a7470666a62fd81c58a4c40be00670e5006a67f4d626e57f013ae/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/16/92/92a76dc2ff3a12e69ba94e7e05168d37d0345fa08c87e1fe24d0c2a42223/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/e6/75/49e5bfe642f71f272236b5b2d2691cf915a7283cc0ceda56357b61daa538/comm-0.2.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/99/e6/d11966962b1aa515f5586d3907ad019f4b812c04e4546cc19ebf62b5178e/contourpy-1.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/6e/be/524e377567defac0e21a46e2a529652d165fed130a0d8a863219303cee18/contourpy-1.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/57/0c/c2ec581541923a4d36cee4fd2419c1211c986849fc61097f87aa81fc6ad3/debugpy-1.8.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/fd/b6/ee71d5e73712daf8307a9e85f5e39301abc8b66d13acd04dfff1702e672e/debugpy-1.8.5-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/d5/50/83c593b07763e1161326b3b8c6686f0f4b0f24d5526546bee538c89837d6/decorator-5.1.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b5/fd/afcd0496feca3276f509df3dbd5dae726fcc756f1a08d9e25abe1733f962/executing-2.1.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e5/12/9a45294a7c4520cc32936edd15df1d5c24af701d2f5f51070a9a43d7664b/fonttools-4.54.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/0c/8b/90e80904fdc24ce33f6fc6f35ebd2232fe731a8528a22008458cf197bc4d/hatchling-1.25.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/08/07/aa85cc62abcc940b25d14b542cf585eebf4830032a7f6a1395d696bb3231/fonttools-4.54.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/72/41/b3e29dc4fe623794070e5dfbb9915acb649ce05d6472f005470cbed9de83/hatchling-1.26.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/94/5c/368ae6c01c7628438358e6d337c19b05425727fbb221d2a3c4303c372f42/ipykernel-6.29.5-py3-none-any.whl @@ -162,41 +161,40 @@ environments: - pypi: https://files.pythonhosted.org/packages/20/9f/bc63f0f0737ad7a60800bfd472a4836661adae21f9c2535f3957b1e54ceb/jedi-0.19.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/11/85/b0394e0b6fcccd2c1eeefc230978a6f8cb0c5df1e4cd3e7625735a0d7d1e/jupyter_client-8.6.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c9/fb/108ecd1fe961941959ad0ee4e12ee7b8b1477247f30b1fdfd83ceaf017f0/jupyter_core-5.7.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/55/91/0a57ce324caf2ff5403edab71c508dd8f648094b18cfbb4c8cc0fde4a6ac/kiwisolver-1.4.7-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/8d/9d/d06860390f9d154fa884f1740a5456378fb153ff57443c91a4a32bab7092/matplotlib-3.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/21/e4/c0b6746fd2eb62fe702118b3ca0cb384ce95e1261cfada58ff693aeec08a/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/27/75/de5b9cd67648051cae40039da0c8cbc497a0d99acb1a1f3d087cd66d27b7/matplotlib-3.9.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/7d/4b/a509d346fffede6120cc17610cc500819417ee9c3da7f08d9aaf15cab2a3/numpy-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/08/aa/cc0199a5f0ad350994d660967a8efb233fe0416e4639146c089643407ce6/packaging-24.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/89/1b/12521efcbc6058e2673583bb096c2b5046a9df39bd73eca392c1efed24e5/pandas-2.2.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/cb/22/2b840d297183916a95847c11f82ae11e248fa98113490b2357f774651e1d/numpy-2.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/40/10/79e52ef01dfeb1c1ca47a109a01a248754ebe990e159a844ece12914de83/pandas-2.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/06/2d/68d58d819798b466e16c01ca934deada2f9165fb3d062f83abbef2f8067e/pandas_stubs-2.2.2.240909-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c6/ac/dac4a63f978e4dcb3c6d3a78c4d8e0192a113d288502a1216950c41b1027/parso-0.8.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b5/5b/6651c288b08df3b8c1e2f8c1152201e0b25d240e22ddade0f1e242fc9fa0/pillow-10.4.0-cp310-cp310-manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/55/77/40daddf677897a923d5d33329acd52a2144d54a9644f2a5422c028c6bf2d/pillow-10.4.0-cp312-cp312-manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e8/23/22750c4b768f09386d1c3cc4337953e8936f48a888fa6dddfb669b2c9088/prompt_toolkit-3.0.47-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/19/74/f59e7e0d392bc1070e9a70e2f9190d652487ac115bb16e2eff6b22ad1d24/psutil-6.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/58/4d/8245e6f76a93c98aab285a43ea71ff1b171bcd90c9d238bf81f7021fb233/psutil-6.1.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/df/e4/ba44652d562cbf0bf320e0f3810206149c8a4e99cdbf66da82e97ab53a15/pydantic-2.9.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/11/ac/1e647dc1121c028b691028fa61a4e7477e6aeb5132628fde41dd34c1671f/pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/06/c8/7d4b708f8d05a5cbfda3243aad468052c6e99de7d0937c9146c24d9f12e9/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e5/0c/0e3c05b1c87bb6a1c76d281b0f35e78d2d80ac91b5f8f524cebf77f51049/pyparsing-3.1.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/11/c3/005fcca25ce078d2cc29fd559379817424e94885510568bc1bc53d7d5846/pytz-2024.2-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/16/29/ca99b4598a9dc7e468b5417eda91f372b595be1e3eec9b7cbe8e5d3584e8/pyzmq-26.2.0-cp310-cp310-manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/07/3b/44ea6266a6761e9eefaa37d98fabefa112328808ac41aa87b4bbb668af30/pyzmq-26.2.0-cp312-cp312-manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/47/78/b0c2c23880dd1e99e938ad49ccfb011ae353758a2dc5ed7ee59baff684c3/scipy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/8e/ee/8a26858ca517e9c64f84b4c7734b89bda8e63bec85c3d2f432d225bb1886/scipy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/97/75/10a9ebee3fd790d20926a90a2547f0bf78f371b2f13aa822c759680ca7b9/tomli-2.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/22/d4/54f9d12668b58336bd30defe0307e6c61589a3e687b05c366f804b7faaf0/tornado-6.4.1-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/48/5d/acf5905c36149bbaec41ccf7f2b68814647347b72075ac0b1fe3022fdc73/tqdm-4.66.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2b/78/57043611a16c655c8350b4c01b8d6abfb38cc2acb475238b62c2146186d7/tqdm-4.67.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b6/7a/e0edec9c8905e851d52076bbc41890603e2ba97cf64966bc1498f2244fd2/trove_classifiers-2024.9.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/35/35/5055ab8d215af853d07bbff1a74edf48f91ed308f037380a5ca52dd73348/trove_classifiers-2024.10.21.16-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b6/a6/8846372f55c6bb470ff7207e4dc601017e264e5fe7d79a441ece3545b36c/types_pytz-2024.2.0.20240913-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/65/58/f9c9e6be752e9fcb8b6a0ee9fb87e6e7a1f6bcab2cdc73f02bb7ba91ada0/tzdata-2024.1-py2.py3-none-any.whl @@ -206,8 +204,8 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/_openmp_mutex-4.5-2_gnu.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/bzip2-1.0.8-h68df207_7.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ca-certificates-2024.8.30-hcefe29a_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/editables-0.5-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ld_impl_linux-aarch64-2.40-h9fc2d93_7.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libexpat-2.6.4-h5ad3122_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libffi-3.4.2-h3557bc0_5.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgcc-14.1.0-he277a41_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgcc-ng-14.1.0-he9431aa_1.conda @@ -220,7 +218,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ncurses-6.5-hcccb83c_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/openssl-3.3.2-h86ecc28_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pip-24.2-pyh8b19718_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/python-3.10.15-hbf90c55_0_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/python-3.12.7-h5d932e8_0_cpython.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/readline-8.2-h8fc344f_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-75.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/tk-8.6.13-h194ca79_0.conda @@ -230,16 +228,15 @@ environments: - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/45/86/4736ac618d82a20d87d2f92ae19441ebc7ac9e7a581d7e58bbe79233b24a/asttokens-2.4.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b8/60/e2f67915a51be59d4539ed189eb0a2b0d292bf79270410746becb32bc2c3/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/9d/e4/9263b8240ed9472a2ae7ddc3e516e71ef46617fe40eaa51221ccd4ad9a27/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl - pypi: https://files.pythonhosted.org/packages/e6/75/49e5bfe642f71f272236b5b2d2691cf915a7283cc0ceda56357b61daa538/comm-0.2.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/6f/b4/6fffdf213ffccc28483c524b9dad46bb78332851133b36ad354b856ddc7c/contourpy-1.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/51/3d/aa0fe6ae67e3ef9f178389e4caaaa68daf2f9024092aa3c6032e3d174670/contourpy-1.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/49/b595c34d7bc690e8d225a6641618a5c111c7e13db5d9e2b756c15ce8f8c6/debugpy-1.8.5-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d5/50/83c593b07763e1161326b3b8c6686f0f4b0f24d5526546bee538c89837d6/decorator-5.1.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b5/fd/afcd0496feca3276f509df3dbd5dae726fcc756f1a08d9e25abe1733f962/executing-2.1.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/16/6f/b99e0c347732fb003077a2cff38c26f381969b74329aa5597e344d540fe1/fonttools-4.54.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl - - pypi: https://files.pythonhosted.org/packages/0c/8b/90e80904fdc24ce33f6fc6f35ebd2232fe731a8528a22008458cf197bc4d/hatchling-1.25.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/90/41/5573e074739efd9227dd23647724f01f6f07ad062fe09d02e91c5549dcf7/fonttools-4.54.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/72/41/b3e29dc4fe623794070e5dfbb9915acb649ce05d6472f005470cbed9de83/hatchling-1.26.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/94/5c/368ae6c01c7628438358e6d337c19b05425727fbb221d2a3c4303c372f42/ipykernel-6.29.5-py3-none-any.whl @@ -247,41 +244,40 @@ environments: - pypi: https://files.pythonhosted.org/packages/20/9f/bc63f0f0737ad7a60800bfd472a4836661adae21f9c2535f3957b1e54ceb/jedi-0.19.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/11/85/b0394e0b6fcccd2c1eeefc230978a6f8cb0c5df1e4cd3e7625735a0d7d1e/jupyter_client-8.6.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c9/fb/108ecd1fe961941959ad0ee4e12ee7b8b1477247f30b1fdfd83ceaf017f0/jupyter_core-5.7.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/12/5d/c36140313f2510e20207708adf36ae4919416d697ee0236b0ddfb6fd1050/kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl - - pypi: https://files.pythonhosted.org/packages/a8/a0/917f3c6d3a8774a3a1502d9f3dfc1456e07c1fa0c211a23b75a69e154180/matplotlib-3.9.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/b6/67/3f4850b5e6cffb75ec40577ddf54f7b82b15269cc5097ff2e968ee32ea7d/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/5b/bd/c404502aa1824456d2862dd6b9b0c1917761a51a32f7f83ff8cf94b6d117/matplotlib-3.9.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl - pypi: https://files.pythonhosted.org/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/cd/c4/869f8db87f5c9df86b93ca42036f58911ff162dd091a41e617977ab50d1f/numpy-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl - - pypi: https://files.pythonhosted.org/packages/08/aa/cc0199a5f0ad350994d660967a8efb233fe0416e4639146c089643407ce6/packaging-24.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/01/c6/d3d2612aea9b9f28e79a30b864835dad8f542dcf474eee09afeee5d15d75/pandas-2.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/9f/8a/76ddef3e621541ddd6984bc24d256a4e3422d036790cbbe449e6cad439ee/numpy-2.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b0/85/f95b5f322e1ae13b7ed7e97bd999160fa003424711ab4dc8344b8772c270/pandas-2.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl - pypi: https://files.pythonhosted.org/packages/06/2d/68d58d819798b466e16c01ca934deada2f9165fb3d062f83abbef2f8067e/pandas_stubs-2.2.2.240909-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c6/ac/dac4a63f978e4dcb3c6d3a78c4d8e0192a113d288502a1216950c41b1027/parso-0.8.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b1/84/9a15cc5726cbbfe7f9f90bfb11f5d028586595907cd093815ca6644932e3/pillow-10.4.0-cp310-cp310-manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/7d/1b/c14b4197b80150fb64453585247e6fb2e1d93761fa0fa9cf63b102fde822/pillow-10.4.0-cp312-cp312-manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e8/23/22750c4b768f09386d1c3cc4337953e8936f48a888fa6dddfb669b2c9088/prompt_toolkit-3.0.47-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/cd/5f/60038e277ff0a9cc8f0c9ea3d0c5eb6ee1d2470ea3f9389d776432888e47/psutil-6.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/27/c2/d034856ac47e3b3cdfa9720d0e113902e615f4190d5d1bdb8df4b2015fb2/psutil-6.1.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/df/e4/ba44652d562cbf0bf320e0f3810206149c8a4e99cdbf66da82e97ab53a15/pydantic-2.9.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/cc/69/5f945b4416f42ea3f3bc9d2aaec66c76084a6ff4ff27555bf9415ab43189/pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/dc/69/8edd5c3cd48bb833a3f7ef9b81d7666ccddd3c9a635225214e044b6e8281/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl - pypi: https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e5/0c/0e3c05b1c87bb6a1c76d281b0f35e78d2d80ac91b5f8f524cebf77f51049/pyparsing-3.1.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/11/c3/005fcca25ce078d2cc29fd559379817424e94885510568bc1bc53d7d5846/pytz-2024.2-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b6/09/b51b6683fde5ca04593a57bbe81788b6b43114d8f8ee4e80afc991e14760/pyzmq-26.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/4f/ef/5a23ec689ff36d7625b38d121ef15abfc3631a9aecb417baf7a4245e4124/pyzmq-26.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl - pypi: https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d8/df/cdb6be5274bc694c4c22862ac3438cb04f360ed9df0aecee02ce0b798380/scipy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/f0/5a/efa92a58dc3a2898705f1dc9dbaf390ca7d4fba26d6ab8cfffb0c72f656f/scipy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl - pypi: https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/97/75/10a9ebee3fd790d20926a90a2547f0bf78f371b2f13aa822c759680ca7b9/tomli-2.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/13/cf/786b8f1e6fe1c7c675e79657448178ad65e41c1c9765ef82e7f6f765c4c5/tornado-6.4.1-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl - - pypi: https://files.pythonhosted.org/packages/48/5d/acf5905c36149bbaec41ccf7f2b68814647347b72075ac0b1fe3022fdc73/tqdm-4.66.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2b/78/57043611a16c655c8350b4c01b8d6abfb38cc2acb475238b62c2146186d7/tqdm-4.67.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b6/7a/e0edec9c8905e851d52076bbc41890603e2ba97cf64966bc1498f2244fd2/trove_classifiers-2024.9.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/35/35/5055ab8d215af853d07bbff1a74edf48f91ed308f037380a5ca52dd73348/trove_classifiers-2024.10.21.16-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b6/a6/8846372f55c6bb470ff7207e4dc601017e264e5fe7d79a441ece3545b36c/types_pytz-2024.2.0.20240913-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/65/58/f9c9e6be752e9fcb8b6a0ee9fb87e6e7a1f6bcab2cdc73f02bb7ba91ada0/tzdata-2024.1-py2.py3-none-any.whl @@ -290,14 +286,14 @@ environments: osx-arm64: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-h99b78c6_7.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ca-certificates-2024.8.30-hf0a4a13_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/editables-0.5-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.6.4-h286801f_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.4.2-h3422bc3_5.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.46.1-hc14010f_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.1-hfb2fe0b_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.5-h7bae524_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.3.2-h8359307_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pip-24.2-pyh8b19718_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.10.15-h7d35d02_0_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.12.7-h739c21a_0_cpython.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.2-h92ec313_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-75.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h5083fa2_1.conda @@ -308,16 +304,15 @@ environments: - pypi: https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/45/86/4736ac618d82a20d87d2f92ae19441ebc7ac9e7a581d7e58bbe79233b24a/asttokens-2.4.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/46/6a/d5c26c41c49b546860cc1acabdddf48b0b3fb2685f4f5617ac59261b44ae/charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/fa/44/b730e2a2580110ced837ac083d8ad222343c96bb6b66e9e4e706e4d0b6df/charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/e6/75/49e5bfe642f71f272236b5b2d2691cf915a7283cc0ceda56357b61daa538/comm-0.2.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/50/d6/c953b400219443535d412fcbbc42e7a5e823291236bc0bb88936e3cc9317/contourpy-1.3.0-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/e3/04/33351c5d5108460a8ce6d512307690b023f0cfcad5899499f5c83b9d63b1/contourpy-1.3.0-cp312-cp312-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/02/49/b595c34d7bc690e8d225a6641618a5c111c7e13db5d9e2b756c15ce8f8c6/debugpy-1.8.5-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9a/82/7d9e1f75fb23c876ab379008c7cf484a1cfa5ed47ccaac8ba37c75e6814e/debugpy-1.8.5-cp312-cp312-macosx_12_0_universal2.whl - pypi: https://files.pythonhosted.org/packages/d5/50/83c593b07763e1161326b3b8c6686f0f4b0f24d5526546bee538c89837d6/decorator-5.1.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b5/fd/afcd0496feca3276f509df3dbd5dae726fcc756f1a08d9e25abe1733f962/executing-2.1.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/2f/9a/9d899e7ae55b0dd30632e6ca36c0f5fa1205b1b096ec171c9be903673058/fonttools-4.54.1-cp310-cp310-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/0c/8b/90e80904fdc24ce33f6fc6f35ebd2232fe731a8528a22008458cf197bc4d/hatchling-1.25.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/67/9d/cfbfe36e5061a8f68b154454ba2304eb01f40d4ba9b63e41d9058909baed/fonttools-4.54.1-cp312-cp312-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/72/41/b3e29dc4fe623794070e5dfbb9915acb649ce05d6472f005470cbed9de83/hatchling-1.26.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/94/5c/368ae6c01c7628438358e6d337c19b05425727fbb221d2a3c4303c372f42/ipykernel-6.29.5-py3-none-any.whl @@ -325,41 +320,40 @@ environments: - pypi: https://files.pythonhosted.org/packages/20/9f/bc63f0f0737ad7a60800bfd472a4836661adae21f9c2535f3957b1e54ceb/jedi-0.19.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/11/85/b0394e0b6fcccd2c1eeefc230978a6f8cb0c5df1e4cd3e7625735a0d7d1e/jupyter_client-8.6.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c9/fb/108ecd1fe961941959ad0ee4e12ee7b8b1477247f30b1fdfd83ceaf017f0/jupyter_core-5.7.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ef/fa/65de49c85838681fc9cb05de2a68067a683717321e01ddafb5b8024286f0/kiwisolver-1.4.7-cp310-cp310-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/36/98/cbacbd30241369d099f9c13a2b6bc3b7068d85214f5b5795e583ac3d8aba/matplotlib-3.9.2-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/80/c5/57fa58276dfdfa612241d640a64ca2f76adc6ffcebdbd135b4ef60095098/kiwisolver-1.4.7-cp312-cp312-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/35/3e/5713b84a02b24b2a4bd4d6673bfc03017e6654e1d8793ece783b7ed4d484/matplotlib-3.9.2-cp312-cp312-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/69/30/f41c9b6dab4e1ec56b40d1daa81ce9f9f8d26da6d02af18768a883676bd5/numpy-2.1.1-cp310-cp310-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/08/aa/cc0199a5f0ad350994d660967a8efb233fe0416e4639146c089643407ce6/packaging-24.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fd/4b/0cd38e68ab690b9df8ef90cba625bf3f93b82d1c719703b8e1b333b2c72d/pandas-2.2.2-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/6b/6c/a9fbef5fd2f9685212af2a9e47485cde9357c3e303e079ccf85127516f2d/numpy-2.1.1-cp312-cp312-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/db/7c/9a60add21b96140e22465d9adf09832feade45235cd22f4cb1668a25e443/pandas-2.2.2-cp312-cp312-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/06/2d/68d58d819798b466e16c01ca934deada2f9165fb3d062f83abbef2f8067e/pandas_stubs-2.2.2.240909-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c6/ac/dac4a63f978e4dcb3c6d3a78c4d8e0192a113d288502a1216950c41b1027/parso-0.8.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9a/9e/4143b907be8ea0bce215f2ae4f7480027473f8b61fcedfda9d851082a5d2/pillow-10.4.0-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/e7/cf/5c558a0f247e0bf9cec92bff9b46ae6474dd736f6d906315e60e4075f737/pillow-10.4.0-cp312-cp312-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e8/23/22750c4b768f09386d1c3cc4337953e8936f48a888fa6dddfb669b2c9088/prompt_toolkit-3.0.47-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/7c/06/63872a64c312a24fb9b4af123ee7007a306617da63ff13bcc1432386ead7/psutil-6.0.0-cp38-abi3-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/1d/cb/313e80644ea407f04f6602a9e23096540d9dc1878755f3952ea8d3d104be/psutil-6.1.0-cp36-abi3-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/df/e4/ba44652d562cbf0bf320e0f3810206149c8a4e99cdbf66da82e97ab53a15/pydantic-2.9.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/46/76/f68272e4c3a7df8777798282c5e47d508274917f29992d84e1898f8908c7/pydantic_core-2.23.4-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/14/de/866bdce10ed808323d437612aca1ec9971b981e1c52e5e42ad9b8e17a6f6/pydantic_core-2.23.4-cp312-cp312-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e5/0c/0e3c05b1c87bb6a1c76d281b0f35e78d2d80ac91b5f8f524cebf77f51049/pyparsing-3.1.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/11/c3/005fcca25ce078d2cc29fd559379817424e94885510568bc1bc53d7d5846/pytz-2024.2-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1f/a8/9837c39aba390eb7d01924ace49d761c8dbe7bc2d6082346d00c8332e431/pyzmq-26.2.0-cp310-cp310-macosx_10_15_universal2.whl + - pypi: https://files.pythonhosted.org/packages/28/2f/78a766c8913ad62b28581777ac4ede50c6d9f249d39c2963e279524a1bbe/pyzmq-26.2.0-cp312-cp312-macosx_10_15_universal2.whl - pypi: https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/43/a5/8d02f9c372790326ad405d94f04d4339482ec082455b9e6e288f7100513b/scipy-1.14.1-cp310-cp310-macosx_12_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/c8/53/35b4d41f5fd42f5781dbd0dd6c05d35ba8aa75c84ecddc7d44756cd8da2e/scipy-1.14.1-cp312-cp312-macosx_12_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/97/75/10a9ebee3fd790d20926a90a2547f0bf78f371b2f13aa822c759680ca7b9/tomli-2.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/00/d9/c33be3c1a7564f7d42d87a8d186371a75fd142097076767a5c27da941fef/tornado-6.4.1-cp38-abi3-macosx_10_9_universal2.whl - - pypi: https://files.pythonhosted.org/packages/48/5d/acf5905c36149bbaec41ccf7f2b68814647347b72075ac0b1fe3022fdc73/tqdm-4.66.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2b/78/57043611a16c655c8350b4c01b8d6abfb38cc2acb475238b62c2146186d7/tqdm-4.67.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b6/7a/e0edec9c8905e851d52076bbc41890603e2ba97cf64966bc1498f2244fd2/trove_classifiers-2024.9.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/35/35/5055ab8d215af853d07bbff1a74edf48f91ed308f037380a5ca52dd73348/trove_classifiers-2024.10.21.16-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b6/a6/8846372f55c6bb470ff7207e4dc601017e264e5fe7d79a441ece3545b36c/types_pytz-2024.2.0.20240913-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/65/58/f9c9e6be752e9fcb8b6a0ee9fb87e6e7a1f6bcab2cdc73f02bb7ba91ada0/tzdata-2024.1-py2.py3-none-any.whl @@ -551,6 +545,24 @@ packages: url: https://files.pythonhosted.org/packages/da/f1/3702ba2a7470666a62fd81c58a4c40be00670e5006a67f4d626e57f013ae/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl sha256: 8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5 requires_python: '>=3.7.0' +- kind: pypi + name: charset-normalizer + version: 3.4.0 + url: https://files.pythonhosted.org/packages/16/92/92a76dc2ff3a12e69ba94e7e05168d37d0345fa08c87e1fe24d0c2a42223/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + sha256: 8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15 + requires_python: '>=3.7.0' +- kind: pypi + name: charset-normalizer + version: 3.4.0 + url: https://files.pythonhosted.org/packages/9d/e4/9263b8240ed9472a2ae7ddc3e516e71ef46617fe40eaa51221ccd4ad9a27/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl + sha256: b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1 + requires_python: '>=3.7.0' +- kind: pypi + name: charset-normalizer + version: 3.4.0 + url: https://files.pythonhosted.org/packages/fa/44/b730e2a2580110ced837ac083d8ad222343c96bb6b66e9e4e706e4d0b6df/charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl + sha256: 4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db + requires_python: '>=3.7.0' - kind: pypi name: comm version: 0.2.2 @@ -563,8 +575,8 @@ packages: - kind: pypi name: contourpy version: 1.3.0 - url: https://files.pythonhosted.org/packages/50/d6/c953b400219443535d412fcbbc42e7a5e823291236bc0bb88936e3cc9317/contourpy-1.3.0-cp310-cp310-macosx_11_0_arm64.whl - sha256: 76c905ef940a4474a6289c71d53122a4f77766eef23c03cd57016ce19d0f7b42 + url: https://files.pythonhosted.org/packages/51/3d/aa0fe6ae67e3ef9f178389e4caaaa68daf2f9024092aa3c6032e3d174670/contourpy-1.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl + sha256: 0be4d8425bfa755e0fd76ee1e019636ccc7c29f77a7c86b4328a9eb6a26d0639 requires_dist: - numpy>=1.23 - furo ; extra == 'docs' @@ -588,8 +600,8 @@ packages: - kind: pypi name: contourpy version: 1.3.0 - url: https://files.pythonhosted.org/packages/6f/b4/6fffdf213ffccc28483c524b9dad46bb78332851133b36ad354b856ddc7c/contourpy-1.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl - sha256: 92f8557cbb07415a4d6fa191f20fd9d2d9eb9c0b61d1b2f52a8926e43c6e9af7 + url: https://files.pythonhosted.org/packages/6e/be/524e377567defac0e21a46e2a529652d165fed130a0d8a863219303cee18/contourpy-1.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + sha256: 3634b5385c6716c258d0419c46d05c8aa7dc8cb70326c9a4fb66b69ad2b52e09 requires_dist: - numpy>=1.23 - furo ; extra == 'docs' @@ -613,8 +625,8 @@ packages: - kind: pypi name: contourpy version: 1.3.0 - url: https://files.pythonhosted.org/packages/99/e6/d11966962b1aa515f5586d3907ad019f4b812c04e4546cc19ebf62b5178e/contourpy-1.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - sha256: 69375194457ad0fad3a839b9e29aa0b0ed53bb54db1bfb6c3ae43d111c31ce41 + url: https://files.pythonhosted.org/packages/e3/04/33351c5d5108460a8ce6d512307690b023f0cfcad5899499f5c83b9d63b1/contourpy-1.3.0-cp312-cp312-macosx_11_0_arm64.whl + sha256: da84c537cb8b97d153e9fb208c221c45605f73147bd4cadd23bdae915042aad6 requires_dist: - numpy>=1.23 - furo ; extra == 'docs' @@ -658,8 +670,14 @@ packages: - kind: pypi name: debugpy version: 1.8.5 - url: https://files.pythonhosted.org/packages/57/0c/c2ec581541923a4d36cee4fd2419c1211c986849fc61097f87aa81fc6ad3/debugpy-1.8.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - sha256: 4413b7a3ede757dc33a273a17d685ea2b0c09dbd312cc03f5534a0fd4d40750a + url: https://files.pythonhosted.org/packages/9a/82/7d9e1f75fb23c876ab379008c7cf484a1cfa5ed47ccaac8ba37c75e6814e/debugpy-1.8.5-cp312-cp312-macosx_12_0_universal2.whl + sha256: 5b5c770977c8ec6c40c60d6f58cacc7f7fe5a45960363d6974ddb9b62dbee156 + requires_python: '>=3.8' +- kind: pypi + name: debugpy + version: 1.8.5 + url: https://files.pythonhosted.org/packages/fd/b6/ee71d5e73712daf8307a9e85f5e39301abc8b66d13acd04dfff1702e672e/debugpy-1.8.5-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl + sha256: c0a65b00b7cdd2ee0c2cf4c7335fef31e15f1b7056c7fdbce9e90193e1a8c8cb requires_python: '>=3.8' - kind: pypi name: decorator @@ -667,31 +685,6 @@ packages: url: https://files.pythonhosted.org/packages/d5/50/83c593b07763e1161326b3b8c6686f0f4b0f24d5526546bee538c89837d6/decorator-5.1.1-py3-none-any.whl sha256: b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186 requires_python: '>=3.5' -- kind: conda - name: editables - version: '0.5' - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/editables-0.5-pyhd8ed1ab_0.conda - sha256: de160a7494e7bc72360eea6a29cbddf194d0a79f45ff417a4de20e6858cf79a9 - md5: 9873878e2a069bc358b69e9a29c1ecd5 - depends: - - python >=3.7 - license: MIT - license_family: MIT - purls: - - pkg:pypi/editables?source=hash-mapping - size: 10988 - timestamp: 1705857085102 -- kind: pypi - name: exceptiongroup - version: 1.2.2 - url: https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl - sha256: 3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b - requires_dist: - - pytest>=6 ; extra == 'test' - requires_python: '>=3.7' - kind: pypi name: executing version: 2.1.0 @@ -709,8 +702,8 @@ packages: - kind: pypi name: fonttools version: 4.54.1 - url: https://files.pythonhosted.org/packages/16/6f/b99e0c347732fb003077a2cff38c26f381969b74329aa5597e344d540fe1/fonttools-4.54.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl - sha256: 7965af9b67dd546e52afcf2e38641b5be956d68c425bef2158e95af11d229f10 + url: https://files.pythonhosted.org/packages/08/07/aa85cc62abcc940b25d14b542cf585eebf4830032a7f6a1395d696bb3231/fonttools-4.54.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl + sha256: 93d458c8a6a354dc8b48fc78d66d2a8a90b941f7fec30e94c7ad9982b1fa6bab requires_dist: - fs<3,>=2.2.0 ; extra == 'all' - lxml>=4.0 ; extra == 'all' @@ -746,8 +739,8 @@ packages: - kind: pypi name: fonttools version: 4.54.1 - url: https://files.pythonhosted.org/packages/2f/9a/9d899e7ae55b0dd30632e6ca36c0f5fa1205b1b096ec171c9be903673058/fonttools-4.54.1-cp310-cp310-macosx_11_0_arm64.whl - sha256: 41bb0b250c8132b2fcac148e2e9198e62ff06f3cc472065dff839327945c5882 + url: https://files.pythonhosted.org/packages/67/9d/cfbfe36e5061a8f68b154454ba2304eb01f40d4ba9b63e41d9058909baed/fonttools-4.54.1-cp312-cp312-macosx_11_0_arm64.whl + sha256: 8fa92cb248e573daab8d032919623cc309c005086d743afb014c836636166f08 requires_dist: - fs<3,>=2.2.0 ; extra == 'all' - lxml>=4.0 ; extra == 'all' @@ -783,8 +776,8 @@ packages: - kind: pypi name: fonttools version: 4.54.1 - url: https://files.pythonhosted.org/packages/e5/12/9a45294a7c4520cc32936edd15df1d5c24af701d2f5f51070a9a43d7664b/fonttools-4.54.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - sha256: 278913a168f90d53378c20c23b80f4e599dca62fbffae4cc620c8eed476b723e + url: https://files.pythonhosted.org/packages/90/41/5573e074739efd9227dd23647724f01f6f07ad062fe09d02e91c5549dcf7/fonttools-4.54.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl + sha256: 0a911591200114969befa7f2cb74ac148bce5a91df5645443371aba6d222e263 requires_dist: - fs<3,>=2.2.0 ; extra == 'all' - lxml>=4.0 ; extra == 'all' @@ -819,11 +812,11 @@ packages: requires_python: '>=3.8' - kind: pypi name: hatchling - version: 1.25.0 - url: https://files.pythonhosted.org/packages/0c/8b/90e80904fdc24ce33f6fc6f35ebd2232fe731a8528a22008458cf197bc4d/hatchling-1.25.0-py3-none-any.whl - sha256: b47948e45d4d973034584dd4cb39c14b6a70227cf287ab7ec0ad7983408a882c + version: 1.26.3 + url: https://files.pythonhosted.org/packages/72/41/b3e29dc4fe623794070e5dfbb9915acb649ce05d6472f005470cbed9de83/hatchling-1.26.3-py3-none-any.whl + sha256: c407e1c6c17b574584a66ae60e8e9a01235ecb6dc61d01559bb936577aaf5846 requires_dist: - - packaging>=23.2 + - packaging>=24.2 - pathspec>=0.10.1 - pluggy>=1.0.0 - tomli>=1.2.2 ; python_full_version < '3.11' @@ -1061,20 +1054,20 @@ packages: - kind: pypi name: kiwisolver version: 1.4.7 - url: https://files.pythonhosted.org/packages/12/5d/c36140313f2510e20207708adf36ae4919416d697ee0236b0ddfb6fd1050/kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl - sha256: 88a9ca9c710d598fd75ee5de59d5bda2684d9db36a9f50b6125eaea3969c2599 + url: https://files.pythonhosted.org/packages/21/e4/c0b6746fd2eb62fe702118b3ca0cb384ce95e1261cfada58ff693aeec08a/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + sha256: 693902d433cf585133699972b6d7c42a8b9f8f826ebcaf0132ff55200afc599e requires_python: '>=3.8' - kind: pypi name: kiwisolver version: 1.4.7 - url: https://files.pythonhosted.org/packages/55/91/0a57ce324caf2ff5403edab71c508dd8f648094b18cfbb4c8cc0fde4a6ac/kiwisolver-1.4.7-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl - sha256: 88f17c5ffa8e9462fb79f62746428dd57b46eb931698e42e990ad63103f35e6c + url: https://files.pythonhosted.org/packages/80/c5/57fa58276dfdfa612241d640a64ca2f76adc6ffcebdbd135b4ef60095098/kiwisolver-1.4.7-cp312-cp312-macosx_11_0_arm64.whl + sha256: 48b571ecd8bae15702e4f22d3ff6a0f13e54d3d00cd25216d5e7f658242065ee requires_python: '>=3.8' - kind: pypi name: kiwisolver version: 1.4.7 - url: https://files.pythonhosted.org/packages/ef/fa/65de49c85838681fc9cb05de2a68067a683717321e01ddafb5b8024286f0/kiwisolver-1.4.7-cp310-cp310-macosx_11_0_arm64.whl - sha256: aa0abdf853e09aff551db11fce173e2177d00786c688203f52c87ad7fcd91ef9 + url: https://files.pythonhosted.org/packages/b6/67/3f4850b5e6cffb75ec40577ddf54f7b82b15269cc5097ff2e968ee32ea7d/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl + sha256: 612a10bdae23404a72941a0fc8fa2660c6ea1217c4ce0dbcab8a8f6543ea9e7f requires_python: '>=3.8' - kind: conda name: ld_impl_linux-64 @@ -1108,6 +1101,58 @@ packages: purls: [] size: 735885 timestamp: 1718625653417 +- kind: conda + name: libexpat + version: 2.6.4 + build: h286801f_0 + subdir: osx-arm64 + url: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.6.4-h286801f_0.conda + sha256: e42ab5ace927ee7c84e3f0f7d813671e1cf3529f5f06ee5899606630498c2745 + md5: 38d2656dd914feb0cab8c629370768bf + depends: + - __osx >=11.0 + constrains: + - expat 2.6.4.* + license: MIT + license_family: MIT + purls: [] + size: 64693 + timestamp: 1730967175868 +- kind: conda + name: libexpat + version: 2.6.4 + build: h5888daf_0 + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.6.4-h5888daf_0.conda + sha256: 56541b98447b58e52d824bd59d6382d609e11de1f8adf20b23143e353d2b8d26 + md5: db833e03127376d461e1e13e76f09b6c + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + constrains: + - expat 2.6.4.* + license: MIT + license_family: MIT + purls: [] + size: 73304 + timestamp: 1730967041968 +- kind: conda + name: libexpat + version: 2.6.4 + build: h5ad3122_0 + subdir: linux-aarch64 + url: https://conda.anaconda.org/conda-forge/linux-aarch64/libexpat-2.6.4-h5ad3122_0.conda + sha256: f42e758009ba9db90d1fe7992bc3e60d0c52f71fb20923375d2c44ae69a5a2b3 + md5: f1b3fab36861b3ce945a13f0dfdfc688 + depends: + - libgcc >=13 + constrains: + - expat 2.6.4.* + license: MIT + license_family: MIT + purls: [] + size: 72345 + timestamp: 1730967203789 - kind: conda name: libffi version: 3.4.2 @@ -1448,8 +1493,8 @@ packages: - kind: pypi name: matplotlib version: 3.9.2 - url: https://files.pythonhosted.org/packages/36/98/cbacbd30241369d099f9c13a2b6bc3b7068d85214f5b5795e583ac3d8aba/matplotlib-3.9.2-cp310-cp310-macosx_11_0_arm64.whl - sha256: c375cc72229614632c87355366bdf2570c2dac01ac66b8ad048d2dabadf2d0d4 + url: https://files.pythonhosted.org/packages/27/75/de5b9cd67648051cae40039da0c8cbc497a0d99acb1a1f3d087cd66d27b7/matplotlib-3.9.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + sha256: f6ee45bc4245533111ced13f1f2cace1e7f89d1c793390392a80c139d6cf0e6c requires_dist: - contourpy>=1.0.1 - cycler>=0.10 @@ -1470,8 +1515,8 @@ packages: - kind: pypi name: matplotlib version: 3.9.2 - url: https://files.pythonhosted.org/packages/8d/9d/d06860390f9d154fa884f1740a5456378fb153ff57443c91a4a32bab7092/matplotlib-3.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - sha256: ab68d50c06938ef28681073327795c5db99bb4666214d2d5f880ed11aeaded66 + url: https://files.pythonhosted.org/packages/35/3e/5713b84a02b24b2a4bd4d6673bfc03017e6654e1d8793ece783b7ed4d484/matplotlib-3.9.2-cp312-cp312-macosx_11_0_arm64.whl + sha256: be0fc24a5e4531ae4d8e858a1a548c1fe33b176bb13eff7f9d0d38ce5112a27d requires_dist: - contourpy>=1.0.1 - cycler>=0.10 @@ -1492,8 +1537,8 @@ packages: - kind: pypi name: matplotlib version: 3.9.2 - url: https://files.pythonhosted.org/packages/a8/a0/917f3c6d3a8774a3a1502d9f3dfc1456e07c1fa0c211a23b75a69e154180/matplotlib-3.9.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl - sha256: 1d94ff717eb2bd0b58fe66380bd8b14ac35f48a98e7c6765117fe67fb7684e64 + url: https://files.pythonhosted.org/packages/5b/bd/c404502aa1824456d2862dd6b9b0c1917761a51a32f7f83ff8cf94b6d117/matplotlib-3.9.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl + sha256: bf81de2926c2db243c9b2cbc3917619a0fc85796c6ba4e58f541df814bbf83c7 requires_dist: - contourpy>=1.0.1 - cycler>=0.10 @@ -1577,12 +1622,30 @@ packages: url: https://files.pythonhosted.org/packages/69/30/f41c9b6dab4e1ec56b40d1daa81ce9f9f8d26da6d02af18768a883676bd5/numpy-2.1.1-cp310-cp310-macosx_11_0_arm64.whl sha256: 7dd86dfaf7c900c0bbdcb8b16e2f6ddf1eb1fe39c6c8cca6e94844ed3152a8fd requires_python: '>=3.10' +- kind: pypi + name: numpy + version: 2.1.1 + url: https://files.pythonhosted.org/packages/6b/6c/a9fbef5fd2f9685212af2a9e47485cde9357c3e303e079ccf85127516f2d/numpy-2.1.1-cp312-cp312-macosx_11_0_arm64.whl + sha256: 6435c48250c12f001920f0751fe50c0348f5f240852cfddc5e2f97e007544cbe + requires_python: '>=3.10' - kind: pypi name: numpy version: 2.1.1 url: https://files.pythonhosted.org/packages/7d/4b/a509d346fffede6120cc17610cc500819417ee9c3da7f08d9aaf15cab2a3/numpy-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl sha256: 913cc1d311060b1d409e609947fa1b9753701dac96e6581b58afc36b7ee35af6 requires_python: '>=3.10' +- kind: pypi + name: numpy + version: 2.1.1 + url: https://files.pythonhosted.org/packages/9f/8a/76ddef3e621541ddd6984bc24d256a4e3422d036790cbbe449e6cad439ee/numpy-2.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl + sha256: fcd8f556cdc8cfe35e70efb92463082b7f43dd7e547eb071ffc36abc0ca4699b + requires_python: '>=3.10' +- kind: pypi + name: numpy + version: 2.1.1 + url: https://files.pythonhosted.org/packages/cb/22/2b840d297183916a95847c11f82ae11e248fa98113490b2357f774651e1d/numpy-2.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + sha256: d2b9cd92c8f8e7b313b80e93cedc12c0112088541dcedd9197b5dee3738c1201 + requires_python: '>=3.10' - kind: pypi name: numpy version: 2.1.1 @@ -1640,9 +1703,9 @@ packages: timestamp: 1725410790053 - kind: pypi name: packaging - version: '24.1' - url: https://files.pythonhosted.org/packages/08/aa/cc0199a5f0ad350994d660967a8efb233fe0416e4639146c089643407ce6/packaging-24.1-py3-none-any.whl - sha256: 5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124 + version: '24.2' + url: https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl + sha256: 09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759 requires_python: '>=3.8' - kind: pypi name: pandas @@ -1736,6 +1799,98 @@ packages: - xlsxwriter>=3.0.5 ; extra == 'all' - zstandard>=0.19.0 ; extra == 'all' requires_python: '>=3.9' +- kind: pypi + name: pandas + version: 2.2.2 + url: https://files.pythonhosted.org/packages/40/10/79e52ef01dfeb1c1ca47a109a01a248754ebe990e159a844ece12914de83/pandas-2.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + sha256: eee3a87076c0756de40b05c5e9a6069c035ba43e8dd71c379e68cab2c20f16ad + requires_dist: + - numpy>=1.22.4 ; python_full_version < '3.11' + - numpy>=1.23.2 ; python_full_version == '3.11.*' + - numpy>=1.26.0 ; python_full_version >= '3.12' + - python-dateutil>=2.8.2 + - pytz>=2020.1 + - tzdata>=2022.7 + - hypothesis>=6.46.1 ; extra == 'test' + - pytest>=7.3.2 ; extra == 'test' + - pytest-xdist>=2.2.0 ; extra == 'test' + - pyarrow>=10.0.1 ; extra == 'pyarrow' + - bottleneck>=1.3.6 ; extra == 'performance' + - numba>=0.56.4 ; extra == 'performance' + - numexpr>=2.8.4 ; extra == 'performance' + - scipy>=1.10.0 ; extra == 'computation' + - xarray>=2022.12.0 ; extra == 'computation' + - fsspec>=2022.11.0 ; extra == 'fss' + - s3fs>=2022.11.0 ; extra == 'aws' + - gcsfs>=2022.11.0 ; extra == 'gcp' + - pandas-gbq>=0.19.0 ; extra == 'gcp' + - odfpy>=1.4.1 ; extra == 'excel' + - openpyxl>=3.1.0 ; extra == 'excel' + - python-calamine>=0.1.7 ; extra == 'excel' + - pyxlsb>=1.0.10 ; extra == 'excel' + - xlrd>=2.0.1 ; extra == 'excel' + - xlsxwriter>=3.0.5 ; extra == 'excel' + - pyarrow>=10.0.1 ; extra == 'parquet' + - pyarrow>=10.0.1 ; extra == 'feather' + - tables>=3.8.0 ; extra == 'hdf5' + - pyreadstat>=1.2.0 ; extra == 'spss' + - sqlalchemy>=2.0.0 ; extra == 'postgresql' + - psycopg2>=2.9.6 ; extra == 'postgresql' + - adbc-driver-postgresql>=0.8.0 ; extra == 'postgresql' + - sqlalchemy>=2.0.0 ; extra == 'mysql' + - pymysql>=1.0.2 ; extra == 'mysql' + - sqlalchemy>=2.0.0 ; extra == 'sql-other' + - adbc-driver-postgresql>=0.8.0 ; extra == 'sql-other' + - adbc-driver-sqlite>=0.8.0 ; extra == 'sql-other' + - beautifulsoup4>=4.11.2 ; extra == 'html' + - html5lib>=1.1 ; extra == 'html' + - lxml>=4.9.2 ; extra == 'html' + - lxml>=4.9.2 ; extra == 'xml' + - matplotlib>=3.6.3 ; extra == 'plot' + - jinja2>=3.1.2 ; extra == 'output-formatting' + - tabulate>=0.9.0 ; extra == 'output-formatting' + - pyqt5>=5.15.9 ; extra == 'clipboard' + - qtpy>=2.3.0 ; extra == 'clipboard' + - zstandard>=0.19.0 ; extra == 'compression' + - dataframe-api-compat>=0.1.7 ; extra == 'consortium-standard' + - adbc-driver-postgresql>=0.8.0 ; extra == 'all' + - adbc-driver-sqlite>=0.8.0 ; extra == 'all' + - beautifulsoup4>=4.11.2 ; extra == 'all' + - bottleneck>=1.3.6 ; extra == 'all' + - dataframe-api-compat>=0.1.7 ; extra == 'all' + - fastparquet>=2022.12.0 ; extra == 'all' + - fsspec>=2022.11.0 ; extra == 'all' + - gcsfs>=2022.11.0 ; extra == 'all' + - html5lib>=1.1 ; extra == 'all' + - hypothesis>=6.46.1 ; extra == 'all' + - jinja2>=3.1.2 ; extra == 'all' + - lxml>=4.9.2 ; extra == 'all' + - matplotlib>=3.6.3 ; extra == 'all' + - numba>=0.56.4 ; extra == 'all' + - numexpr>=2.8.4 ; extra == 'all' + - odfpy>=1.4.1 ; extra == 'all' + - openpyxl>=3.1.0 ; extra == 'all' + - pandas-gbq>=0.19.0 ; extra == 'all' + - psycopg2>=2.9.6 ; extra == 'all' + - pyarrow>=10.0.1 ; extra == 'all' + - pymysql>=1.0.2 ; extra == 'all' + - pyqt5>=5.15.9 ; extra == 'all' + - pyreadstat>=1.2.0 ; extra == 'all' + - pytest>=7.3.2 ; extra == 'all' + - pytest-xdist>=2.2.0 ; extra == 'all' + - python-calamine>=0.1.7 ; extra == 'all' + - pyxlsb>=1.0.10 ; extra == 'all' + - qtpy>=2.3.0 ; extra == 'all' + - scipy>=1.10.0 ; extra == 'all' + - s3fs>=2022.11.0 ; extra == 'all' + - sqlalchemy>=2.0.0 ; extra == 'all' + - tables>=3.8.0 ; extra == 'all' + - tabulate>=0.9.0 ; extra == 'all' + - xarray>=2022.12.0 ; extra == 'all' + - xlrd>=2.0.1 ; extra == 'all' + - xlsxwriter>=3.0.5 ; extra == 'all' + - zstandard>=0.19.0 ; extra == 'all' + requires_python: '>=3.9' - kind: pypi name: pandas version: 2.2.2 @@ -1828,6 +1983,190 @@ packages: - xlsxwriter>=3.0.5 ; extra == 'all' - zstandard>=0.19.0 ; extra == 'all' requires_python: '>=3.9' +- kind: pypi + name: pandas + version: 2.2.2 + url: https://files.pythonhosted.org/packages/b0/85/f95b5f322e1ae13b7ed7e97bd999160fa003424711ab4dc8344b8772c270/pandas-2.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl + sha256: 1cb51fe389360f3b5a4d57dbd2848a5f033350336ca3b340d1c53a1fad33bcad + requires_dist: + - numpy>=1.22.4 ; python_full_version < '3.11' + - numpy>=1.23.2 ; python_full_version == '3.11.*' + - numpy>=1.26.0 ; python_full_version >= '3.12' + - python-dateutil>=2.8.2 + - pytz>=2020.1 + - tzdata>=2022.7 + - hypothesis>=6.46.1 ; extra == 'test' + - pytest>=7.3.2 ; extra == 'test' + - pytest-xdist>=2.2.0 ; extra == 'test' + - pyarrow>=10.0.1 ; extra == 'pyarrow' + - bottleneck>=1.3.6 ; extra == 'performance' + - numba>=0.56.4 ; extra == 'performance' + - numexpr>=2.8.4 ; extra == 'performance' + - scipy>=1.10.0 ; extra == 'computation' + - xarray>=2022.12.0 ; extra == 'computation' + - fsspec>=2022.11.0 ; extra == 'fss' + - s3fs>=2022.11.0 ; extra == 'aws' + - gcsfs>=2022.11.0 ; extra == 'gcp' + - pandas-gbq>=0.19.0 ; extra == 'gcp' + - odfpy>=1.4.1 ; extra == 'excel' + - openpyxl>=3.1.0 ; extra == 'excel' + - python-calamine>=0.1.7 ; extra == 'excel' + - pyxlsb>=1.0.10 ; extra == 'excel' + - xlrd>=2.0.1 ; extra == 'excel' + - xlsxwriter>=3.0.5 ; extra == 'excel' + - pyarrow>=10.0.1 ; extra == 'parquet' + - pyarrow>=10.0.1 ; extra == 'feather' + - tables>=3.8.0 ; extra == 'hdf5' + - pyreadstat>=1.2.0 ; extra == 'spss' + - sqlalchemy>=2.0.0 ; extra == 'postgresql' + - psycopg2>=2.9.6 ; extra == 'postgresql' + - adbc-driver-postgresql>=0.8.0 ; extra == 'postgresql' + - sqlalchemy>=2.0.0 ; extra == 'mysql' + - pymysql>=1.0.2 ; extra == 'mysql' + - sqlalchemy>=2.0.0 ; extra == 'sql-other' + - adbc-driver-postgresql>=0.8.0 ; extra == 'sql-other' + - adbc-driver-sqlite>=0.8.0 ; extra == 'sql-other' + - beautifulsoup4>=4.11.2 ; extra == 'html' + - html5lib>=1.1 ; extra == 'html' + - lxml>=4.9.2 ; extra == 'html' + - lxml>=4.9.2 ; extra == 'xml' + - matplotlib>=3.6.3 ; extra == 'plot' + - jinja2>=3.1.2 ; extra == 'output-formatting' + - tabulate>=0.9.0 ; extra == 'output-formatting' + - pyqt5>=5.15.9 ; extra == 'clipboard' + - qtpy>=2.3.0 ; extra == 'clipboard' + - zstandard>=0.19.0 ; extra == 'compression' + - dataframe-api-compat>=0.1.7 ; extra == 'consortium-standard' + - adbc-driver-postgresql>=0.8.0 ; extra == 'all' + - adbc-driver-sqlite>=0.8.0 ; extra == 'all' + - beautifulsoup4>=4.11.2 ; extra == 'all' + - bottleneck>=1.3.6 ; extra == 'all' + - dataframe-api-compat>=0.1.7 ; extra == 'all' + - fastparquet>=2022.12.0 ; extra == 'all' + - fsspec>=2022.11.0 ; extra == 'all' + - gcsfs>=2022.11.0 ; extra == 'all' + - html5lib>=1.1 ; extra == 'all' + - hypothesis>=6.46.1 ; extra == 'all' + - jinja2>=3.1.2 ; extra == 'all' + - lxml>=4.9.2 ; extra == 'all' + - matplotlib>=3.6.3 ; extra == 'all' + - numba>=0.56.4 ; extra == 'all' + - numexpr>=2.8.4 ; extra == 'all' + - odfpy>=1.4.1 ; extra == 'all' + - openpyxl>=3.1.0 ; extra == 'all' + - pandas-gbq>=0.19.0 ; extra == 'all' + - psycopg2>=2.9.6 ; extra == 'all' + - pyarrow>=10.0.1 ; extra == 'all' + - pymysql>=1.0.2 ; extra == 'all' + - pyqt5>=5.15.9 ; extra == 'all' + - pyreadstat>=1.2.0 ; extra == 'all' + - pytest>=7.3.2 ; extra == 'all' + - pytest-xdist>=2.2.0 ; extra == 'all' + - python-calamine>=0.1.7 ; extra == 'all' + - pyxlsb>=1.0.10 ; extra == 'all' + - qtpy>=2.3.0 ; extra == 'all' + - scipy>=1.10.0 ; extra == 'all' + - s3fs>=2022.11.0 ; extra == 'all' + - sqlalchemy>=2.0.0 ; extra == 'all' + - tables>=3.8.0 ; extra == 'all' + - tabulate>=0.9.0 ; extra == 'all' + - xarray>=2022.12.0 ; extra == 'all' + - xlrd>=2.0.1 ; extra == 'all' + - xlsxwriter>=3.0.5 ; extra == 'all' + - zstandard>=0.19.0 ; extra == 'all' + requires_python: '>=3.9' +- kind: pypi + name: pandas + version: 2.2.2 + url: https://files.pythonhosted.org/packages/db/7c/9a60add21b96140e22465d9adf09832feade45235cd22f4cb1668a25e443/pandas-2.2.2-cp312-cp312-macosx_11_0_arm64.whl + sha256: e9b79011ff7a0f4b1d6da6a61aa1aa604fb312d6647de5bad20013682d1429ce + requires_dist: + - numpy>=1.22.4 ; python_full_version < '3.11' + - numpy>=1.23.2 ; python_full_version == '3.11.*' + - numpy>=1.26.0 ; python_full_version >= '3.12' + - python-dateutil>=2.8.2 + - pytz>=2020.1 + - tzdata>=2022.7 + - hypothesis>=6.46.1 ; extra == 'test' + - pytest>=7.3.2 ; extra == 'test' + - pytest-xdist>=2.2.0 ; extra == 'test' + - pyarrow>=10.0.1 ; extra == 'pyarrow' + - bottleneck>=1.3.6 ; extra == 'performance' + - numba>=0.56.4 ; extra == 'performance' + - numexpr>=2.8.4 ; extra == 'performance' + - scipy>=1.10.0 ; extra == 'computation' + - xarray>=2022.12.0 ; extra == 'computation' + - fsspec>=2022.11.0 ; extra == 'fss' + - s3fs>=2022.11.0 ; extra == 'aws' + - gcsfs>=2022.11.0 ; extra == 'gcp' + - pandas-gbq>=0.19.0 ; extra == 'gcp' + - odfpy>=1.4.1 ; extra == 'excel' + - openpyxl>=3.1.0 ; extra == 'excel' + - python-calamine>=0.1.7 ; extra == 'excel' + - pyxlsb>=1.0.10 ; extra == 'excel' + - xlrd>=2.0.1 ; extra == 'excel' + - xlsxwriter>=3.0.5 ; extra == 'excel' + - pyarrow>=10.0.1 ; extra == 'parquet' + - pyarrow>=10.0.1 ; extra == 'feather' + - tables>=3.8.0 ; extra == 'hdf5' + - pyreadstat>=1.2.0 ; extra == 'spss' + - sqlalchemy>=2.0.0 ; extra == 'postgresql' + - psycopg2>=2.9.6 ; extra == 'postgresql' + - adbc-driver-postgresql>=0.8.0 ; extra == 'postgresql' + - sqlalchemy>=2.0.0 ; extra == 'mysql' + - pymysql>=1.0.2 ; extra == 'mysql' + - sqlalchemy>=2.0.0 ; extra == 'sql-other' + - adbc-driver-postgresql>=0.8.0 ; extra == 'sql-other' + - adbc-driver-sqlite>=0.8.0 ; extra == 'sql-other' + - beautifulsoup4>=4.11.2 ; extra == 'html' + - html5lib>=1.1 ; extra == 'html' + - lxml>=4.9.2 ; extra == 'html' + - lxml>=4.9.2 ; extra == 'xml' + - matplotlib>=3.6.3 ; extra == 'plot' + - jinja2>=3.1.2 ; extra == 'output-formatting' + - tabulate>=0.9.0 ; extra == 'output-formatting' + - pyqt5>=5.15.9 ; extra == 'clipboard' + - qtpy>=2.3.0 ; extra == 'clipboard' + - zstandard>=0.19.0 ; extra == 'compression' + - dataframe-api-compat>=0.1.7 ; extra == 'consortium-standard' + - adbc-driver-postgresql>=0.8.0 ; extra == 'all' + - adbc-driver-sqlite>=0.8.0 ; extra == 'all' + - beautifulsoup4>=4.11.2 ; extra == 'all' + - bottleneck>=1.3.6 ; extra == 'all' + - dataframe-api-compat>=0.1.7 ; extra == 'all' + - fastparquet>=2022.12.0 ; extra == 'all' + - fsspec>=2022.11.0 ; extra == 'all' + - gcsfs>=2022.11.0 ; extra == 'all' + - html5lib>=1.1 ; extra == 'all' + - hypothesis>=6.46.1 ; extra == 'all' + - jinja2>=3.1.2 ; extra == 'all' + - lxml>=4.9.2 ; extra == 'all' + - matplotlib>=3.6.3 ; extra == 'all' + - numba>=0.56.4 ; extra == 'all' + - numexpr>=2.8.4 ; extra == 'all' + - odfpy>=1.4.1 ; extra == 'all' + - openpyxl>=3.1.0 ; extra == 'all' + - pandas-gbq>=0.19.0 ; extra == 'all' + - psycopg2>=2.9.6 ; extra == 'all' + - pyarrow>=10.0.1 ; extra == 'all' + - pymysql>=1.0.2 ; extra == 'all' + - pyqt5>=5.15.9 ; extra == 'all' + - pyreadstat>=1.2.0 ; extra == 'all' + - pytest>=7.3.2 ; extra == 'all' + - pytest-xdist>=2.2.0 ; extra == 'all' + - python-calamine>=0.1.7 ; extra == 'all' + - pyxlsb>=1.0.10 ; extra == 'all' + - qtpy>=2.3.0 ; extra == 'all' + - scipy>=1.10.0 ; extra == 'all' + - s3fs>=2022.11.0 ; extra == 'all' + - sqlalchemy>=2.0.0 ; extra == 'all' + - tables>=3.8.0 ; extra == 'all' + - tabulate>=0.9.0 ; extra == 'all' + - xarray>=2022.12.0 ; extra == 'all' + - xlrd>=2.0.1 ; extra == 'all' + - xlsxwriter>=3.0.5 ; extra == 'all' + - zstandard>=0.19.0 ; extra == 'all' + requires_python: '>=3.9' - kind: pypi name: pandas version: 2.2.2 @@ -1957,8 +2296,8 @@ packages: - kind: pypi name: pillow version: 10.4.0 - url: https://files.pythonhosted.org/packages/9a/9e/4143b907be8ea0bce215f2ae4f7480027473f8b61fcedfda9d851082a5d2/pillow-10.4.0-cp310-cp310-macosx_11_0_arm64.whl - sha256: 543f3dc61c18dafb755773efc89aae60d06b6596a63914107f75459cf984164d + url: https://files.pythonhosted.org/packages/55/77/40daddf677897a923d5d33329acd52a2144d54a9644f2a5422c028c6bf2d/pillow-10.4.0-cp312-cp312-manylinux_2_28_x86_64.whl + sha256: 86dcb5a1eb778d8b25659d5e4341269e8590ad6b4e8b44d9f4b07f8d136c414a requires_dist: - furo ; extra == 'docs' - olefile ; extra == 'docs' @@ -1984,8 +2323,8 @@ packages: - kind: pypi name: pillow version: 10.4.0 - url: https://files.pythonhosted.org/packages/b1/84/9a15cc5726cbbfe7f9f90bfb11f5d028586595907cd093815ca6644932e3/pillow-10.4.0-cp310-cp310-manylinux_2_28_aarch64.whl - sha256: 6c762a5b0997f5659a5ef2266abc1d8851ad7749ad9a6a5506eb23d314e4f46b + url: https://files.pythonhosted.org/packages/7d/1b/c14b4197b80150fb64453585247e6fb2e1d93761fa0fa9cf63b102fde822/pillow-10.4.0-cp312-cp312-manylinux_2_28_aarch64.whl + sha256: f5b92f4d70791b4a67157321c4e8225d60b119c5cc9aee8ecf153aace4aad4ef requires_dist: - furo ; extra == 'docs' - olefile ; extra == 'docs' @@ -2011,8 +2350,8 @@ packages: - kind: pypi name: pillow version: 10.4.0 - url: https://files.pythonhosted.org/packages/b5/5b/6651c288b08df3b8c1e2f8c1152201e0b25d240e22ddade0f1e242fc9fa0/pillow-10.4.0-cp310-cp310-manylinux_2_28_x86_64.whl - sha256: a985e028fc183bf12a77a8bbf36318db4238a3ded7fa9df1b9a133f1cb79f8fc + url: https://files.pythonhosted.org/packages/e7/cf/5c558a0f247e0bf9cec92bff9b46ae6474dd736f6d906315e60e4075f737/pillow-10.4.0-cp312-cp312-macosx_11_0_arm64.whl + sha256: 866b6942a92f56300012f5fbac71f2d610312ee65e22f1aa2609e491284e5597 requires_dist: - furo ; extra == 'docs' - olefile ; extra == 'docs' @@ -2093,39 +2432,84 @@ packages: requires_python: '>=3.7.0' - kind: pypi name: psutil - version: 6.0.0 - url: https://files.pythonhosted.org/packages/19/74/f59e7e0d392bc1070e9a70e2f9190d652487ac115bb16e2eff6b22ad1d24/psutil-6.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl - sha256: 5fd9a97c8e94059b0ef54a7d4baf13b405011176c3b6ff257c247cae0d560ecd - requires_dist: - - ipaddress ; python_full_version < '3.0' and extra == 'test' - - mock ; python_full_version < '3.0' and extra == 'test' - - enum34 ; python_full_version < '3.5' and extra == 'test' - - pywin32 ; sys_platform == 'win32' and extra == 'test' - - wmi ; sys_platform == 'win32' and extra == 'test' + version: 6.1.0 + url: https://files.pythonhosted.org/packages/1d/cb/313e80644ea407f04f6602a9e23096540d9dc1878755f3952ea8d3d104be/psutil-6.1.0-cp36-abi3-macosx_11_0_arm64.whl + sha256: 0895b8414afafc526712c498bd9de2b063deaac4021a3b3c34566283464aff8e + requires_dist: + - black ; extra == 'dev' + - check-manifest ; extra == 'dev' + - coverage ; extra == 'dev' + - packaging ; extra == 'dev' + - pylint ; extra == 'dev' + - pyperf ; extra == 'dev' + - pypinfo ; extra == 'dev' + - pytest-cov ; extra == 'dev' + - requests ; extra == 'dev' + - rstcheck ; extra == 'dev' + - ruff ; extra == 'dev' + - sphinx ; extra == 'dev' + - sphinx-rtd-theme ; extra == 'dev' + - toml-sort ; extra == 'dev' + - twine ; extra == 'dev' + - virtualenv ; extra == 'dev' + - wheel ; extra == 'dev' + - pytest ; extra == 'test' + - pytest-xdist ; extra == 'test' + - setuptools ; extra == 'test' requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*' - kind: pypi name: psutil - version: 6.0.0 - url: https://files.pythonhosted.org/packages/7c/06/63872a64c312a24fb9b4af123ee7007a306617da63ff13bcc1432386ead7/psutil-6.0.0-cp38-abi3-macosx_11_0_arm64.whl - sha256: ffe7fc9b6b36beadc8c322f84e1caff51e8703b88eee1da46d1e3a6ae11b4fd0 - requires_dist: - - ipaddress ; python_full_version < '3.0' and extra == 'test' - - mock ; python_full_version < '3.0' and extra == 'test' - - enum34 ; python_full_version < '3.5' and extra == 'test' - - pywin32 ; sys_platform == 'win32' and extra == 'test' - - wmi ; sys_platform == 'win32' and extra == 'test' + version: 6.1.0 + url: https://files.pythonhosted.org/packages/27/c2/d034856ac47e3b3cdfa9720d0e113902e615f4190d5d1bdb8df4b2015fb2/psutil-6.1.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl + sha256: d905186d647b16755a800e7263d43df08b790d709d575105d419f8b6ef65423a + requires_dist: + - black ; extra == 'dev' + - check-manifest ; extra == 'dev' + - coverage ; extra == 'dev' + - packaging ; extra == 'dev' + - pylint ; extra == 'dev' + - pyperf ; extra == 'dev' + - pypinfo ; extra == 'dev' + - pytest-cov ; extra == 'dev' + - requests ; extra == 'dev' + - rstcheck ; extra == 'dev' + - ruff ; extra == 'dev' + - sphinx ; extra == 'dev' + - sphinx-rtd-theme ; extra == 'dev' + - toml-sort ; extra == 'dev' + - twine ; extra == 'dev' + - virtualenv ; extra == 'dev' + - wheel ; extra == 'dev' + - pytest ; extra == 'test' + - pytest-xdist ; extra == 'test' + - setuptools ; extra == 'test' requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*' - kind: pypi name: psutil - version: 6.0.0 - url: https://files.pythonhosted.org/packages/cd/5f/60038e277ff0a9cc8f0c9ea3d0c5eb6ee1d2470ea3f9389d776432888e47/psutil-6.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl - sha256: e2e8d0054fc88153ca0544f5c4d554d42e33df2e009c4ff42284ac9ebdef4132 - requires_dist: - - ipaddress ; python_full_version < '3.0' and extra == 'test' - - mock ; python_full_version < '3.0' and extra == 'test' - - enum34 ; python_full_version < '3.5' and extra == 'test' - - pywin32 ; sys_platform == 'win32' and extra == 'test' - - wmi ; sys_platform == 'win32' and extra == 'test' + version: 6.1.0 + url: https://files.pythonhosted.org/packages/58/4d/8245e6f76a93c98aab285a43ea71ff1b171bcd90c9d238bf81f7021fb233/psutil-6.1.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl + sha256: 498c6979f9c6637ebc3a73b3f87f9eb1ec24e1ce53a7c5173b8508981614a90b + requires_dist: + - black ; extra == 'dev' + - check-manifest ; extra == 'dev' + - coverage ; extra == 'dev' + - packaging ; extra == 'dev' + - pylint ; extra == 'dev' + - pyperf ; extra == 'dev' + - pypinfo ; extra == 'dev' + - pytest-cov ; extra == 'dev' + - requests ; extra == 'dev' + - rstcheck ; extra == 'dev' + - ruff ; extra == 'dev' + - sphinx ; extra == 'dev' + - sphinx-rtd-theme ; extra == 'dev' + - toml-sort ; extra == 'dev' + - twine ; extra == 'dev' + - virtualenv ; extra == 'dev' + - wheel ; extra == 'dev' + - pytest ; extra == 'test' + - pytest-xdist ; extra == 'test' + - setuptools ; extra == 'test' requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*' - kind: pypi name: ptyprocess @@ -2152,6 +2536,14 @@ packages: - email-validator>=2.0.0 ; extra == 'email' - tzdata ; python_full_version >= '3.9' and sys_platform == 'win32' and extra == 'timezone' requires_python: '>=3.8' +- kind: pypi + name: pydantic-core + version: 2.23.4 + url: https://files.pythonhosted.org/packages/06/c8/7d4b708f8d05a5cbfda3243aad468052c6e99de7d0937c9146c24d9f12e9/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + sha256: 128585782e5bfa515c590ccee4b727fb76925dd04a98864182b22e89a4e6ed36 + requires_dist: + - typing-extensions>=4.6.0,!=4.7.0 + requires_python: '>=3.8' - kind: pypi name: pydantic-core version: 2.23.4 @@ -2160,6 +2552,14 @@ packages: requires_dist: - typing-extensions>=4.6.0,!=4.7.0 requires_python: '>=3.8' +- kind: pypi + name: pydantic-core + version: 2.23.4 + url: https://files.pythonhosted.org/packages/14/de/866bdce10ed808323d437612aca1ec9971b981e1c52e5e42ad9b8e17a6f6/pydantic_core-2.23.4-cp312-cp312-macosx_11_0_arm64.whl + sha256: f69a8e0b033b747bb3e36a44e7732f0c99f7edd5cea723d45bc0d6e95377ffee + requires_dist: + - typing-extensions>=4.6.0,!=4.7.0 + requires_python: '>=3.8' - kind: pypi name: pydantic-core version: 2.23.4 @@ -2176,6 +2576,14 @@ packages: requires_dist: - typing-extensions>=4.6.0,!=4.7.0 requires_python: '>=3.8' +- kind: pypi + name: pydantic-core + version: 2.23.4 + url: https://files.pythonhosted.org/packages/dc/69/8edd5c3cd48bb833a3f7ef9b81d7666ccddd3c9a635225214e044b6e8281/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl + sha256: 723314c1d51722ab28bfcd5240d858512ffd3116449c557a1336cbe3919beb87 + requires_dist: + - typing-extensions>=4.6.0,!=4.7.0 + requires_python: '>=3.8' - kind: pypi name: pygments version: 2.18.0 @@ -2301,6 +2709,96 @@ packages: purls: [] size: 13024590 timestamp: 1726850017490 +- kind: conda + name: python + version: 3.12.7 + build: h5d932e8_0_cpython + subdir: linux-aarch64 + url: https://conda.anaconda.org/conda-forge/linux-aarch64/python-3.12.7-h5d932e8_0_cpython.conda + sha256: 25570873d92d4d9490c6db780cc85e6c28bd3ff61dc1ece79f602cf82bc73bc1 + md5: e6cab21bb5787270388939cf41cc5f43 + depends: + - bzip2 >=1.0.8,<2.0a0 + - ld_impl_linux-aarch64 >=2.36.1 + - libexpat >=2.6.3,<3.0a0 + - libffi >=3.4,<4.0a0 + - libgcc >=13 + - libnsl >=2.0.1,<2.1.0a0 + - libsqlite >=3.46.1,<4.0a0 + - libuuid >=2.38.1,<3.0a0 + - libxcrypt >=4.4.36 + - libzlib >=1.3.1,<2.0a0 + - ncurses >=6.5,<7.0a0 + - openssl >=3.3.2,<4.0a0 + - readline >=8.2,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + - xz >=5.2.6,<6.0a0 + constrains: + - python_abi 3.12.* *_cp312 + license: Python-2.0 + purls: [] + size: 13762126 + timestamp: 1728057461028 +- kind: conda + name: python + version: 3.12.7 + build: h739c21a_0_cpython + subdir: osx-arm64 + url: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.12.7-h739c21a_0_cpython.conda + sha256: 45d7ca2074aa92594bd2f91a9003b338cc1df8a46b9492b7fc8167110783c3ef + md5: e0d82e57ebb456077565e6d82cd4a323 + depends: + - __osx >=11.0 + - bzip2 >=1.0.8,<2.0a0 + - libexpat >=2.6.3,<3.0a0 + - libffi >=3.4,<4.0a0 + - libsqlite >=3.46.1,<4.0a0 + - libzlib >=1.3.1,<2.0a0 + - ncurses >=6.5,<7.0a0 + - openssl >=3.3.2,<4.0a0 + - readline >=8.2,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + - xz >=5.2.6,<6.0a0 + constrains: + - python_abi 3.12.* *_cp312 + license: Python-2.0 + purls: [] + size: 12975439 + timestamp: 1728057819519 +- kind: conda + name: python + version: 3.12.7 + build: hc5c86c4_0_cpython + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/python-3.12.7-hc5c86c4_0_cpython.conda + sha256: 674be31ff152d9f0e0fe16959a45e3803a730fc4f54d87df6a9ac4e6a698c41d + md5: 0515111a9cdf69f83278f7c197db9807 + depends: + - __glibc >=2.17,<3.0.a0 + - bzip2 >=1.0.8,<2.0a0 + - ld_impl_linux-64 >=2.36.1 + - libexpat >=2.6.3,<3.0a0 + - libffi >=3.4,<4.0a0 + - libgcc >=13 + - libnsl >=2.0.1,<2.1.0a0 + - libsqlite >=3.46.1,<4.0a0 + - libuuid >=2.38.1,<3.0a0 + - libxcrypt >=4.4.36 + - libzlib >=1.3.1,<2.0a0 + - ncurses >=6.5,<7.0a0 + - openssl >=3.3.2,<4.0a0 + - readline >=8.2,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + - xz >=5.2.6,<6.0a0 + constrains: + - python_abi 3.12.* *_cp312 + license: Python-2.0 + purls: [] + size: 31574780 + timestamp: 1728059777603 - kind: pypi name: python-dateutil version: 2.9.0.post0 @@ -2317,24 +2815,24 @@ packages: - kind: pypi name: pyzmq version: 26.2.0 - url: https://files.pythonhosted.org/packages/16/29/ca99b4598a9dc7e468b5417eda91f372b595be1e3eec9b7cbe8e5d3584e8/pyzmq-26.2.0-cp310-cp310-manylinux_2_28_x86_64.whl - sha256: a2224fa4a4c2ee872886ed00a571f5e967c85e078e8e8c2530a2fb01b3309b88 + url: https://files.pythonhosted.org/packages/07/3b/44ea6266a6761e9eefaa37d98fabefa112328808ac41aa87b4bbb668af30/pyzmq-26.2.0-cp312-cp312-manylinux_2_28_x86_64.whl + sha256: 7f98f6dfa8b8ccaf39163ce872bddacca38f6a67289116c8937a02e30bbe9711 requires_dist: - cffi ; implementation_name == 'pypy' requires_python: '>=3.7' - kind: pypi name: pyzmq version: 26.2.0 - url: https://files.pythonhosted.org/packages/1f/a8/9837c39aba390eb7d01924ace49d761c8dbe7bc2d6082346d00c8332e431/pyzmq-26.2.0-cp310-cp310-macosx_10_15_universal2.whl - sha256: ddf33d97d2f52d89f6e6e7ae66ee35a4d9ca6f36eda89c24591b0c40205a3629 + url: https://files.pythonhosted.org/packages/28/2f/78a766c8913ad62b28581777ac4ede50c6d9f249d39c2963e279524a1bbe/pyzmq-26.2.0-cp312-cp312-macosx_10_15_universal2.whl + sha256: ded0fc7d90fe93ae0b18059930086c51e640cdd3baebdc783a695c77f123dcd9 requires_dist: - cffi ; implementation_name == 'pypy' requires_python: '>=3.7' - kind: pypi name: pyzmq version: 26.2.0 - url: https://files.pythonhosted.org/packages/b6/09/b51b6683fde5ca04593a57bbe81788b6b43114d8f8ee4e80afc991e14760/pyzmq-26.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl - sha256: 89289a5ee32ef6c439086184529ae060c741334b8970a6855ec0b6ad3ff28764 + url: https://files.pythonhosted.org/packages/4f/ef/5a23ec689ff36d7625b38d121ef15abfc3631a9aecb417baf7a4245e4124/pyzmq-26.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl + sha256: 55cf66647e49d4621a7e20c8d13511ef1fe1efbbccf670811864452487007e08 requires_dist: - cffi ; implementation_name == 'pypy' requires_python: '>=3.7' @@ -2404,8 +2902,8 @@ packages: - kind: pypi name: scipy version: 1.14.1 - url: https://files.pythonhosted.org/packages/43/a5/8d02f9c372790326ad405d94f04d4339482ec082455b9e6e288f7100513b/scipy-1.14.1-cp310-cp310-macosx_12_0_arm64.whl - sha256: d0d2821003174de06b69e58cef2316a6622b60ee613121199cb2852a873f8cf3 + url: https://files.pythonhosted.org/packages/8e/ee/8a26858ca517e9c64f84b4c7734b89bda8e63bec85c3d2f432d225bb1886/scipy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + sha256: 8f9ea80f2e65bdaa0b7627fb00cbeb2daf163caa015e59b7516395fe3bd1e066 requires_dist: - numpy<2.3,>=1.23.5 - pytest ; extra == 'test' @@ -2446,8 +2944,8 @@ packages: - kind: pypi name: scipy version: 1.14.1 - url: https://files.pythonhosted.org/packages/47/78/b0c2c23880dd1e99e938ad49ccfb011ae353758a2dc5ed7ee59baff684c3/scipy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - sha256: 8e32dced201274bf96899e6491d9ba3e9a5f6b336708656466ad0522d8528f69 + url: https://files.pythonhosted.org/packages/c8/53/35b4d41f5fd42f5781dbd0dd6c05d35ba8aa75c84ecddc7d44756cd8da2e/scipy-1.14.1-cp312-cp312-macosx_12_0_arm64.whl + sha256: af29a935803cc707ab2ed7791c44288a682f9c8107bc00f0eccc4f92c08d6e07 requires_dist: - numpy<2.3,>=1.23.5 - pytest ; extra == 'test' @@ -2488,8 +2986,8 @@ packages: - kind: pypi name: scipy version: 1.14.1 - url: https://files.pythonhosted.org/packages/d8/df/cdb6be5274bc694c4c22862ac3438cb04f360ed9df0aecee02ce0b798380/scipy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl - sha256: 2ff0a7e01e422c15739ecd64432743cf7aae2b03f3084288f399affcefe5222d + url: https://files.pythonhosted.org/packages/f0/5a/efa92a58dc3a2898705f1dc9dbaf390ca7d4fba26d6ab8cfffb0c72f656f/scipy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl + sha256: 30ac8812c1d2aab7131a79ba62933a2a76f582d5dbbc695192453dae67ad6310 requires_dist: - numpy<2.3,>=1.23.5 - pytest ; extra == 'test' @@ -2613,12 +3111,6 @@ packages: purls: [] size: 3318875 timestamp: 1699202167581 -- kind: pypi - name: tomli - version: 2.0.1 - url: https://files.pythonhosted.org/packages/97/75/10a9ebee3fd790d20926a90a2547f0bf78f371b2f13aa822c759680ca7b9/tomli-2.0.1-py3-none-any.whl - sha256: 939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc - requires_python: '>=3.7' - kind: pypi name: tornado version: 6.4.1 @@ -2652,6 +3144,22 @@ packages: - slack-sdk ; extra == 'slack' - requests ; extra == 'telegram' requires_python: '>=3.7' +- kind: pypi + name: tqdm + version: 4.67.0 + url: https://files.pythonhosted.org/packages/2b/78/57043611a16c655c8350b4c01b8d6abfb38cc2acb475238b62c2146186d7/tqdm-4.67.0-py3-none-any.whl + sha256: 0cd8af9d56911acab92182e88d763100d4788bdf421d251616040cc4d44863be + requires_dist: + - colorama ; platform_system == 'Windows' + - pytest>=6 ; extra == 'dev' + - pytest-cov ; extra == 'dev' + - pytest-timeout ; extra == 'dev' + - pytest-xdist ; extra == 'dev' + - requests ; extra == 'discord' + - ipywidgets>=6 ; extra == 'notebook' + - slack-sdk ; extra == 'slack' + - requests ; extra == 'telegram' + requires_python: '>=3.7' - kind: pypi name: traitlets version: 5.14.3 @@ -2670,9 +3178,9 @@ packages: requires_python: '>=3.8' - kind: pypi name: trove-classifiers - version: 2024.9.12 - url: https://files.pythonhosted.org/packages/b6/7a/e0edec9c8905e851d52076bbc41890603e2ba97cf64966bc1498f2244fd2/trove_classifiers-2024.9.12-py3-none-any.whl - sha256: f88a27a892891c87c5f8bbdf110710ae9e0a4725ea8e0fb45f1bcadf088a491f + version: 2024.10.21.16 + url: https://files.pythonhosted.org/packages/35/35/5055ab8d215af853d07bbff1a74edf48f91ed308f037380a5ca52dd73348/trove_classifiers-2024.10.21.16-py3-none-any.whl + sha256: 0fb11f1e995a757807a8ef1c03829fbd4998d817319abcef1f33165750f103be - kind: pypi name: types-pytz version: 2024.2.0.20240913 diff --git a/pyproject.toml b/pyproject.toml index 9cf7039..348d9e6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "openprotein_python" packages = [{ include = "openprotein" }] -version = "0.5.0" +version = "0.5.2" description = "OpenProtein Python interface." license = "MIT" readme = "README.md" @@ -15,19 +15,19 @@ dependencies = [ "requests>=2.32.3,<3", "pydantic>=2.5,<3", "tqdm>=4.66.5,<5", - "pandas>=2.1.4,<3", - "numpy>=2.1.1,<3", + "pandas>=2.2.2,<3", + "numpy>=1.9,<3", ] -requires-python = ">=3.10,<3.11" +requires-python = ">=3.10" [project.optional-dependencies] dev = [ "ipykernel>=6.29.5,<7", "pytest>=8.3.3,<9", "pandas-stubs>=2.1.4.240909,<3", - "hatchling>=1.25.0,<2", "matplotlib>=3.9.2,<4", "scipy>=1.14.1,<2", + "hatchling>=1.26.1", ] [tool.pixi.project] @@ -35,22 +35,21 @@ channels = ["conda-forge"] platforms = ["linux-64", "linux-aarch64", "osx-arm64"] [tool.pixi.dependencies] -python = ">=3.10,<3.11" +python = ">=3.10" -[tool.pixi.environments] -default = { solve-group = "default" } -dev = { features = ["dev"], solve-group = "default" } +# allow installing as editable +[tool.pixi.feature.dev.dependencies] +pip = ">=24.2,<25" -[tool.pixi.tasks] +[tool.pixi.feature.dev.tasks] postinstall = "pip install --no-build-isolation --no-deps --disable-pip-version-check -e ." -[tool.pixi.feature.dev.dependencies] -pip = ">=24.2,<25" -editables = ">=0.5,<0.6" +[tool.pixi.environments] +dev = ["dev"] [build-system] +requires = ["hatchling>=1.26.1"] build-backend = "hatchling.build" -requires = ["hatchling"] [tool.hatch.build.targets.sdist] packages = ["openprotein"] @@ -58,6 +57,20 @@ packages = ["openprotein"] [tool.hatch.build.targets.wheel] packages = ["openprotein"] +[tool.hatch.env] +requires = [ + "hatch-conda>=0.5.2", +] + +[tool.hatch.envs.conda] +type = "conda" +command = "micromamba" +dependencies = [ + "conda-build>=24.9.0", +] +requires = [ + "hatch-conda-build", +] + [tool.pyright] -# typeCheckingMode = "off" # this shit too hard LOL -typeCheckingMode = "basic" # LETS DO THIS SHIT +typeCheckingMode = "basic" diff --git a/tests/api/test_assay.py b/tests/api/test_assay.py index 7b234c5..d043fa9 100644 --- a/tests/api/test_assay.py +++ b/tests/api/test_assay.py @@ -9,7 +9,7 @@ from typing import List, Optional, Union from io import BytesIO -import pydantic +import openprotein.pydantic as pydantic from unittest.mock import ANY import json from openprotein.base import BearerAuth diff --git a/tests/api/test_design.py b/tests/api/test_design.py index 69b7568..39cf108 100644 --- a/tests/api/test_design.py +++ b/tests/api/test_design.py @@ -1,31 +1,25 @@ -import pytest -from unittest.mock import MagicMock -from typing import List, Optional, Union import io -from unittest.mock import ANY import json -from urllib.parse import urljoin from datetime import datetime +from typing import List, Optional, Union +from unittest.mock import ANY, MagicMock +from urllib.parse import urljoin - -from openprotein.base import APISession -from openprotein.api.jobs import Job +import pytest from openprotein.api.design import ( - DesignJobCreate, - DesignResults, - ModelCriterion, Criterion, -) -from openprotein.api.design import ( - create_design_job, - get_design_results, DesignAPI, DesignFuture, + ModelCriterion, + WorkflowDesignJobCreate, + WorkflowDesignResults, + create_design_job, + get_design_results, ) -from openprotein.api.jobs import load_job -from openprotein.base import BearerAuth -from tests.conf import BACKEND +from openprotein.api.jobs import Job, load_job +from openprotein.base import APISession, BearerAuth from openprotein.jobs import Job, JobType +from tests.conf import BACKEND class APISessionMock(APISession): @@ -124,7 +118,7 @@ def test_create_design_job(api_session_mock): ] assay_id = "assay123" - design_job = DesignJobCreate(assay_id=assay_id, criteria=criteria) + design_job = WorkflowDesignJobCreate(assay_id=assay_id, criteria=criteria) result = create_design_job(api_session_mock, design_job) api_session_mock.post.assert_called_once_with( @@ -207,7 +201,7 @@ def test_design_api_create_design_job(api_session_mock): ) ] ] - job_create_sample = DesignJobCreate(assay_id=assay_id, criteria=criteria) + job_create_sample = WorkflowDesignJobCreate(assay_id=assay_id, criteria=criteria) job_response = { "status": "SUCCESS", @@ -262,7 +256,7 @@ def test_design_future_get(api_session_mock): ], } - design_results = DesignResults(**results) + design_results = WorkflowDesignResults(**results) DesignFuture.get_results = MagicMock(return_value=design_results) @@ -331,4 +325,4 @@ def test_design_api_get_design_results(api_session_mock): ) # Verify that the correct results were returned - assert results == DesignResults(**job_sample) + assert results == WorkflowDesignResults(**job_sample) diff --git a/tests/api/test_poet.py b/tests/api/test_poet.py index 2ff3a09..41766c0 100644 --- a/tests/api/test_poet.py +++ b/tests/api/test_poet.py @@ -246,7 +246,7 @@ def test_poet_msa_post(api_session_mock): result = msa_post(api_session_mock, msa_file=msa_fasta) api_session_mock.post.assert_called_once_with( - "v1/poet/align/msa", files={"msa_file": msa_fasta}, params={"is_seed": False} + "v1/align/msa", files={"msa_file": msa_fasta}, params={"is_seed": False} ) assert result.msa_id == "12345" @@ -265,7 +265,7 @@ def test_poet_upload_prompt_post(api_session_mock): result = upload_prompt_post(api_session_mock, prompt_file=prompt_fasta) api_session_mock.post.assert_called_once_with( - "v1/poet/align/upload_prompt", files={"prompt_file": prompt_fasta} + "v1/align/upload_prompt", files={"prompt_file": prompt_fasta} ) assert result.job.job_id == "j123" @@ -298,7 +298,7 @@ def test_poet_prompt_post(api_session_mock): ) api_session_mock.post.assert_called_once_with( - "v1/poet/align/prompt", + "v1/align/prompt", params={ "msa_id": msa_id, "msa_method": MSASamplingMethod.NEIGHBORS_NONGAP_NORM_NO_LIMIT, @@ -324,7 +324,7 @@ def test_poet_get_align_job_inputs(api_session_mock): result = get_align_job_inputs(api_session_mock, job_id, input_type) api_session_mock.get.assert_called_once_with( - "v1/poet/align/inputs", + "v1/align/inputs", params={"job_id": job_id, "msa_type": input_type}, stream=True, ) @@ -344,7 +344,7 @@ def test_poet_get_align_job_inputs_prompt_index(api_session_mock): ) api_session_mock.get.assert_called_once_with( - "v1/poet/align/inputs", + "v1/align/inputs", params={"job_id": job_id, "msa_type": input_type, "replicate": prompt_index}, stream=True, ) diff --git a/tests/api/test_train.py b/tests/api/test_train.py index c828f86..0310180 100644 --- a/tests/api/test_train.py +++ b/tests/api/test_train.py @@ -1,21 +1,20 @@ -import pytest from datetime import datetime from unittest.mock import MagicMock +import pytest from openprotein.api.data import AssayDataset, AssayMetadata -from openprotein.jobs import Job, JobType - from openprotein.api.train import ( TrainFuture, TrainingAPI, - create_train_job, + TrainJob, _create_train_job_br, _create_train_job_gp, + create_train_job, get_training_results, ) from openprotein.base import APISession from openprotein.errors import InvalidParameterError -from openprotein.api.train import TrainGraph +from openprotein.jobs import Job, JobType from tests.conf import BACKEND # pending refactor @@ -102,7 +101,7 @@ def test_get_training_results(mock_setup): session_mock.get.return_value.json.return_value = response_data train_graph = get_training_results(session_mock, "5678") session_mock.get.assert_called_once_with("v1/workflow/train/5678") - assert isinstance(train_graph, TrainGraph) + assert isinstance(train_graph, TrainJob) assert len(train_graph.traingraph) == 2 assert train_graph.traingraph[0].step == 1 assert train_graph.traingraph[0].loss == 0.123 @@ -116,7 +115,7 @@ def test_get_training_results2(): session = MagicMock(spec=APISession) job_id = "1234" - expected_train_graph = TrainGraph( + expected_train_graph = TrainJob( traingraph=[], created_date=datetime.now(), job_id="1234" ) diff --git a/tests/conf.py b/tests/conf.py index 5a41d05..1f92a6f 100644 --- a/tests/conf.py +++ b/tests/conf.py @@ -1 +1,2 @@ -BACKEND = "https://dev.api.openprotein.ai/api/" \ No newline at end of file +BACKEND = "https://api.openprotein.ai/api/" +TIMEOUT = 900 diff --git a/tests/e2e/test_align_e2e.py b/tests/e2e/test_align_e2e.py index 4280dc5..541e3c6 100644 --- a/tests/e2e/test_align_e2e.py +++ b/tests/e2e/test_align_e2e.py @@ -1,15 +1,15 @@ import pytest from openprotein.api.align import * import json -from tests.conf import BACKEND +from tests.conf import BACKEND, TIMEOUT import time import collections import openprotein from openprotein.schemas import JobType from openprotein.jobs import * +from AWSTools.Batchtools.batch_utils import fakeseq - -TEST_SEQUENCE = "MYRMQLLSCIALSLALVTNSAPTSSSTKKTQLQLEHLLLDLQMILNGINNYKNPKLTRMLTFKFYMPKKATELKHLQCLEEELKPLEEVLNLAQSKNFHLRPRDLISNINVIVLELKGMYRMQLLSCIALSLALVTNSAPTSSSTKKTQLQLEHLLLDLQMILNGINNYKNPKLTRMLTFKFYMPKKATELKHLQCLEEELKPLEEVLNLAQSKNFHLRPRDLISNINVIVLELKGSEP" +TEST_SEQUENCE = f"{fakeseq(5)}APPMYRMQLLSCIALSLALVTNSAPTSSSTKKTQLQLEHLLLDLQMILNGINNYKNPKLTRMLTFKFYMPKKATELKHLQCLEEELKPLEEVLNLAQSKNFHLRPRDLISNINVIVLELKGMYRMQLLSCIALSLALVTNSAPTSSSTKKTQLQLEHLLLDLQMILNGINNYKNPKLTRMLTFKFYMPKKATELKHLQCLEEELKPLEEVLNLAQSKNFHLRPRDLISNINVIVLELKGSEP" print(f"USING BACKEND: {BACKEND} ") @@ -39,6 +39,7 @@ class Static: def test_msa_post(api_session): job = msa_post(api_session, seed=TEST_SEQUENCE.encode()) job = job.job + print(job) assert job.job_type == JobType.align_align assert isinstance(job, MSAJob) assert job.job_id is not None @@ -58,6 +59,7 @@ def test_prompt_post(api_session, test_msa_post): num_ensemble_prompts=3, ) job = job.job + print(job) assert isinstance(job, PromptJob) assert job.job_id is not None @@ -83,6 +85,7 @@ def test_upload_prompt_post(api_session): job = upload_prompt_post(api_session, prompt_file) job = job.job + print(job) assert isinstance(job, PromptJob) assert job.job_id is not None @@ -101,10 +104,12 @@ def test_csv_stream(): def test_get_input(api_session, test_msa_post): job = job_get(api_session, job_id=STATIC.msa_id) + print(job) + reader = get_input(api_session, job, PoetInputType.INPUT) x = list(reader) assert len(x) == 1 - assert x[0][0] == "seed" + assert x[0][0] == "seed" or x[0][0] == "101" assert x[0][1] == TEST_SEQUENCE @@ -112,9 +117,9 @@ def test_get_prompt(api_session, test_upload_prompt_post): prompt = api_session.load_job(STATIC.uploaded_prompt_id) assert isinstance(prompt, PromptFuture) - prompt.wait_until_done(timeout=200) + prompt.wait_until_done(verbose=True, timeout=TIMEOUT) - r = prompt.wait() + r = prompt.wait(verbose=True) x = list(r) assert any([i == [""] for i in x]) assert len(x) == 9 # total seqs @@ -139,13 +144,13 @@ def test_get_prompt(api_session, test_upload_prompt_post): def test_get_msa(api_session, test_msa_post): msa = api_session.load_job(STATIC.msa_id) assert isinstance(msa, MSAFuture) - msa.wait_until_done(timeout=200) + msa.wait_until_done(verbose=True, timeout=TIMEOUT) assert msa.status == "SUCCESS" - r = msa.wait() + r = msa.wait(verbose=True) x = list(r) assert len(x) > 1 - assert x[0][0] == "seed" + assert x[0][0] == "seed" or x[0][0] == "101" assert x[0][1] == TEST_SEQUENCE assert len(list(msa.get_input("GENERATED"))) > 1 @@ -154,14 +159,16 @@ def test_get_msa(api_session, test_msa_post): def test_msa_future(api_session, test_msa_post): job = job_get(api_session, job_id=STATIC.msa_id) + print(job) + future = MSAFuture(api_session, job) - assert future.wait_until_done(timeout=200) + assert future.wait_until_done(verbose=True, timeout=TIMEOUT) assert future.id == STATIC.msa_id assert future.msa_id == STATIC.msa_id assert future.prompt_id is None - reader = future.wait() + reader = future.wait(verbose=True) assert isinstance(reader, collections.Iterator) reader = future.get() @@ -174,13 +181,13 @@ def test_msa_future(api_session, test_msa_post): def test_prompt_future(api_session, test_prompt_post): job = job_get(api_session, job_id=STATIC.prompt_id) future = PromptFuture(api_session, job, msa_id=STATIC.msa_id) - assert future.wait_until_done(timeout=200) + assert future.wait_until_done(verbose=True, timeout=TIMEOUT) assert future.id == STATIC.prompt_id assert future.msa_id == STATIC.msa_id assert future.prompt_id == STATIC.prompt_id - reader = future.wait() + reader = future.wait(verbose=True) assert isinstance(reader, collections.Iterator) reader = future.get() @@ -190,6 +197,7 @@ def test_prompt_future(api_session, test_prompt_post): def test_prompt_future_get(api_session, test_upload_prompt_post): # job = api_session.poet.load_prompt_job(STATIC.prompt_id).wait_until_done(timeout=120) job = job_get(api_session, job_id=STATIC.prompt_id) + print(job) future = PromptFuture(session=api_session, job=job) reader = future.get_input(PoetInputType.MSA) diff --git a/tests/e2e/test_embeddings_e2e.py b/tests/e2e/test_embeddings_e2e.py index 21115f2..090cd5e 100644 --- a/tests/e2e/test_embeddings_e2e.py +++ b/tests/e2e/test_embeddings_e2e.py @@ -1,18 +1,19 @@ import pytest +import time + +import numpy as np from openprotein.base import APISession +from AWSTools.Batchtools.batch_utils import fakeseq +from openprotein.api.embedding import * from openprotein.api.embedding import ( - embedding_models_list_get, - embedding_model_get, embedding_get, + embedding_model_get, + embedding_models_list_get, ) -import json -from tests.conf import BACKEND -from openprotein.api.embedding import * -from openprotein.jobs import * from openprotein.api.jobs import load_job -import time -import numpy as np -from AWSTools.Batchtools.batch_utils import fakeseq +from openprotein.base import APISession +from openprotein.jobs import * +from tests.conf import BACKEND class Static: @@ -158,7 +159,7 @@ def tst_svd_get(api_session, test_svd_post): def test_svd_model_embed(api_session, test_svd_post): svd_id = STATIC.svd_id meta = svd_get(api_session, STATIC.svd_id) - svd_model = SVDModel(api_session, meta) + svd_model = SVDModel(session=api_session, metadata=meta) sequences = [b"AAAPPPLLL"] future = svd_model.embed(sequences) assert isinstance(future, EmbeddingResultFuture) diff --git a/tests/e2e/test_fold_e2e.py b/tests/e2e/test_fold_e2e.py index 70447fd..e3b18e6 100644 --- a/tests/e2e/test_fold_e2e.py +++ b/tests/e2e/test_fold_e2e.py @@ -1,12 +1,14 @@ import pytest import json -from tests.conf import BACKEND +from tests.conf import BACKEND, TIMEOUT from openprotein.api.fold import * import time import openprotein from openprotein.api.align import msa_post, MSAFuture, MSAJob from openprotein.jobs import job_get +from AWSTools.Batchtools.batch_utils import fakeseq + class Static: esmfold_id: str = None @@ -26,7 +28,7 @@ def api_session(): yield sess -SEQUENCES = [b"AAAPPPLLL"] +SEQUENCES = [b"LAAAPPPLLL"] AF_SEQUENCE = "MYRMQLLSCIALSLALVTNSAPTSSSTKKTQLQLEHLLLDLQMILNGINNYKNPKLTRMLTFKFYMPKKATELKHLQCLEEELKPLEEVLNLAQSKNFHLRPRDLISNINVIVLELKGMYRMQLLSCIALSLALVTNSAPTSSSTKKTQLQLEHLLLDLQMILNGINNYKNPKLTRMLTFKFYMPKKATELKHLQCLEEELKPLEEVLNLAQSKNFHLRPRDLISNINVIVLELKGSEP" print(f"USING BACKEND: {BACKEND} ") @@ -66,17 +68,23 @@ def test_fold_get(api_session, test_fold_post): time.sleep(4) # job.wait_until_done(api_session) - job_details = api_session.load_job(STATIC.esmfold_id) - assert job_details + f = api_session.load_job(STATIC.esmfold_id) + assert f + assert f.wait_until_done(timeout=TIMEOUT) + pdb = f.get() + print(pdb) + pdb = pdb[0][1] + assert "ATOM" in pdb.decode() + assert len(pdb.decode().split("\n")) > 10 sequences = fold_get_sequences(api_session, job_id=STATIC.esmfold_id) assert sorted(sequences) == sorted(SEQUENCES) - pdb = fold_get_sequence_result( + pdbresult = fold_get_sequence_result( api_session, job_id=STATIC.esmfold_id, sequence=SEQUENCES[0] ) - assert "ATOM" in pdb.decode() - assert len(pdb.decode().split("\n")) > 10 + assert "ATOM" in pdbresult.decode() + assert len(pdbresult.decode().split("\n")) > 10 def test_fold_model(api_session): @@ -86,11 +94,11 @@ def test_fold_model(api_session): assert model.id == "esmfold" future = model.fold(SEQUENCES) - assert future.wait(timeout=400) - result = future.wait(verbose=True, timeout=100) + assert future.wait(timeout=TIMEOUT) + result = future.wait(verbose=True, timeout=TIMEOUT) assert len(result) == 1 assert len(result[0]) == 2 - assert result[0][0] == b"AAAPPPLLL" + assert result[0][0] == SEQUENCES[0] assert "ATOM" in result[0][1].decode() @@ -107,7 +115,7 @@ def test_fold_api(api_session): result = f.wait() assert len(result) == 1 assert len(result[0]) == 2 - assert result[0][0] == b"AAAPPPLLL" + assert result[0][0] == SEQUENCES[0] assert "ATOM" in result[0][1].decode() @@ -133,8 +141,8 @@ def test_fold_api_colabfold(api_session, test_msa_post): assert "alphafold2" in [m.id for m in models] f = api_session.fold.alphafold2.fold(msa=test_msa_post, num_recycles=1) - - assert f.wait_until_done(timeout=600) + time.sleep(2) # wait for job to reg + assert f.wait_until_done(timeout=TIMEOUT) result = f.wait() assert len(result) == 1 assert len(result[0]) == 2 diff --git a/tests/e2e/test_poet_e2e.py b/tests/e2e/test_poet_e2e.py index b9aca50..9a275fa 100644 --- a/tests/e2e/test_poet_e2e.py +++ b/tests/e2e/test_poet_e2e.py @@ -1,7 +1,7 @@ import pytest from openprotein.api.align import * import json -from tests.conf import BACKEND +from tests.conf import BACKEND, TIMEOUT import time import collections import openprotein @@ -10,8 +10,9 @@ import numpy as np from openprotein.api.poet import * from openprotein.api.jobs import load_job +from AWSTools.Batchtools.batch_utils import fakeseq -TEST_SEQUENCE = "MYRMQLLSCIALSLALVTNSAPTSSSTKKTQLQLEHLLLDLQMILNGINNYKNPKLTRMLTFKFYMPKKATELKHLQCLEEELKPLEEVLNLAQSKNFHLRPRDLISNINVIVLELKGMYRMQLLSCIALSLALVTNSAPTSSSTKKTQLQLEHLLLDLQMILNGINNYKNPKLTRMLTFKFYMPKKATELKHLQCLEEELKPLEEVLNLAQSKNFHLRPRDLISNINVIVLELKGSEP" +TEST_SEQUENCE = f"{fakeseq(5)}MYRMQLLSCIALSLALVTNSAPTSSSTKKTQLQLEHLLLDLQMILNGINNYKNPKLTRMLTFKFYMPKKATELKHLQCLEEELKPLEEVLNLAQSKNFHLRPRDLISNINVIVLELKGMYRMQLLSCIALSLALVTNSAPTSSSTKKTQLQLEHLLLDLQMILNGINNYKNPKLTRMLTFKFYMPKKATELKHLQCLEEELKPLEEVLNLAQSKNFHLRPRDLISNINVIVLELKGSEP" print(f"USING BACKEND: {BACKEND} ") @@ -32,6 +33,8 @@ class Static: ssp_job_id: str = None generate_job_id: str = None prompt_single: str = None + prompt_from_msa: str = None + score_from_msa: str = None STATIC = Static() @@ -51,6 +54,33 @@ def test_msa_post(api_session): yield job.job_id +@pytest.fixture(autouse=False) +def test_prompt_from_msa(api_session, test_msa_post): + random_seq = f"{fakeseq(5)}{TEST_SEQUENCE}" + msa = msa_post(api_session, seed=random_seq.encode()) + msajob = msa.job + print(f"MSA ID {msajob.job_id}") + + prompt = prompt_post( + api_session, + msa_id=msajob.job_id, + num_sequences=10, + min_similarity=0.1, + num_ensemble_prompts=3, + ) + job = prompt.job + + assert job.job_id is not None + assert job.status in ["PENDING", "RUNNING", "SUCCESS"] + assert msa.wait_until_done(timeout=TIMEOUT) + assert prompt.wait_until_done(timeout=TIMEOUT) + print(f"prompt ID {job.job_id}") + + if STATIC.prompt_from_msa is None: + STATIC.prompt_from_msa = job.job_id + yield job.job_id + + @pytest.fixture(autouse=False) def test_upload_prompt_post(api_session): sep = "\n" @@ -89,6 +119,28 @@ def test_csv_stream(): assert list(reader) == [["col1", "col2"], ["val1", "val2"]] +@pytest.mark.skip("Optional test for msa-prompt-score") +def test_poet_score_from_msaprompt(api_session, test_prompt_from_msa): + pid = test_prompt_from_msa + + queries = [b"AAAPPPLLL", b"AAAAPPPLK"] + score = poet_score_post(api_session, prompt_id=pid, queries=queries) + job = score.job + print("SCORE job", job.job_id) + assert isinstance(job, PoetScoreJob) + assert job.job_id is not None + assert job.status in ["PENDING", "RUNNING", "SUCCESS"] + assert "poet" in job.job_type + if STATIC.score_from_msa is None: + STATIC.score_from_msa = job.job_id + assert score.wait_until_done(timeout=TIMEOUT) + assert score.done() + result = score.get() + assert len(result) == 2 # 2 seq + assert len(result[0][-1]) == 3 # 3 prompts + assert len(result[1][-1]) == 3 # 3 prompts + + @pytest.fixture(autouse=False) def test_poet_score_post(api_session, test_upload_prompt_post): queries = [b"AAAPPPLLL", b"AAAAPPPLK"] @@ -168,7 +220,7 @@ def test_poet_future(api_session, test_upload_prompt_post): def test_poet_score_get(api_session, test_poet_score_post): job = job_get(api_session, job_id=STATIC.score_job_id) assert isinstance(job, PoetScoreJob) - timeout = 600 # dev scales to 0 so slow + timeout = TIMEOUT # dev scales to 0 so slow while job.status != "SUCCESS": time.sleep(10) job = job_get(api_session, job_id=STATIC.score_job_id) # refresh results @@ -197,7 +249,7 @@ def test_poet_score_get(api_session, test_poet_score_post): def test_poet_single_site_get(api_session, test_poet_single_site_post): job = poet_single_site_get(api_session, STATIC.ssp_job_id) assert isinstance(job, PoetSSPJob) - timeout = 600 # dev scales to 0 so slow + timeout = TIMEOUT # dev scales to 0 so slow while job.status != "SUCCESS": time.sleep(10) job = poet_single_site_get( @@ -226,7 +278,7 @@ def test_poet_single_site_get(api_session, test_poet_single_site_post): def test_poet_generate_get(api_session, test_poet_generate_post): f = load_job(api_session, STATIC.generate_job_id) job = f.job - timeout = 600 # dev scales to 0 so slow + timeout = TIMEOUT # dev scales to 0 so slow while job.status != "SUCCESS": time.sleep(10) job = load_job(api_session, STATIC.generate_job_id) @@ -255,7 +307,7 @@ def test_poet_generate_get(api_session, test_poet_generate_post): def test_poet_model_interface(api_session, test_upload_single_prompt): - seqs = [b"AAAPPLLLAAKAKAAA", b"IIGGGPPGGGGIIIILLAAA", b"ILKMEAPEAPEAPEA"] + seqs = [b"LKLK", b"LKLA", b"LKLI"] poet = api_session.embedding.get_model("poet") scorejob = poet.score(sequences=seqs, prompt=STATIC.prompt_single) embedjob = poet.embed(sequences=seqs, reduction="MEAN", prompt=STATIC.prompt_single) @@ -273,11 +325,12 @@ def test_poet_model_interface(api_session, test_upload_single_prompt): assert len(r) == len(seqs) for i in r: assert len(i) == 2 - assert isinstance(i[0], str) # sequence + assert isinstance(i[0], bytes) # sequence assert isinstance(i[1], np.ndarray) assert i[1].shape == (1024,) - r = poetsvd.wait() + svd_embed = poetsvd.embed(seqs) + r = svd_embed.wait() assert len(r) == len(seqs) for i in r: assert len(i) == 2