openprotein-python 0.14.2__tar.gz → 0.15.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/PKG-INFO +2 -1
- openprotein_python-0.15.0/openprotein/auth_store.py +79 -0
- openprotein_python-0.15.0/openprotein/base.py +372 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/pyproject.toml +1 -0
- openprotein_python-0.14.2/openprotein/base.py +0 -181
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/.gitignore +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/LICENSE.txt +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/README.md +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/__init__.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/_version.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/align/__init__.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/align/align.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/align/api.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/align/future.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/align/msa.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/align/schemas.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/api/__init__.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/api/align.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/api/assaydata.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/api/deprecated/__init__.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/api/deprecated/design.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/api/deprecated/poet.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/api/deprecated/predict.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/api/deprecated/train.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/api/design.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/api/designer.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/api/embedding.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/api/error.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/api/fold.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/api/job.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/api/predict.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/api/predictor.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/api/prompt.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/api/svd.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/api/train.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/api/umap.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/app/__init__.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/app/deprecated.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/app/models/__init__.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/app/models/align/__init__.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/app/models/align/base.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/app/models/align/msa.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/app/models/align/prompt.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/app/models/assaydata.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/app/models/deprecated/__init__.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/app/models/deprecated/design.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/app/models/deprecated/poet.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/app/models/deprecated/predict.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/app/models/deprecated/train.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/app/models/design.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/app/models/designer.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/app/models/embeddings/__init__.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/app/models/embeddings/base.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/app/models/embeddings/esm.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/app/models/embeddings/future.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/app/models/embeddings/openprotein.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/app/models/embeddings/poet.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/app/models/embeddings/poet2.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/app/models/embeddings/test.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/app/models/fold/__init__.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/app/models/fold/alphafold2.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/app/models/fold/base.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/app/models/fold/boltz.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/app/models/fold/esmfold.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/app/models/fold/future.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/app/models/futures.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/app/models/predict.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/app/models/predictor/__init__.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/app/models/predictor/predict.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/app/models/predictor/predictor.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/app/models/predictor/validate.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/app/models/prompt.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/app/models/svd.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/app/models/train.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/app/models/umap.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/app/services/__init__.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/app/services/align.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/app/services/assaydata.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/app/services/deprecated/__init__.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/app/services/deprecated/design.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/app/services/deprecated/predict.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/app/services/deprecated/train.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/app/services/design.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/app/services/designer.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/app/services/embeddings.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/app/services/fold.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/app/services/job.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/app/services/predict.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/app/services/predictor.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/app/services/prompt.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/app/services/svd.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/app/services/train.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/app/services/umap.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/chains.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/clustering/__init__.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/clustering/api.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/clustering/clustering.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/clustering/models.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/clustering/schemas.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/common/__init__.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/common/features.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/common/model_metadata.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/common/reduction.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/common/residue_contants.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/config.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/csv.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/data/__init__.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/data/api.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/data/assaydataset.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/data/data.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/data/schemas.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/design/__init__.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/design/api.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/design/design.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/design/future.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/design/schemas.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/embeddings/__init__.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/embeddings/ablang.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/embeddings/api.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/embeddings/embeddings.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/embeddings/esm.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/embeddings/future.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/embeddings/models.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/embeddings/openprotein.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/embeddings/poet.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/embeddings/poet2.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/embeddings/schemas.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/errors.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/fasta.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/fold/__init__.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/fold/alphafold2.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/fold/api.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/fold/boltz.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/fold/common.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/fold/complex.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/fold/esmfold.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/fold/esmfold2.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/fold/fold.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/fold/future.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/fold/minifold.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/fold/models.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/fold/protenix.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/fold/rosettafold3.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/fold/schemas.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/jobs/__init__.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/jobs/api.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/jobs/futures.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/jobs/jobs.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/jobs/schemas.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/models/__init__.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/models/base.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/models/foundation/boltzgen.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/models/foundation/boltzgen_schema.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/models/foundation/esmif1.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/models/foundation/proteinmpnn.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/models/foundation/rfdiffusion.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/models/models.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/models/structure_generation.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/molecules/__init__.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/molecules/chains.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/molecules/complex.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/molecules/protein.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/molecules/structure.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/molecules/template.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/predictor/__init__.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/predictor/api.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/predictor/models.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/predictor/prediction.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/predictor/predictor.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/predictor/schemas.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/predictor/validate.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/prompt/__init__.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/prompt/api.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/prompt/models.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/prompt/prompt.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/prompt/schemas.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/protein.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/scaffolds.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/schemas/__init__.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/schemas/align.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/schemas/assaydata.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/schemas/deprecated/__init__.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/schemas/deprecated/design.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/schemas/deprecated/poet.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/schemas/deprecated/predict.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/schemas/deprecated/train.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/schemas/design.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/schemas/designer.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/schemas/embeddings.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/schemas/features.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/schemas/fold.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/schemas/job.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/schemas/predict.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/schemas/predictor.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/schemas/prompt.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/schemas/svd.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/schemas/train.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/schemas/umap.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/svd/__init__.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/svd/api.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/svd/models.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/svd/schemas.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/svd/svd.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/umap/__init__.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/umap/api.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/umap/models.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/umap/schemas.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/umap/umap.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/utils/__init__.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/utils/chain_id.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/utils/cif.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/utils/numpy.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/utils/sequence.py +0 -0
- {openprotein_python-0.14.2 → openprotein_python-0.15.0}/openprotein/utils/uuid.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openprotein-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.15.0
|
|
4
4
|
Summary: OpenProtein Python interface.
|
|
5
5
|
Author-email: Mark Gee <markgee@ne47.bio>, "Timothy Truong Jr." <ttruong@ne47.bio>, Tristan Bepler <tbepler@ne47.bio>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -8,6 +8,7 @@ License-File: LICENSE.txt
|
|
|
8
8
|
Classifier: Development Status :: 4 - Beta
|
|
9
9
|
Classifier: Programming Language :: Python :: 3
|
|
10
10
|
Requires-Python: <3.15,>=3.10
|
|
11
|
+
Requires-Dist: filelock<4,>=3.13
|
|
11
12
|
Requires-Dist: gemmi<0.8,>=0.6.0
|
|
12
13
|
Requires-Dist: numpy<3,>=1.9
|
|
13
14
|
Requires-Dist: pandas<3,>=2.2.2
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import tempfile
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from filelock import FileLock
|
|
7
|
+
from pydantic import BaseModel, ValidationError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TokenRecord(BaseModel):
|
|
11
|
+
"""A persisted auth record for one identity."""
|
|
12
|
+
|
|
13
|
+
access_token: str
|
|
14
|
+
refresh_token: str | None = None
|
|
15
|
+
expires_at: float = 0.0
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TokenStore:
|
|
19
|
+
"""Persist the rotating refresh token across processes.
|
|
20
|
+
|
|
21
|
+
The on-disk file is a JSON object keyed by identity
|
|
22
|
+
``"{backend}|{username}"`` -> ``TokenRecord``. The read-modify-write
|
|
23
|
+
critical section is guarded by a cross-process ``filelock`` so two
|
|
24
|
+
processes never refresh the same rotating token concurrently.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, dir: Path | None = None):
|
|
28
|
+
self.dir = Path(dir) if dir is not None else Path.home() / ".openprotein"
|
|
29
|
+
self.dir.mkdir(parents=True, exist_ok=True)
|
|
30
|
+
self.path = self.dir / "token.json"
|
|
31
|
+
# One FileLock instance per store -> reentrant within this process.
|
|
32
|
+
self._lock = FileLock(str(self.dir / "token.lock"))
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def identity(backend: str, username: str) -> str:
|
|
36
|
+
return f"{backend}|{username}"
|
|
37
|
+
|
|
38
|
+
def lock(self):
|
|
39
|
+
"""Context manager for the cross-process critical section."""
|
|
40
|
+
return self._lock
|
|
41
|
+
|
|
42
|
+
def _read_all(self) -> dict:
|
|
43
|
+
# Caller must hold self._lock: this is the read half of the
|
|
44
|
+
# read-modify-write critical section in load()/save().
|
|
45
|
+
try:
|
|
46
|
+
return json.loads(self.path.read_text())
|
|
47
|
+
except (OSError, json.JSONDecodeError):
|
|
48
|
+
# Missing or corrupt file -> behave as if there were no records.
|
|
49
|
+
return {}
|
|
50
|
+
|
|
51
|
+
def load(self, identity: str) -> TokenRecord | None:
|
|
52
|
+
with self._lock:
|
|
53
|
+
data = self._read_all()
|
|
54
|
+
entry = data.get(identity)
|
|
55
|
+
if entry is None:
|
|
56
|
+
return None
|
|
57
|
+
try:
|
|
58
|
+
return TokenRecord.model_validate(entry)
|
|
59
|
+
except ValidationError:
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
def save(self, identity: str, record: TokenRecord) -> None:
|
|
63
|
+
with self._lock:
|
|
64
|
+
data = self._read_all()
|
|
65
|
+
data[identity] = record.model_dump()
|
|
66
|
+
self._atomic_write(data)
|
|
67
|
+
|
|
68
|
+
def _atomic_write(self, data: dict) -> None:
|
|
69
|
+
fd, tmp = tempfile.mkstemp(dir=str(self.dir), prefix="token.", suffix=".tmp")
|
|
70
|
+
try:
|
|
71
|
+
with os.fdopen(fd, "w") as f:
|
|
72
|
+
json.dump(data, f)
|
|
73
|
+
os.chmod(tmp, 0o600)
|
|
74
|
+
os.replace(tmp, self.path)
|
|
75
|
+
except BaseException:
|
|
76
|
+
# Don't leave a temp file behind on failure.
|
|
77
|
+
if os.path.exists(tmp):
|
|
78
|
+
os.unlink(tmp)
|
|
79
|
+
raise
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import threading
|
|
3
|
+
import time
|
|
4
|
+
import warnings
|
|
5
|
+
from typing import Mapping, Sequence
|
|
6
|
+
from urllib.parse import urljoin
|
|
7
|
+
|
|
8
|
+
import requests
|
|
9
|
+
import requests.auth
|
|
10
|
+
from requests.adapters import HTTPAdapter
|
|
11
|
+
from requests.packages.urllib3.util.retry import Retry # type: ignore
|
|
12
|
+
|
|
13
|
+
from openprotein._version import __version__
|
|
14
|
+
from openprotein.auth_store import TokenRecord, TokenStore
|
|
15
|
+
from openprotein.errors import AuthError, HTTPError
|
|
16
|
+
|
|
17
|
+
# Refresh the access token this many seconds before it actually expires,
|
|
18
|
+
# to absorb clock skew and request latency. 30s is a conservative buffer for
|
|
19
|
+
# typical clock drift plus a round-trip against a ~15-minute access token.
|
|
20
|
+
SKEW_SECONDS = 30
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class BearerAuth(requests.auth.AuthBase):
|
|
24
|
+
"""
|
|
25
|
+
See https://stackoverflow.com/a/58055668
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, username, token):
|
|
29
|
+
self._username = username
|
|
30
|
+
self._token = token
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def token(self):
|
|
34
|
+
warnings.warn(
|
|
35
|
+
"DeprecationWarning: Accessing session.auth.token is deprecated and will throw an error in the future. Use session.auth._token if you need it."
|
|
36
|
+
)
|
|
37
|
+
return self._token
|
|
38
|
+
|
|
39
|
+
def __call__(self, r):
|
|
40
|
+
r.headers["Authorization"] = "Bearer " + self._token
|
|
41
|
+
return r
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class APISession(requests.Session):
|
|
45
|
+
"""
|
|
46
|
+
A class to handle API sessions. This class provides a connection session to the OpenProtein API.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
username: str,
|
|
52
|
+
password: str,
|
|
53
|
+
backend: str,
|
|
54
|
+
timeout: int = 180,
|
|
55
|
+
):
|
|
56
|
+
if not username or not password:
|
|
57
|
+
raise AuthError(
|
|
58
|
+
"Expected username and password. Or use environment variables `OPENPROTEIN_USERNAME` and `OPENPROTEIN_PASSWORD`. Or provide these variables (`username` and `password`) in ~/.openprotein/config.toml."
|
|
59
|
+
)
|
|
60
|
+
super().__init__()
|
|
61
|
+
self.backend = backend
|
|
62
|
+
self.verify = True
|
|
63
|
+
self.timeout = timeout
|
|
64
|
+
|
|
65
|
+
# Custom retry strategies
|
|
66
|
+
# auto retry for pesky connection reset errors and others
|
|
67
|
+
# 503 will catch if BE is refreshing
|
|
68
|
+
retry = Retry(
|
|
69
|
+
total=4,
|
|
70
|
+
backoff_factor=3, # 0,1,4,13s
|
|
71
|
+
status_forcelist=[500, 502, 503, 504, 101, 104],
|
|
72
|
+
)
|
|
73
|
+
adapter = HTTPAdapter(max_retries=retry)
|
|
74
|
+
self.mount("https://", adapter)
|
|
75
|
+
self.headers["User-Agent"] = f"openprotein-python/{__version__}"
|
|
76
|
+
|
|
77
|
+
# Auth / refresh state. Initialised before any network call so that
|
|
78
|
+
# request() can read _refreshing / _legacy_mode safely.
|
|
79
|
+
self._username = username
|
|
80
|
+
self._password = password
|
|
81
|
+
self._token_store = TokenStore()
|
|
82
|
+
self._identity = self._token_store.identity(backend, username)
|
|
83
|
+
# _refresh_lock serializes concurrent refreshes across threads.
|
|
84
|
+
# _refreshing (see the _refreshing property) is a thread-local recursion
|
|
85
|
+
# guard: the refresh POST flows back through request(), so the refreshing
|
|
86
|
+
# thread must skip the refresh logic, but OTHER threads sharing this
|
|
87
|
+
# session (e.g. via stream_parallel) must not be suppressed by it.
|
|
88
|
+
self._refresh_lock = threading.Lock()
|
|
89
|
+
self._tl = threading.local()
|
|
90
|
+
self._refresh_token: str | None = None
|
|
91
|
+
self._expires_at: float = 0.0
|
|
92
|
+
self._legacy_mode: bool = False
|
|
93
|
+
|
|
94
|
+
self._init_auth()
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def _refreshing(self) -> bool:
|
|
98
|
+
return getattr(self._tl, "refreshing", False)
|
|
99
|
+
|
|
100
|
+
@_refreshing.setter
|
|
101
|
+
def _refreshing(self, value: bool) -> None:
|
|
102
|
+
self._tl.refreshing = value
|
|
103
|
+
|
|
104
|
+
def _init_auth(self):
|
|
105
|
+
"""Adopt a valid stored record if present, otherwise log in fresh."""
|
|
106
|
+
record = self._token_store.load(self._identity)
|
|
107
|
+
if record is not None and record.refresh_token:
|
|
108
|
+
self._adopt(record)
|
|
109
|
+
else:
|
|
110
|
+
self.login(self._username, self._password)
|
|
111
|
+
|
|
112
|
+
def post(self, url, data=None, json=None, **kwargs):
|
|
113
|
+
r"""Sends a POST request. Returns :class:`Response` object.
|
|
114
|
+
|
|
115
|
+
:param url: URL for the new :class:`Request` object.
|
|
116
|
+
:param data: (optional) Dictionary, list of tuples, bytes, or file-like
|
|
117
|
+
object to send in the body of the :class:`Request`.
|
|
118
|
+
:param json: (optional) json to send in the body of the :class:`Request`.
|
|
119
|
+
:param \*\*kwargs: Optional arguments that ``request`` takes.
|
|
120
|
+
:rtype: requests.Response
|
|
121
|
+
"""
|
|
122
|
+
timeout = self.timeout
|
|
123
|
+
if "timeout" in kwargs:
|
|
124
|
+
timeout = kwargs.pop("timeout")
|
|
125
|
+
|
|
126
|
+
return self.request(
|
|
127
|
+
"POST", url, data=data, json=json, timeout=timeout, **kwargs
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
def login(self, username: str, password: str):
|
|
131
|
+
"""
|
|
132
|
+
Authenticate connection to OpenProtein with your credentials.
|
|
133
|
+
|
|
134
|
+
Parameters
|
|
135
|
+
-----------
|
|
136
|
+
username: str
|
|
137
|
+
username
|
|
138
|
+
password: str
|
|
139
|
+
password
|
|
140
|
+
"""
|
|
141
|
+
# unset the auth first
|
|
142
|
+
self.auth = None
|
|
143
|
+
self._get_auth_token(username, password)
|
|
144
|
+
|
|
145
|
+
def _auth_post(self, url: str, **kwargs):
|
|
146
|
+
"""POST for the auth endpoints, with the refresh logic suppressed.
|
|
147
|
+
|
|
148
|
+
Sets ``_refreshing`` so the proactive/reactive refresh in request()
|
|
149
|
+
does not recurse while we are logging in or refreshing.
|
|
150
|
+
"""
|
|
151
|
+
self._refreshing = True
|
|
152
|
+
try:
|
|
153
|
+
return self.post(url, **kwargs)
|
|
154
|
+
finally:
|
|
155
|
+
self._refreshing = False
|
|
156
|
+
|
|
157
|
+
def _get_auth_token(self, username: str, password: str):
|
|
158
|
+
url = urljoin(self.backend, "v1/auth/login")
|
|
159
|
+
try:
|
|
160
|
+
response = self._auth_post(
|
|
161
|
+
url,
|
|
162
|
+
data={"username": username, "password": password},
|
|
163
|
+
headers={"X-Token-Delivery": "body"},
|
|
164
|
+
timeout=3,
|
|
165
|
+
)
|
|
166
|
+
except HTTPError as e:
|
|
167
|
+
if e.status_code == 404:
|
|
168
|
+
# Backend not yet upgraded -> use the legacy endpoint.
|
|
169
|
+
return self._legacy_login(username, password)
|
|
170
|
+
raise AuthError(
|
|
171
|
+
"Authentication failed. Please check your credentials and connection."
|
|
172
|
+
) from e
|
|
173
|
+
self._apply_token_response(response.json(), username)
|
|
174
|
+
|
|
175
|
+
def _legacy_login(self, username: str, password: str):
|
|
176
|
+
url = urljoin(self.backend, "v1/login/access-token")
|
|
177
|
+
try:
|
|
178
|
+
response = self._auth_post(
|
|
179
|
+
url,
|
|
180
|
+
data={"username": username, "password": password},
|
|
181
|
+
timeout=3,
|
|
182
|
+
)
|
|
183
|
+
except HTTPError as e:
|
|
184
|
+
raise AuthError(
|
|
185
|
+
"Authentication failed. Please check your credentials and connection."
|
|
186
|
+
) from e
|
|
187
|
+
token = response.json().get("access_token")
|
|
188
|
+
if token is None:
|
|
189
|
+
raise AuthError("Unable to authenticate with given credentials.")
|
|
190
|
+
self._legacy_mode = True
|
|
191
|
+
self._refresh_token = None
|
|
192
|
+
self._expires_at = 0.0
|
|
193
|
+
self._set_token(username, token)
|
|
194
|
+
|
|
195
|
+
def _apply_token_response(self, result: dict, username: str):
|
|
196
|
+
"""Parse a login/refresh response, update memory + store, set the bearer."""
|
|
197
|
+
access_token = result.get("access_token")
|
|
198
|
+
if access_token is None:
|
|
199
|
+
raise AuthError("Unable to authenticate with given credentials.")
|
|
200
|
+
refresh_token = result.get("refresh_token")
|
|
201
|
+
if refresh_token is None:
|
|
202
|
+
# Cookie-mode / pre-rotation backend -> no refresh available.
|
|
203
|
+
self._legacy_mode = True
|
|
204
|
+
self._refresh_token = None
|
|
205
|
+
self._expires_at = 0.0
|
|
206
|
+
else:
|
|
207
|
+
self._legacy_mode = False
|
|
208
|
+
self._refresh_token = refresh_token
|
|
209
|
+
# A missing/zero expires_in is treated as "expired now", which makes
|
|
210
|
+
# the proactive check refresh on the next request rather than trust a
|
|
211
|
+
# token of unknown lifetime.
|
|
212
|
+
self._expires_at = time.time() + (result.get("expires_in") or 0)
|
|
213
|
+
self._token_store.save(
|
|
214
|
+
self._identity,
|
|
215
|
+
TokenRecord(
|
|
216
|
+
access_token=access_token,
|
|
217
|
+
refresh_token=refresh_token,
|
|
218
|
+
expires_at=self._expires_at,
|
|
219
|
+
),
|
|
220
|
+
)
|
|
221
|
+
self._set_token(username, access_token)
|
|
222
|
+
|
|
223
|
+
def _set_token(self, username: str, access_token: str):
|
|
224
|
+
if isinstance(self.auth, BearerAuth):
|
|
225
|
+
self.auth._token = access_token
|
|
226
|
+
else:
|
|
227
|
+
self.auth = BearerAuth(username=username, token=access_token)
|
|
228
|
+
|
|
229
|
+
def _adopt(self, record: TokenRecord):
|
|
230
|
+
"""Adopt an existing token record (from disk or a fresher in-store copy).
|
|
231
|
+
|
|
232
|
+
Callers must only pass a record whose ``refresh_token`` is set — a
|
|
233
|
+
record without one is not adoptable (it would leave the session in
|
|
234
|
+
non-legacy mode with nothing to refresh).
|
|
235
|
+
"""
|
|
236
|
+
self._legacy_mode = False
|
|
237
|
+
self._refresh_token = record.refresh_token
|
|
238
|
+
self._expires_at = record.expires_at
|
|
239
|
+
self._set_token(self._username, record.access_token)
|
|
240
|
+
|
|
241
|
+
def _refresh(self):
|
|
242
|
+
"""Refresh the access token, serialized across threads and processes.
|
|
243
|
+
|
|
244
|
+
Re-reads the store under the lock first: if another thread/process
|
|
245
|
+
already rotated the token, adopt that instead of refreshing again
|
|
246
|
+
(replaying a superseded refresh token would revoke the session).
|
|
247
|
+
"""
|
|
248
|
+
with self._refresh_lock, self._token_store.lock():
|
|
249
|
+
record = self._token_store.load(self._identity)
|
|
250
|
+
if (
|
|
251
|
+
record is not None
|
|
252
|
+
and record.refresh_token
|
|
253
|
+
and record.expires_at > self._expires_at
|
|
254
|
+
):
|
|
255
|
+
self._adopt(record)
|
|
256
|
+
return
|
|
257
|
+
|
|
258
|
+
if self._refresh_token is None:
|
|
259
|
+
self._relogin()
|
|
260
|
+
return
|
|
261
|
+
|
|
262
|
+
url = urljoin(self.backend, "v1/auth/refresh")
|
|
263
|
+
try:
|
|
264
|
+
response = self._auth_post(
|
|
265
|
+
url,
|
|
266
|
+
json={"refresh_token": self._refresh_token},
|
|
267
|
+
headers={"X-Token-Delivery": "body"},
|
|
268
|
+
timeout=10,
|
|
269
|
+
)
|
|
270
|
+
except HTTPError as e:
|
|
271
|
+
# Recover by re-logging-in when the refresh token is rejected
|
|
272
|
+
# (401/403) or when the endpoint is absent (404 from a backend
|
|
273
|
+
# that issued a refresh token but doesn't expose /auth/refresh,
|
|
274
|
+
# e.g. mid-rollback) — re-login itself drops to legacy mode if
|
|
275
|
+
# the backend has no refresh support. Other statuses are real
|
|
276
|
+
# errors and propagate.
|
|
277
|
+
if e.status_code in (401, 403, 404):
|
|
278
|
+
self._relogin()
|
|
279
|
+
return
|
|
280
|
+
raise
|
|
281
|
+
self._apply_token_response(response.json(), self._username)
|
|
282
|
+
|
|
283
|
+
def _relogin(self):
|
|
284
|
+
"""Re-establish a session from the in-memory credentials."""
|
|
285
|
+
try:
|
|
286
|
+
self.login(self._username, self._password)
|
|
287
|
+
except AuthError:
|
|
288
|
+
raise
|
|
289
|
+
except Exception as e:
|
|
290
|
+
raise AuthError("Session expired and re-login failed.") from e
|
|
291
|
+
|
|
292
|
+
def _should_refresh_proactively(self) -> bool:
|
|
293
|
+
return (
|
|
294
|
+
not self._refreshing
|
|
295
|
+
and not self._legacy_mode
|
|
296
|
+
and self._refresh_token is not None
|
|
297
|
+
and time.time() >= self._expires_at - SKEW_SECONDS
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
def request(self, method: str, url: str, *args, **kwargs):
|
|
301
|
+
if self._should_refresh_proactively():
|
|
302
|
+
self._refresh()
|
|
303
|
+
|
|
304
|
+
full_url = urljoin(self.backend, url)
|
|
305
|
+
response = super().request(method, full_url, *args, **kwargs)
|
|
306
|
+
|
|
307
|
+
# Reactive refresh: a 401 means the access token expired between the
|
|
308
|
+
# proactive check and the call (or skew). Refresh once and retry once.
|
|
309
|
+
if (
|
|
310
|
+
response.status_code == 401
|
|
311
|
+
and not self._refreshing
|
|
312
|
+
and not self._legacy_mode
|
|
313
|
+
):
|
|
314
|
+
self._refresh()
|
|
315
|
+
response = super().request(method, full_url, *args, **kwargs)
|
|
316
|
+
|
|
317
|
+
if (js := kwargs.get("json")) and js is not None:
|
|
318
|
+
if _total_size(js) > 1e6:
|
|
319
|
+
warnings.warn(
|
|
320
|
+
"The requested payload is >1MB. There might be some delays or issues in processing. If the request fails, please try again with smaller sizes."
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
# intercept CloudFront errors
|
|
324
|
+
if "cloudfront" in response.headers.get("Server", "").lower():
|
|
325
|
+
if response.status_code in (502, 503):
|
|
326
|
+
raise CloudFrontError(
|
|
327
|
+
f"We're experiencing a temporary backend issue via CloudFront. Please try again later. Error {response.status_code}."
|
|
328
|
+
)
|
|
329
|
+
elif response.status_code == 504:
|
|
330
|
+
raise TimeoutError(
|
|
331
|
+
"Your request took too long to process likely due to it's size. Please try breaking it up into smaller requests if possible."
|
|
332
|
+
)
|
|
333
|
+
elif not response.ok:
|
|
334
|
+
# raise custom exception that prints better error message than requests.HTTPError
|
|
335
|
+
raise HTTPError(response)
|
|
336
|
+
return response
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _total_size(o: Sequence | Mapping | object, seen=None):
|
|
340
|
+
"""Recursively finds size of objects including contents."""
|
|
341
|
+
if seen is None:
|
|
342
|
+
seen = set()
|
|
343
|
+
obj_id = id(o)
|
|
344
|
+
if obj_id in seen:
|
|
345
|
+
return 0
|
|
346
|
+
seen.add(obj_id)
|
|
347
|
+
size = sys.getsizeof(o)
|
|
348
|
+
if isinstance(o, dict):
|
|
349
|
+
size += sum((_total_size(k, seen) + _total_size(v, seen)) for k, v in o.items())
|
|
350
|
+
elif isinstance(o, (list, tuple, set, frozenset)):
|
|
351
|
+
size += sum(_total_size(i, seen) for i in o)
|
|
352
|
+
return size
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
class TimeoutError(requests.exceptions.HTTPError):
|
|
356
|
+
"""
|
|
357
|
+
An Exception raised due to timeout, possibly from overly large
|
|
358
|
+
requests.
|
|
359
|
+
"""
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
class CloudFrontError(requests.exceptions.HTTPError):
|
|
363
|
+
"""
|
|
364
|
+
An Exception raised due to CloudFront.
|
|
365
|
+
|
|
366
|
+
This is usually due to the strict timeout from CloudFront.
|
|
367
|
+
AWS CloudFront limits responses to return within 2 minutes.
|
|
368
|
+
This can be a bit prohibitive for our system that tends to
|
|
369
|
+
deal with large data. It is usually safe to just ignore/retry upon
|
|
370
|
+
hitting this error. Our system will scale up and still handle
|
|
371
|
+
the job.
|
|
372
|
+
"""
|
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
import sys
|
|
2
|
-
import warnings
|
|
3
|
-
from typing import Mapping, Sequence
|
|
4
|
-
from urllib.parse import urljoin
|
|
5
|
-
|
|
6
|
-
import requests
|
|
7
|
-
import requests.auth
|
|
8
|
-
from requests.adapters import HTTPAdapter
|
|
9
|
-
from requests.packages.urllib3.util.retry import Retry # type: ignore
|
|
10
|
-
|
|
11
|
-
from openprotein._version import __version__
|
|
12
|
-
from openprotein.errors import AuthError, HTTPError
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class BearerAuth(requests.auth.AuthBase):
|
|
16
|
-
"""
|
|
17
|
-
See https://stackoverflow.com/a/58055668
|
|
18
|
-
"""
|
|
19
|
-
|
|
20
|
-
def __init__(self, username, token):
|
|
21
|
-
self._username = username
|
|
22
|
-
self._token = token
|
|
23
|
-
|
|
24
|
-
@property
|
|
25
|
-
def token(self):
|
|
26
|
-
warnings.warn(
|
|
27
|
-
"DeprecationWarning: Accessing session.auth.token is deprecated and will throw an error in the future. Use session.auth._token if you need it."
|
|
28
|
-
)
|
|
29
|
-
return self._token
|
|
30
|
-
|
|
31
|
-
def __call__(self, r):
|
|
32
|
-
r.headers["Authorization"] = "Bearer " + self._token
|
|
33
|
-
return r
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
class APISession(requests.Session):
|
|
37
|
-
"""
|
|
38
|
-
A class to handle API sessions. This class provides a connection session to the OpenProtein API.
|
|
39
|
-
"""
|
|
40
|
-
|
|
41
|
-
def __init__(
|
|
42
|
-
self,
|
|
43
|
-
username: str,
|
|
44
|
-
password: str,
|
|
45
|
-
backend: str,
|
|
46
|
-
timeout: int = 180,
|
|
47
|
-
):
|
|
48
|
-
if not username or not password:
|
|
49
|
-
raise AuthError(
|
|
50
|
-
"Expected username and password. Or use environment variables `OPENPROTEIN_USERNAME` and `OPENPROTEIN_PASSWORD`. Or provide these variables (`username` and `password`) in ~/.openprotein/config.toml."
|
|
51
|
-
)
|
|
52
|
-
super().__init__()
|
|
53
|
-
self.backend = backend
|
|
54
|
-
self.verify = True
|
|
55
|
-
self.timeout = timeout
|
|
56
|
-
|
|
57
|
-
# Custom retry strategies
|
|
58
|
-
# auto retry for pesky connection reset errors and others
|
|
59
|
-
# 503 will catch if BE is refreshing
|
|
60
|
-
retry = Retry(
|
|
61
|
-
total=4,
|
|
62
|
-
backoff_factor=3, # 0,1,4,13s
|
|
63
|
-
status_forcelist=[500, 502, 503, 504, 101, 104],
|
|
64
|
-
)
|
|
65
|
-
adapter = HTTPAdapter(max_retries=retry)
|
|
66
|
-
self.mount("https://", adapter)
|
|
67
|
-
self.headers["User-Agent"] = f"openprotein-python/{__version__}"
|
|
68
|
-
self.login(username, password)
|
|
69
|
-
|
|
70
|
-
def post(self, url, data=None, json=None, **kwargs):
|
|
71
|
-
r"""Sends a POST request. Returns :class:`Response` object.
|
|
72
|
-
|
|
73
|
-
:param url: URL for the new :class:`Request` object.
|
|
74
|
-
:param data: (optional) Dictionary, list of tuples, bytes, or file-like
|
|
75
|
-
object to send in the body of the :class:`Request`.
|
|
76
|
-
:param json: (optional) json to send in the body of the :class:`Request`.
|
|
77
|
-
:param \*\*kwargs: Optional arguments that ``request`` takes.
|
|
78
|
-
:rtype: requests.Response
|
|
79
|
-
"""
|
|
80
|
-
timeout = self.timeout
|
|
81
|
-
if "timeout" in kwargs:
|
|
82
|
-
timeout = kwargs.pop("timeout")
|
|
83
|
-
|
|
84
|
-
return self.request(
|
|
85
|
-
"POST", url, data=data, json=json, timeout=timeout, **kwargs
|
|
86
|
-
)
|
|
87
|
-
|
|
88
|
-
def login(self, username: str, password: str):
|
|
89
|
-
"""
|
|
90
|
-
Authenticate connection to OpenProtein with your credentials.
|
|
91
|
-
|
|
92
|
-
Parameters
|
|
93
|
-
-----------
|
|
94
|
-
username: str
|
|
95
|
-
username
|
|
96
|
-
password: str
|
|
97
|
-
password
|
|
98
|
-
"""
|
|
99
|
-
# unset the auth first
|
|
100
|
-
self.auth = None
|
|
101
|
-
self.auth = self._get_auth_token(username, password)
|
|
102
|
-
|
|
103
|
-
def _get_auth_token(self, username: str, password: str):
|
|
104
|
-
endpoint = "v1/login/access-token"
|
|
105
|
-
url = urljoin(self.backend, endpoint)
|
|
106
|
-
try:
|
|
107
|
-
response = self.post(
|
|
108
|
-
url, data={"username": username, "password": password}, timeout=3
|
|
109
|
-
)
|
|
110
|
-
except HTTPError as e:
|
|
111
|
-
# if an error occured during auth, we raise an AuthError with reference to the HTTPError
|
|
112
|
-
raise AuthError(
|
|
113
|
-
"Authentication failed. Please check your credentials and connection."
|
|
114
|
-
) from e
|
|
115
|
-
|
|
116
|
-
result = response.json()
|
|
117
|
-
token = result.get("access_token")
|
|
118
|
-
if token is None:
|
|
119
|
-
raise AuthError("Unable to authenticate with given credentials.")
|
|
120
|
-
return BearerAuth(username=username, token=token)
|
|
121
|
-
|
|
122
|
-
def request(self, method: str, url: str, *args, **kwargs):
|
|
123
|
-
full_url = urljoin(self.backend, url)
|
|
124
|
-
response = super().request(method, full_url, *args, **kwargs)
|
|
125
|
-
|
|
126
|
-
if (js := kwargs.get("json")) and js is not None:
|
|
127
|
-
if _total_size(js) > 1e6:
|
|
128
|
-
warnings.warn(
|
|
129
|
-
"The requested payload is >1MB. There might be some delays or issues in processing. If the request fails, please try again with smaller sizes."
|
|
130
|
-
)
|
|
131
|
-
|
|
132
|
-
# intercept CloudFront errors
|
|
133
|
-
if "cloudfront" in response.headers.get("Server", "").lower():
|
|
134
|
-
if response.status_code in (502, 503):
|
|
135
|
-
raise CloudFrontError(
|
|
136
|
-
f"We're experiencing a temporary backend issue via CloudFront. Please try again later. Error {response.status_code}."
|
|
137
|
-
)
|
|
138
|
-
elif response.status_code == 504:
|
|
139
|
-
raise TimeoutError(
|
|
140
|
-
"Your request took too long to process likely due to it's size. Please try breaking it up into smaller requests if possible."
|
|
141
|
-
)
|
|
142
|
-
elif not response.ok:
|
|
143
|
-
# raise custom exception that prints better error message than requests.HTTPError
|
|
144
|
-
raise HTTPError(response)
|
|
145
|
-
return response
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
def _total_size(o: Sequence | Mapping | object, seen=None):
|
|
149
|
-
"""Recursively finds size of objects including contents."""
|
|
150
|
-
if seen is None:
|
|
151
|
-
seen = set()
|
|
152
|
-
obj_id = id(o)
|
|
153
|
-
if obj_id in seen:
|
|
154
|
-
return 0
|
|
155
|
-
seen.add(obj_id)
|
|
156
|
-
size = sys.getsizeof(o)
|
|
157
|
-
if isinstance(o, dict):
|
|
158
|
-
size += sum((_total_size(k, seen) + _total_size(v, seen)) for k, v in o.items())
|
|
159
|
-
elif isinstance(o, (list, tuple, set, frozenset)):
|
|
160
|
-
size += sum(_total_size(i, seen) for i in o)
|
|
161
|
-
return size
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
class TimeoutError(requests.exceptions.HTTPError):
|
|
165
|
-
"""
|
|
166
|
-
An Exception raised due to timeout, possibly from overly large
|
|
167
|
-
requests.
|
|
168
|
-
"""
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
class CloudFrontError(requests.exceptions.HTTPError):
|
|
172
|
-
"""
|
|
173
|
-
An Exception raised due to CloudFront.
|
|
174
|
-
|
|
175
|
-
This is usually due to the strict timeout from CloudFront.
|
|
176
|
-
AWS CloudFront limits responses to return within 2 minutes.
|
|
177
|
-
This can be a bit prohibitive for our system that tends to
|
|
178
|
-
deal with large data. It is usually safe to just ignore/retry upon
|
|
179
|
-
hitting this error. Our system will scale up and still handle
|
|
180
|
-
the job.
|
|
181
|
-
"""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|