jfaustin AchilleSoulieID commited on
Commit
b8a625b
·
verified ·
1 Parent(s): e26c72c

tab_1_model_comparison (#2)

Browse files

- add multiple model predictions (a444e2745bd3207fdd514b2546b8249a9d67b96b)


Co-authored-by: Achille Soulie <[email protected]>

.gitignore CHANGED
@@ -1,3 +1,4 @@
1
  .envrc
2
-
3
- boltz_results/
 
 
1
  .envrc
2
+ __pycache__/
3
+ output/
4
+ sequences/
Dockerfile CHANGED
@@ -8,7 +8,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
8
  wget build-essential git && \
9
  rm -rf /var/lib/apt/lists/*
10
 
11
- COPY --from=uv-fetcher /uv /uvx /usr/local/bin/
12
 
13
  ENV VIRTUAL_ENV=/modules/.venv
14
  RUN uv venv "$VIRTUAL_ENV" && . "$VIRTUAL_ENV/bin/activate"
@@ -17,34 +17,33 @@ ENV PATH="$VIRTUAL_ENV/bin:$PATH"
17
  # Set the working directory to the user's home directory
18
  WORKDIR /app
19
 
20
- COPY pyproject.toml uv.lock /app/
 
 
 
 
 
 
 
 
21
 
22
- RUN uv sync --frozen --active --directory /app --inexact
23
 
24
- COPY folding-studio /app/folding-studio
25
- RUN cd /app/folding-studio && uv pip install -e .
26
 
27
- COPY app.py /app/app.py
28
- COPY molecule.py /app/molecule.py
29
  # COPY pred.cif /app/boltz_results/pred_model_0.cif
30
  # COPY plddt_0.npz /app/boltz_results/plddt_0.npz
31
 
32
  EXPOSE 7860
33
  ENV GRADIO_SERVER_NAME="0.0.0.0"
34
- ARG FOLDING_PROJECT_CODE
35
  ENV FOLDING_PROJECT_CODE=$FOLDING_PROJECT_CODE
36
 
37
  # Create directory for HTML output
38
- RUN mkdir -p /app/output/html
39
 
40
  # Create volume for HTML output
41
  VOLUME ["/app/output/html"]
42
 
43
- # Create a non-root user and set permissions
44
- RUN useradd -m -u 1000 appuser && \
45
- chown -R appuser:appuser /app
46
-
47
- # Switch to non-root user
48
- USER appuser
49
-
50
- CMD ["python3", "app.py"]
 
8
  wget build-essential git && \
9
  rm -rf /var/lib/apt/lists/*
10
 
11
+ COPY --from=uv-fetcher /uv /uvx /bin/
12
 
13
  ENV VIRTUAL_ENV=/modules/.venv
14
  RUN uv venv "$VIRTUAL_ENV" && . "$VIRTUAL_ENV/bin/activate"
 
17
  # Set the working directory to the user's home directory
18
  WORKDIR /app
19
 
20
+ # Create a non-root user and set permissions
21
+ RUN useradd -m -u 1000 appuser && \
22
+ chown -R appuser:appuser /app
23
+
24
+ # Switch to non-root user
25
+ USER appuser
26
+
27
+ COPY pyproject.toml uv.lock README.md ./
28
+ COPY folding-studio ./folding-studio
29
 
30
+ RUN uv sync --locked --no-install-project --no-dev
31
 
32
+ COPY folding_studio_demo ./folding_studio_demo
33
+ RUN uv sync --locked --no-dev
34
 
 
 
35
  # COPY pred.cif /app/boltz_results/pred_model_0.cif
36
  # COPY plddt_0.npz /app/boltz_results/plddt_0.npz
37
 
38
  EXPOSE 7860
39
  ENV GRADIO_SERVER_NAME="0.0.0.0"
40
+ ARG FOLDING_PROJECT_CODE="folding_dev"
41
  ENV FOLDING_PROJECT_CODE=$FOLDING_PROJECT_CODE
42
 
43
  # Create directory for HTML output
44
+ RUN mkdir -p ./output/html
45
 
46
  # Create volume for HTML output
47
  VOLUME ["/app/output/html"]
48
 
49
+ CMD ["uv", "run", "folding-studio-demo"]
 
 
 
 
 
 
 
Makefile CHANGED
@@ -1,5 +1,5 @@
1
  # Variables
2
- DOCKER_IMAGE_NAME = folding-studio
3
  DOCKER_IMAGE_TAG = latest
4
  PORT = 7860
5
  OUTPUT_DIR = ./output
 
1
  # Variables
2
+ DOCKER_IMAGE_NAME = folding-studio-demo
3
  DOCKER_IMAGE_TAG = latest
4
  PORT = 7860
5
  OUTPUT_DIR = ./output
app.py DELETED
@@ -1,213 +0,0 @@
1
- from Bio.PDB import MMCIFParser, PDBIO
2
- from folding_studio.client import Client
3
- from folding_studio.query.boltz import BoltzQuery, BoltzParameters
4
- from pathlib import Path
5
- import gradio as gr
6
- import hashlib
7
- import logging
8
- import numpy as np
9
- import os
10
- import plotly.graph_objects as go
11
-
12
- from molecule import molecule
13
-
14
- # Configure logging
15
- logging.basicConfig(
16
- level=logging.INFO,
17
- format='%(asctime)s - %(levelname)s - %(message)s',
18
- handlers=[
19
- logging.StreamHandler(),
20
- ]
21
- )
22
- logger = logging.getLogger(__name__)
23
-
24
-
25
- def convert_cif_to_pdb(cif_path: str, pdb_path: str) -> None:
26
- """Convert a .cif file to .pdb format using Biopython.
27
-
28
- Args:
29
- cif_path (str): Path to input .cif file
30
- pdb_path (str): Path to output .pdb file
31
- """
32
- # Parse the CIF file
33
- parser = MMCIFParser()
34
- structure = parser.get_structure("structure", cif_path)
35
-
36
- # Save as PDB
37
- io = PDBIO()
38
- io.set_structure(structure)
39
- io.save(pdb_path)
40
-
41
- def call_boltz(seq_file: Path | str, api_key: str, output_dir: Path) -> None:
42
- """Call Boltz prediction."""
43
- # Initialize parameters with CLI-provided values
44
- parameters = {
45
- "recycling_steps": 3,
46
- "sampling_steps": 200,
47
- "diffusion_samples": 1,
48
- "step_scale": 1.638,
49
- "msa_pairing_strategy": "greedy",
50
- "write_full_pae": False,
51
- "write_full_pde": False,
52
- "use_msa_server": True,
53
- "seed": 0,
54
- "custom_msa_paths": None,
55
- }
56
-
57
- # Create a client using API key
58
- logger.info("Authenticating client with API key")
59
- client = Client.from_api_key(api_key=api_key)
60
-
61
- # Define query
62
- seq_file = Path(seq_file)
63
- query = BoltzQuery.from_file(seq_file, query_name="gradio", parameters=BoltzParameters(**parameters))
64
- query.save_parameters(output_dir)
65
-
66
- logger.info("Payload: %s", query.payload)
67
-
68
- # Send a request
69
- logger.info("Sending request to Folding Studio API")
70
- response = client.send_request(query, project_code=os.environ["FOLDING_PROJECT_CODE"])
71
-
72
- # Access confidence data
73
- logger.info("Confidence data: %s", response.confidence_data)
74
-
75
- response.download_results(output_dir=output_dir, force=True, unzip=True)
76
- logger.info("Results downloaded to %s", output_dir)
77
-
78
-
79
- def predict(sequence: str, api_key: str) -> str:
80
- """Predict protein structure from amino acid sequence using Boltz model.
81
-
82
- Args:
83
- sequence (str): Amino acid sequence to predict structure for
84
- api_key (str): Folding API key
85
-
86
- Returns:
87
- str: HTML iframe containing 3D molecular visualization
88
- """
89
-
90
- # Set up unique output directory based on sequence hash
91
- seq_id = hashlib.sha1(sequence.encode()).hexdigest()
92
- seq_file = Path(f"sequence_{seq_id}.fasta")
93
- _write_fasta_file(seq_file, sequence)
94
- output_dir = Path(f"sequence_{seq_id}")
95
- output_dir.mkdir(parents=True, exist_ok=True)
96
-
97
- # Check if prediction already exists
98
- pred_cif = list(output_dir.rglob("*_model_0.cif"))
99
- if not pred_cif:
100
- # Run Boltz prediction
101
- logger.info(f"Predicting {seq_file.stem}")
102
- call_boltz(seq_file=seq_file, api_key=api_key, output_dir=output_dir)
103
- logger.info("Prediction done. Output directory: %s", output_dir)
104
- else:
105
- logger.info("Prediction already exists. Output directory: %s", output_dir)
106
-
107
- # output_dir = Path("boltz_results") # debug
108
- # Convert output CIF to PDB
109
- pred_cif = list(output_dir.rglob("*_model_0.cif"))[0]
110
- logger.info("Output file: %s", pred_cif)
111
-
112
- converted_pdb_path = str(output_dir / "pred.pdb")
113
- convert_cif_to_pdb(str(pred_cif), str(converted_pdb_path))
114
- logger.info("Converted PDB file: %s", converted_pdb_path)
115
-
116
-
117
- # Generate molecular visualization
118
- mol = _create_molecule_visualization(
119
- converted_pdb_path,
120
- sequence,
121
- )
122
-
123
- plddt_file = list(pred_cif.parent.glob("plddt_*.npz"))[0]
124
- logger.info("plddt file: %s", plddt_file)
125
- plddt_vals = np.load(plddt_file)["plddt"]
126
-
127
- return _wrap_in_iframe(mol), add_plddt_plot(plddt_vals=plddt_vals)
128
-
129
-
130
- def _write_fasta_file(filepath: Path, sequence: str) -> None:
131
- """Write sequence to FASTA file."""
132
- with open(filepath, "w") as f:
133
- f.write(f">A|protein\n{sequence}")
134
-
135
-
136
- def _create_molecule_visualization(pdb_path: Path, sequence: str) -> str:
137
- """Create molecular visualization using molecule module."""
138
- return molecule(
139
- str(pdb_path),
140
- lenSeqs=1,
141
- num_res=len(sequence),
142
- selectedResidues=list(range(1, len(sequence) + 1)),
143
- allSeqs=[sequence],
144
- sequences=[{
145
- "Score": 0,
146
- "RMSD": 0,
147
- "Recovery": 0,
148
- "Mean pLDDT": 0,
149
- "seq": sequence
150
- }],
151
- )
152
-
153
-
154
- def _wrap_in_iframe(content: str) -> str:
155
- """Wrap content in an HTML iframe with appropriate styling and permissions."""
156
- return f"""<iframe
157
- name="result"
158
- style="width: 100%; height: 100vh;"
159
- allow="midi; geolocation; microphone; camera; display-capture; encrypted-media;"
160
- sandbox="allow-modals allow-forms allow-scripts allow-same-origin allow-popups allow-top-navigation-by-user-activation allow-downloads"
161
- allowfullscreen=""
162
- allowpaymentrequest=""
163
- frameborder="0"
164
- srcdoc='{content}'
165
- ></iframe>"""
166
-
167
- def add_plddt_plot(plddt_vals: list[float]) -> str:
168
- """Create a plot of metrics."""
169
- visible = True
170
- plddt_trace = go.Scatter(
171
- x=np.arange(len(plddt_vals)),
172
- y=plddt_vals,
173
- hovertemplate="<i>pLDDT</i>: %{y:.2f} <br><i>Residue index:</i> %{x}<br>",
174
- name="seq",
175
- visible=visible,
176
- )
177
-
178
- plddt_fig = go.Figure(data=[plddt_trace])
179
- plddt_fig.update_layout(
180
- title="pLDDT",
181
- xaxis_title="Residue index",
182
- yaxis_title="pLDDT",
183
- height=500,
184
- template="simple_white",
185
- legend=dict(yanchor="bottom", y=0.01, xanchor="left", x=0.99),
186
- )
187
- return plddt_fig
188
-
189
- demo = gr.Blocks(title="Folding Studio: structure prediction with Boltz-1")
190
-
191
- with demo:
192
- gr.Markdown("# Input")
193
- with gr.Row():
194
- with gr.Column():
195
- sequence = gr.Textbox(label="Sequence", value="")
196
- api_key = gr.Textbox(label="Folding API Key", type="password")
197
- gr.Markdown("# Output")
198
- with gr.Row():
199
- predict_btn = gr.Button("Predict")
200
- with gr.Row():
201
- with gr.Column():
202
- mol_output = gr.HTML()
203
- with gr.Column():
204
- metrics_plot = gr.Plot(label="pLDDT")
205
-
206
- predict_btn.click(
207
- fn=predict,
208
- inputs=[sequence, api_key],
209
- outputs=[mol_output, metrics_plot]
210
- )
211
-
212
- demo.launch()
213
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
folding_studio_demo/__init__.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Folding Studio Demo."""
2
+
3
+ import logging
4
+
5
+ # Configure logging
6
+ logging.basicConfig(
7
+ level=logging.INFO,
8
+ format="%(asctime)s - %(levelname)s - %(message)s",
9
+ handlers=[
10
+ logging.StreamHandler(),
11
+ ],
12
+ )
folding_studio_demo/app.py ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Folding Studio Demo App."""
2
+
3
+ import logging
4
+
5
+ import gradio as gr
6
+ from folding_studio_data_models import FoldingModel
7
+ from gradio_molecule3d import Molecule3D
8
+
9
+ from folding_studio_demo.predict import predict
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ MOLECULE_REPS = [
15
+ {
16
+ "model": 0,
17
+ "chain": "",
18
+ "resname": "",
19
+ "style": "cartoon",
20
+ "color": "alphafold",
21
+ # "residue_range": "",
22
+ "around": 0,
23
+ "byres": False,
24
+ # "visible": False,
25
+ # "opacity": 0.5
26
+ }
27
+ ]
28
+
29
+ DEFAULT_PROTEIN_SEQ = ">protein description\nMALWMRLLPLLALLALWGPDPAAA"
30
+
31
+ MODEL_CHOICES = [
32
+ # ("AlphaFold2", FoldingModel.AF2),
33
+ # ("OpenFold", FoldingModel.OPENFOLD),
34
+ # ("SoloSeq", FoldingModel.SOLOSEQ),
35
+ ("Boltz-1", FoldingModel.BOLTZ),
36
+ ("Chai-1", FoldingModel.CHAI),
37
+ ("Protenix", FoldingModel.PROTENIX),
38
+ ]
39
+
40
+
41
+ def sequence_input() -> gr.Textbox:
42
+ """Sequence input component.
43
+
44
+ Returns:
45
+ gr.Textbox: Sequence input component
46
+ """
47
+ sequence = gr.Textbox(
48
+ label="Protein Sequence",
49
+ value=DEFAULT_PROTEIN_SEQ,
50
+ lines=2,
51
+ placeholder="Enter a protein sequence or upload a FASTA file",
52
+ )
53
+ file_input = gr.File(
54
+ label="Upload a FASTA file",
55
+ file_types=[".fasta", ".fa"],
56
+ )
57
+
58
+ def _process_file(file: gr.File | None) -> gr.Textbox:
59
+ if file is None:
60
+ return gr.Textbox()
61
+ try:
62
+ with open(file.name, "r") as f:
63
+ content = f.read().strip()
64
+ return gr.Textbox(value=content)
65
+ except Exception as e:
66
+ logger.error(f"Error reading file: {e}")
67
+ return gr.Textbox()
68
+
69
+ file_input.change(fn=_process_file, inputs=[file_input], outputs=[sequence])
70
+ return sequence
71
+
72
+
73
+ def simple_prediction(api_key: str) -> None:
74
+ """Simple prediction tab.
75
+
76
+ Args:
77
+ api_key (str): Folding Studio API key
78
+ """
79
+ gr.Markdown(
80
+ """
81
+ ### Predict a Protein Structure
82
+
83
+ It will be run in the background and the results will be displayed in the output section.
84
+ The output will contain the protein structure and the pLDDT plot.
85
+
86
+ Select a model to run the inference with and enter a protein sequence or upload a FASTA file.
87
+ """
88
+ )
89
+ with gr.Row():
90
+ dropdown = gr.Dropdown(
91
+ label="Model",
92
+ choices=MODEL_CHOICES,
93
+ scale=0,
94
+ value=FoldingModel.BOLTZ,
95
+ )
96
+ with gr.Column():
97
+ sequence = sequence_input()
98
+
99
+ predict_btn = gr.Button("Predict")
100
+
101
+ with gr.Row():
102
+ mol_output = Molecule3D(label="Protein Structure", reps=MOLECULE_REPS)
103
+ metrics_plot = gr.Plot(label="pLDDT")
104
+
105
+ predict_btn.click(
106
+ fn=predict,
107
+ inputs=[sequence, api_key, dropdown],
108
+ outputs=[mol_output, metrics_plot],
109
+ )
110
+
111
+
112
+ def model_comparison(api_key: str) -> None:
113
+ """Model comparison tab.
114
+
115
+ Args:
116
+ api_key (str): Folding Studio API key
117
+ """
118
+
119
+ with gr.Row():
120
+ model = gr.Dropdown(
121
+ label="Model",
122
+ choices=MODEL_CHOICES,
123
+ multiselect=True,
124
+ scale=0,
125
+ min_width=300,
126
+ value=[FoldingModel.BOLTZ, FoldingModel.CHAI, FoldingModel.PROTENIX],
127
+ )
128
+ with gr.Column():
129
+ sequence = sequence_input()
130
+
131
+ predict_btn = gr.Button("Compare Models")
132
+
133
+ with gr.Row():
134
+ mol_output = Molecule3D(label="Protein Structure", reps=MOLECULE_REPS)
135
+ metrics_plot = gr.Plot(label="pLDDT")
136
+
137
+ predict_btn.click(
138
+ fn=predict,
139
+ inputs=[sequence, api_key, model],
140
+ outputs=[mol_output, metrics_plot],
141
+ )
142
+
143
+
144
+ def __main__():
145
+ with gr.Blocks(title="Folding Studio Demo") as demo:
146
+ gr.Markdown(
147
+ """
148
+ # Folding Studio: Harness the Power of Protein Folding 🧬
149
+
150
+ Folding Studio is a platform for protein structure prediction.
151
+ It uses the latest AI-powered folding models to predict the structure of a protein.
152
+
153
+ Available models are : AlphaFold2, OpenFold, SoloSeq, Boltz-1, Chai and Protenix.
154
+
155
+ ## API Key
156
+ To use the Folding Studio API, you need to provide an API key.
157
+ You can get your API key by asking to the Folding Studio team.
158
+ """
159
+ )
160
+ api_key = gr.Textbox(label="Folding Studio API Key", type="password")
161
+ gr.Markdown("## Demo Usage")
162
+ with gr.Tab("🚀 Simple Prediction"):
163
+ simple_prediction(api_key)
164
+ with gr.Tab("📊 Model Comparison"):
165
+ model_comparison(api_key)
166
+
167
+ demo.launch()
folding_studio_demo/model_fasta_validators.py ADDED
@@ -0,0 +1,367 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Utils for validating the FASTA files for the AF3 like models."""
2
+
3
+ import logging
4
+ import re
5
+ import shutil
6
+ from abc import abstractmethod
7
+ from collections import defaultdict
8
+ from enum import Enum
9
+ from pathlib import Path
10
+
11
+ from Bio import SeqIO
12
+
13
+
14
+ class EntityType(str, Enum):
15
+ """Enum for the entity type of a given sequence."""
16
+
17
+ DNA = "dna"
18
+ RNA = "rna"
19
+ PROTEIN = "protein"
20
+ PEPTIDE = "peptide"
21
+ ION = "ion"
22
+ LIGAND = "ligand"
23
+ SMILES = "smiles"
24
+ CCD = "ccd"
25
+
26
+
27
+ def get_entity_type(sequence: str) -> EntityType:
28
+ """Get the entity type of a given sequence.
29
+
30
+ The entity type is determined based on the sequence composition.
31
+
32
+ Args:
33
+ sequence (str): The input sequence.
34
+
35
+ Returns:
36
+ EntityType: The entity type of the input sequence.
37
+ """
38
+ DNA_SEQUENCE_SET = set("ACGT")
39
+ RNA_SEQUENCE_SET = set("ACGU")
40
+ PROTEIN_SEQUENCE_SET = set("ACDEFGHIKLMNPQRSTVWY")
41
+
42
+ # Detect IONS (e.g., Mg2+, Na+, Cl-)
43
+ if re.fullmatch(r"[A-Za-z]{1,2}[\d\+\-]*", sequence):
44
+ return EntityType.ION
45
+
46
+ # Detect DNA
47
+ if set(sequence.upper()).issubset(DNA_SEQUENCE_SET):
48
+ return EntityType.DNA
49
+
50
+ # Detect RNA
51
+ elif set(sequence.upper()).issubset(RNA_SEQUENCE_SET):
52
+ return EntityType.RNA
53
+
54
+ # Detect PROTEIN
55
+ elif set(sequence.upper()).issubset(PROTEIN_SEQUENCE_SET):
56
+ return EntityType.PROTEIN
57
+
58
+ # Default to LIGAND
59
+ return EntityType.LIGAND
60
+
61
+
62
+ def has_multiple_chains(header: str) -> bool:
63
+ """Check if a given header contains multiple chains in RCSB format.
64
+
65
+ A header with multiple chains will have the following in the header:
66
+ ```
67
+ Chains A, B, C, ...
68
+ ```
69
+ where `A`, `B`, `C`, ... are the chain identifiers.
70
+
71
+
72
+ Args:
73
+ header (str): The input header string containing chain information.
74
+
75
+ Returns:
76
+ bool: True if the header contains multiple chains, False otherwise.
77
+ """
78
+ match = re.search(r"chains?\s+([A-Za-z, ]+)", header, re.I)
79
+ return len(match.group(1).replace(" ", "").split(",")) > 1 if match else False
80
+
81
+
82
+ class BaseFastaValidator:
83
+ """Base class for validating FASTA files."""
84
+
85
+ @abstractmethod
86
+ def is_valid_fasta(self, fasta_path: Path) -> tuple[bool, str | None]:
87
+ """Validate whether a given FASTA file follows the required format.
88
+
89
+ Args:
90
+ fasta_path (Path): Path to the FASTA file.
91
+
92
+ Returns:
93
+ tuple[bool, str | None]: Tuple containing a boolean indicating if the format is correct and an error message if not
94
+ """
95
+ raise NotImplementedError("Subclasses must implement this method")
96
+
97
+ @abstractmethod
98
+ def transform_fasta(self, fasta_path: Path) -> str:
99
+ """Transform a FASTA file into the required format.
100
+
101
+ Args:
102
+ fasta_path (Path): Path to the FASTA file.
103
+
104
+ Returns:
105
+ Transformed FASTA content in the required format.
106
+ """
107
+ raise NotImplementedError("Subclasses must implement this method")
108
+
109
+ def process_directory(self, input_dir: str, output_dir: str) -> None:
110
+ """Process all FASTA files in the input directory, validate or transform them, and save them to the output directory.
111
+
112
+ Args:
113
+ input_dir (str): Path to the directory containing FASTA files.
114
+ output_dir (str): Path to the output directory where processed files will be saved.
115
+ """
116
+
117
+ output_path = Path(output_dir)
118
+ output_path.mkdir(parents=True, exist_ok=True)
119
+
120
+ for fasta_file in Path(input_dir).glob("*.fasta"):
121
+ output_file = output_path / fasta_file.name
122
+ if has_multiple_chains(fasta_file.read_text()):
123
+ logging.warning(
124
+ f"Skipping {fasta_file} because it contains multiple chains in a single sequence.\n"
125
+ "Please split multiple chains into separate sequences using the following format:\n"
126
+ ">Chain A\n"
127
+ "MTEIVLKFL...\n"
128
+ ">Chain B\n"
129
+ "MTEIVLKFL...\n\n"
130
+ "Instead of:\n"
131
+ ">Chains A, B\n"
132
+ "MTEIVLKFL..."
133
+ )
134
+ continue
135
+ if self.is_valid_fasta(fasta_file):
136
+ shutil.copy(fasta_file, output_file)
137
+ else:
138
+ transformed_content = self.transform_fasta(fasta_file)
139
+ output_file.write_text(transformed_content)
140
+
141
+
142
+ class BoltzFastaValidator(BaseFastaValidator):
143
+ """Validate whether a given FASTA file follows the required format for Boltz."""
144
+
145
+ SUPPORTED_ENTITY_TYPES = {
146
+ EntityType.PROTEIN,
147
+ EntityType.RNA,
148
+ EntityType.DNA,
149
+ EntityType.SMILES,
150
+ EntityType.CCD,
151
+ }
152
+
153
+ def is_valid_fasta(self, fasta_path: Path) -> tuple[bool, str | None]:
154
+ """Validate whether a given FASTA file follows the required format.
155
+
156
+ The expected FASTA header format is:
157
+ ```
158
+ >CHAIN_ID|ENTITY_TYPE
159
+ ```
160
+ where `ENTITY_TYPE` must be one of: "protein", "rna", "dna", "smiles" or "ccd".
161
+
162
+ Args:
163
+ fasta_path (Path): Path to the FASTA file.
164
+
165
+ Returns:
166
+ tuple[bool, str | None]: Tuple containing a boolean indicating if the format is correct and an error message if not
167
+ """
168
+ with fasta_path.open("r") as f:
169
+ for record in SeqIO.parse(f, "fasta"):
170
+ header_parts = record.id.split("|")
171
+ if not (1 < len(header_parts) <= 3):
172
+ msg = "BOLTZ Validation Error: Invalid header format. Expected '>CHAIN_ID|ENTITY_TYPE'"
173
+ return False, msg
174
+ if header_parts[1].lower() not in self.SUPPORTED_ENTITY_TYPES:
175
+ return (
176
+ False,
177
+ f"BOLTZ Validation Error: Invalid entity type '{header_parts[1]}'. Supported types: {', '.join(self.SUPPORTED_ENTITY_TYPES)}",
178
+ )
179
+ return True, None
180
+
181
+ def transform_fasta(self, fasta_path: Path) -> str:
182
+ """Transform a FASTA file into the '>CHAIN_ID|ENTITY_TYPE|MSA_ID' format.
183
+
184
+ This function extracts chain identifiers from the FASTA header and determines
185
+ the entity type (DNA, RNA, or PROTEIN) based on the sequence composition.
186
+
187
+ Args:
188
+ fasta_path (Path): Path to the FASTA file.
189
+
190
+ Returns:
191
+ Transformed FASTA content in the required format.
192
+ """
193
+ transformed_lines = []
194
+
195
+ with fasta_path.open("r") as f:
196
+ for record_index, record in enumerate(SeqIO.parse(f, "fasta")):
197
+ chain = chr(ord("A") + record_index)
198
+ # extract entity type
199
+ entity_type = get_entity_type(str(record.seq))
200
+ transformed_lines.append(f">{chain.upper()}|{entity_type.value}")
201
+ # append sequence
202
+ transformed_lines.append(str(record.seq))
203
+
204
+ return "\n".join(transformed_lines)
205
+
206
+
207
+ class ChaiFastaValidator(BaseFastaValidator):
208
+ """Validate whether a given FASTA file follows the required format for Chai."""
209
+
210
+ SUPPORTED_ENTITY_TYPES = EntityType.__members__.values()
211
+
212
+ def is_valid_fasta(self, fasta_path: Path) -> tuple[bool, str | None]:
213
+ """Validate whether a given FASTA file follows the required format.
214
+
215
+ The expected FASTA header format is:
216
+ ```
217
+ >ENTITY_TYPE|name=NAME
218
+ ```
219
+ Args:
220
+ fasta_path (Path): Path to the FASTA file.
221
+
222
+ Returns:
223
+ tuple[bool, str | None]: Tuple containing a boolean indicating if the format is correct and an error message if not
224
+ """
225
+
226
+ seen_names = set()
227
+ with fasta_path.open("r") as f:
228
+ for record in SeqIO.parse(f, "fasta"):
229
+ # validate header format
230
+ match = re.match(r"^([A-Za-z]+)\|name=([\w\-]+)$", record.description)
231
+ if not match:
232
+ return (
233
+ False,
234
+ "CHAI Validation Error: Invalid header format. Expected '>ENTITY_TYPE|name=NAME'",
235
+ )
236
+ # validate entity type
237
+ entity_type, name = match.groups()
238
+ if entity_type not in self.SUPPORTED_ENTITY_TYPES or not name:
239
+ return (
240
+ False,
241
+ f"CHAI Validation Error: Invalid entity type '{entity_type}'. Supported types: {', '.join(self.SUPPORTED_ENTITY_TYPES)}",
242
+ )
243
+ # check uniqueness of name
244
+ if name in seen_names:
245
+ return (
246
+ False,
247
+ f"CHAI Validation Error: Duplicate name '{name}'. Each sequence must have a unique name",
248
+ )
249
+ seen_names.add(name)
250
+ # validate sequence format
251
+ sequence = str(record.seq).strip()
252
+ if (
253
+ entity_type in {EntityType.PEPTIDE, EntityType.PROTEIN}
254
+ and not get_entity_type(sequence) == entity_type
255
+ ):
256
+ return (
257
+ False,
258
+ f"CHAI Validation Error: Sequence type mismatch. Expected '{entity_type}' but found '{get_entity_type(sequence)}'",
259
+ )
260
+
261
+ return True, None
262
+
263
+ def transform_fasta(self, fasta_path: Path) -> str:
264
+ """Transform a FASTA file into the '>TYPE|name=NAME' format by ensuring each main header
265
+ is unique (adding a number if necessary).
266
+
267
+ The expected output format is:
268
+ '>protein|name=NAME'
269
+ 'SEQUENCE'
270
+
271
+ Args:
272
+ fasta_path (Path): Path to the FASTA file.
273
+
274
+ Returns:
275
+ Transformed FASTA content in the required Chai format.
276
+ """
277
+ transformed_lines = []
278
+ header_map = {}
279
+
280
+ with fasta_path.open("r") as f:
281
+ for record in SeqIO.parse(f, "fasta"):
282
+ main_header = record.description.split("|")[0].strip()
283
+
284
+ if main_header not in header_map:
285
+ header_map[main_header] = 1
286
+ updated_header = main_header
287
+
288
+ else:
289
+ header_map[main_header] += 1
290
+ updated_header = main_header + "_" + str(header_map[main_header])
291
+
292
+ entity_type = get_entity_type(str(record.seq))
293
+ header = f">{entity_type.value}|name={updated_header}"
294
+
295
+ transformed_lines.append(header)
296
+ transformed_lines.append(str(record.seq))
297
+
298
+ return "\n".join(transformed_lines)
299
+
300
+
301
+ class ProtenixFastaValidator(BaseFastaValidator):
302
+ """Validate whether a given FASTA file follows the required format for Protenix."""
303
+
304
+ def is_valid_fasta(self, fasta_path: Path) -> tuple[bool, str | None]:
305
+ """Validate whether a given FASTA file follows the required format.
306
+
307
+ The expected FASTA header format is:
308
+ ```
309
+ > UNIQUE ID[|...]
310
+ ```
311
+
312
+ Args:
313
+ fasta_path (Path): Path to the FASTA file.
314
+
315
+ Returns:
316
+ tuple[bool, str | None]: Tuple containing a boolean indicating if the format is correct and an error message if not
317
+ """
318
+ seen_headers = set()
319
+
320
+ with fasta_path.open("r") as f:
321
+ for record in SeqIO.parse(f, "fasta"):
322
+ main_header = record.description.split("|")[0].strip()
323
+ if main_header in seen_headers:
324
+ return (
325
+ False,
326
+ f"PROTENIX Validation Error: Duplicate header '{main_header}'. Each sequence must have a unique header",
327
+ )
328
+ seen_headers.add(main_header)
329
+
330
+ return True, None
331
+
332
+ def transform_fasta(self, fasta_path: Path) -> str:
333
+ """Transform a FASTA file into the '>NAME|Chain X' format by ensuring each main header
334
+ is unique (adding a number if necessary).
335
+
336
+ The expected output format is:
337
+ '>protein_1 | Chain A'
338
+ 'SEQUENCE'
339
+ '>protein_2 | Chain B'
340
+ 'SEQUENCE'
341
+
342
+ Args:
343
+ fasta_path (Path): Path to the FASTA file.
344
+
345
+ Returns:
346
+ Transformed FASTA content in the required Protenix format.
347
+ """
348
+ transformed_lines = []
349
+ header_count = defaultdict(int)
350
+
351
+ with fasta_path.open("r") as f:
352
+ for record in SeqIO.parse(f, "fasta"):
353
+ header_parts = [part.strip() for part in record.description.split("|")]
354
+ main_header = header_parts[0]
355
+
356
+ # Ensure unique headers
357
+ header_count[main_header] += 1
358
+ updated_main_header = (
359
+ f"{main_header}_{header_count[main_header]}"
360
+ if header_count[main_header] > 1
361
+ else main_header
362
+ )
363
+
364
+ transformed_lines.append(f">{updated_main_header}")
365
+ transformed_lines.append(str(record.seq))
366
+
367
+ return "\n".join(transformed_lines)
molecule.py → folding_studio_demo/molecule.py RENAMED
@@ -1,15 +1,16 @@
1
- import logging
2
 
3
  # Configure logging
4
  logging.basicConfig(
5
  level=logging.INFO,
6
- format='%(asctime)s - %(levelname)s - %(message)s',
7
  handlers=[
8
  logging.StreamHandler(),
9
- ]
10
  )
11
  logger = logging.getLogger(__name__)
12
 
 
13
  def read_mol(molpath):
14
  with open(molpath, "r") as fp:
15
  lines = fp.readlines()
@@ -19,10 +20,7 @@ def read_mol(molpath):
19
  return mol
20
 
21
 
22
- def molecule(
23
- input_pdb, lenSeqs, num_res, selectedResidues, allSeqs, sequences
24
- ):
25
-
26
  options = ""
27
  pred_mol = "["
28
  seqdata = "{"
@@ -265,5 +263,5 @@ def molecule(
265
  </script>
266
  </body></html>"""
267
  )
268
-
269
- return x
 
1
+ import logging
2
 
3
  # Configure logging
4
  logging.basicConfig(
5
  level=logging.INFO,
6
+ format="%(asctime)s - %(levelname)s - %(message)s",
7
  handlers=[
8
  logging.StreamHandler(),
9
+ ],
10
  )
11
  logger = logging.getLogger(__name__)
12
 
13
+
14
  def read_mol(molpath):
15
  with open(molpath, "r") as fp:
16
  lines = fp.readlines()
 
20
  return mol
21
 
22
 
23
+ def molecule(input_pdb, lenSeqs, num_res, selectedResidues, allSeqs, sequences):
 
 
 
24
  options = ""
25
  pred_mol = "["
26
  seqdata = "{"
 
263
  </script>
264
  </body></html>"""
265
  )
266
+
267
+ return x
folding_studio_demo/predict.py ADDED
@@ -0,0 +1,279 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Predict protein structure using Folding Studio."""
2
+
3
+ import hashlib
4
+ import logging
5
+ import os
6
+ from pathlib import Path
7
+
8
+ import gradio as gr
9
+ import numpy as np
10
+ import plotly.graph_objects as go
11
+ from Bio import SeqIO
12
+ from Bio.PDB import PDBIO, MMCIFParser
13
+ from folding_studio.client import Client
14
+ from folding_studio.query import Query
15
+ from folding_studio.query.boltz import BoltzQuery
16
+ from folding_studio.query.chai import ChaiQuery
17
+ from folding_studio.query.protenix import ProtenixQuery
18
+ from folding_studio_data_models import FoldingModel
19
+
20
+ from folding_studio_demo.model_fasta_validators import (
21
+ BaseFastaValidator,
22
+ BoltzFastaValidator,
23
+ ChaiFastaValidator,
24
+ ProtenixFastaValidator,
25
+ )
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+ SEQUENCE_DIR = Path("sequences")
30
+ SEQUENCE_DIR.mkdir(parents=True, exist_ok=True)
31
+
32
+ OUTPUT_DIR = Path("output")
33
+ OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
34
+
35
+
36
+ def convert_cif_to_pdb(cif_path: str, pdb_path: str) -> None:
37
+ """Convert a .cif file to .pdb format using Biopython.
38
+
39
+ Args:
40
+ cif_path (str): Path to input .cif file
41
+ pdb_path (str): Path to output .pdb file
42
+ """
43
+ # Parse the CIF file
44
+ parser = MMCIFParser()
45
+ structure = parser.get_structure("structure", cif_path)
46
+
47
+ # Save as PDB
48
+ io = PDBIO()
49
+ io.set_structure(structure)
50
+ io.save(pdb_path)
51
+
52
+
53
+ def add_plddt_plot(plddt_vals: list[float]) -> str:
54
+ """Create a plot of metrics."""
55
+ visible = True
56
+ plddt_trace = go.Scatter(
57
+ x=np.arange(len(plddt_vals)),
58
+ y=plddt_vals,
59
+ hovertemplate="<i>pLDDT</i>: %{y:.2f} <br><i>Residue index:</i> %{x}<br>",
60
+ name="seq",
61
+ visible=visible,
62
+ )
63
+
64
+ plddt_fig = go.Figure(data=[plddt_trace])
65
+ plddt_fig.update_layout(
66
+ title="pLDDT",
67
+ xaxis_title="Residue index",
68
+ yaxis_title="pLDDT",
69
+ height=500,
70
+ template="simple_white",
71
+ legend=dict(yanchor="bottom", y=0.01, xanchor="left", x=0.99),
72
+ )
73
+ return plddt_fig
74
+
75
+
76
+ def _write_fasta_file(
77
+ sequence: str, directory: Path = SEQUENCE_DIR
78
+ ) -> tuple[str, Path]:
79
+ """Write sequence to FASTA file.
80
+
81
+ Args:
82
+ sequence (str): Sequence to write to FASTA file
83
+ directory (Path): Directory to write FASTA file to (default: SEQUENCE_DIR)
84
+
85
+ Returns:
86
+ tuple[str, Path]: Tuple containing the sequence ID and the path to the FASTA file
87
+ """
88
+ seq_id = hashlib.sha1(sequence.encode()).hexdigest()
89
+ seq_file = directory / f"sequence_{seq_id}.fasta"
90
+ with open(seq_file, "w") as f:
91
+ f.write(sequence)
92
+ return seq_id, seq_file
93
+
94
+
95
+ class AF3Model:
96
+ def __init__(
97
+ self, api_key: str, model_name: str, query: Query, validator: BaseFastaValidator
98
+ ):
99
+ self.api_key = api_key
100
+ self.model_name = model_name
101
+ self.query = query
102
+ self.validator = validator
103
+
104
+ def call(self, seq_file: Path | str, output_dir: Path) -> None:
105
+ """Predict protein structure from amino acid sequence using AF3 model.
106
+
107
+ Args:
108
+ seq_file (Path | str): Path to FASTA file containing amino acid sequence
109
+ output_dir (Path): Path to output directory
110
+ """
111
+ # Validate FASTA format before calling
112
+ is_valid, error_msg = self.check_file_description(seq_file)
113
+ if not is_valid:
114
+ logger.error(error_msg)
115
+ raise gr.Error(error_msg)
116
+
117
+ # Create a client using API key
118
+ logger.info("Authenticating client with API key")
119
+ client = Client.from_api_key(api_key=self.api_key)
120
+
121
+ # Define query
122
+ query: Query = self.query.from_file(path=seq_file, query_name="gradio")
123
+ query.save_parameters(output_dir)
124
+
125
+ logger.info("Payload: %s", query.payload)
126
+
127
+ # Send a request
128
+ logger.info(f"Sending {self.model_name} request to Folding Studio API")
129
+ response = client.send_request(
130
+ query, project_code=os.environ["FOLDING_PROJECT_CODE"]
131
+ )
132
+
133
+ # Access confidence data
134
+ logger.info("Confidence data: %s", response.confidence_data)
135
+
136
+ response.download_results(output_dir=output_dir, force=True, unzip=True)
137
+ logger.info("Results downloaded to %s", output_dir)
138
+
139
+ def format_fasta(self, sequence: str) -> str:
140
+ """Format sequence to FASTA format."""
141
+ return f">{self.model_name}\n{sequence}"
142
+
143
+ def predictions(self, output_dir: Path) -> list[Path]:
144
+ """Get the path to the prediction."""
145
+ raise NotImplementedError("Not implemented")
146
+
147
+ def has_prediction(self, output_dir: Path) -> bool:
148
+ """Check if prediction exists in output directory."""
149
+ return any(self.predictions(output_dir))
150
+
151
+ def check_file_description(self, seq_file: Path | str) -> tuple[bool, str | None]:
152
+ """Check if the file description is correct.
153
+
154
+ Args:
155
+ seq_file (Path | str): Path to FASTA file
156
+
157
+ Returns:
158
+ tuple[bool, str | None]: Tuple containing a boolean indicating if the format is correct and an error message if not
159
+ """
160
+ input_rep = list(SeqIO.parse(seq_file, "fasta"))
161
+ if not input_rep:
162
+ error_msg = f"{self.model_name.upper()} Validation Error: No sequence found"
163
+ return False, error_msg
164
+
165
+ is_valid, error_msg = self.validator.is_valid_fasta(seq_file)
166
+ if not is_valid:
167
+ return False, error_msg
168
+
169
+ return True, None
170
+
171
+
172
+ class ChaiModel(AF3Model):
173
+ def __init__(self, api_key: str):
174
+ super().__init__(api_key, "Chai", ChaiQuery, ChaiFastaValidator())
175
+
176
+ def call(self, seq_file: Path | str, output_dir: Path) -> None:
177
+ """Predict protein structure from amino acid sequence using Chai model.
178
+
179
+ Args:
180
+ seq_file (Path | str): Path to FASTA file containing amino acid sequence
181
+ output_dir (Path): Path to output directory
182
+ """
183
+ super().call(seq_file, output_dir)
184
+
185
+ def predictions(self, output_dir: Path) -> list[Path]:
186
+ """Get the path to the prediction."""
187
+ return list(output_dir.rglob("*_model_[0-9].cif"))
188
+
189
+
190
+ class ProtenixModel(AF3Model):
191
+ def __init__(self, api_key: str):
192
+ super().__init__(api_key, "Protenix", ProtenixQuery, ProtenixFastaValidator())
193
+
194
+ def call(self, seq_file: Path | str, output_dir: Path) -> None:
195
+ """Predict protein structure from amino acid sequence using Protenix model.
196
+
197
+ Args:
198
+ seq_file (Path | str): Path to FASTA file containing amino acid sequence
199
+ output_dir (Path): Path to output directory
200
+ """
201
+ super().call(seq_file, output_dir)
202
+
203
+ def predictions(self, output_dir: Path) -> list[Path]:
204
+ """Get the path to the prediction."""
205
+ return list(output_dir.rglob("*_model_[0-9].cif"))
206
+
207
+
208
+ class BoltzModel(AF3Model):
209
+ def __init__(self, api_key: str):
210
+ super().__init__(api_key, "Boltz", BoltzQuery, BoltzFastaValidator())
211
+
212
+ def call(self, seq_file: Path | str, output_dir: Path) -> None:
213
+ """Predict protein structure from amino acid sequence using Boltz model.
214
+
215
+ Args:
216
+ seq_file (Path | str): Path to FASTA file containing amino acid sequence
217
+ output_dir (Path): Path to output directory
218
+ """
219
+
220
+ super().call(seq_file, output_dir)
221
+
222
+ def predictions(self, output_dir: Path) -> list[Path]:
223
+ """Get the path to the prediction."""
224
+ return list(output_dir.rglob("*_model_[0-9].cif"))
225
+
226
+
227
+ def predict(sequence: str, api_key: str, model_type: FoldingModel) -> tuple[str, str]:
228
+ """Predict protein structure from amino acid sequence using Boltz model.
229
+
230
+ Args:
231
+ sequence (str): Amino acid sequence to predict structure for
232
+ api_key (str): Folding API key
233
+ model (FoldingModel): Folding model to use
234
+
235
+ Returns:
236
+ tuple[str, str]: Tuple containing the path to the PDB file and the pLDDT plot
237
+ """
238
+
239
+ # Set up unique output directory based on sequence hash
240
+ seq_id, seq_file = _write_fasta_file(sequence)
241
+ output_dir = OUTPUT_DIR / seq_id / model_type
242
+ output_dir.mkdir(parents=True, exist_ok=True)
243
+
244
+ if model_type == FoldingModel.BOLTZ:
245
+ model = BoltzModel(api_key)
246
+ elif model_type == FoldingModel.CHAI:
247
+ model = ChaiModel(api_key)
248
+ elif model_type == FoldingModel.PROTENIX:
249
+ model = ProtenixModel(api_key)
250
+ else:
251
+ raise ValueError(f"Model {model_type} not supported")
252
+
253
+ # Check if prediction already exists
254
+ if not model.has_prediction(output_dir):
255
+ # Run Boltz prediction
256
+ logger.info(f"Predicting {seq_id}")
257
+ model.call(seq_file=seq_file, output_dir=output_dir)
258
+ logger.info("Prediction done. Output directory: %s", output_dir)
259
+ else:
260
+ logger.info("Prediction already exists. Output directory: %s", output_dir)
261
+
262
+ # output_dir = Path("boltz_results") # debug
263
+
264
+ # Convert output CIF to PDB
265
+ if not model.has_prediction(output_dir):
266
+ raise gr.Error("No prediction found")
267
+
268
+ pred_cif = model.predictions(output_dir)[0]
269
+ logger.info("Output file: %s", pred_cif)
270
+
271
+ converted_pdb_path = str(output_dir / f"pred_{seq_id}.pdb")
272
+ convert_cif_to_pdb(str(pred_cif), str(converted_pdb_path))
273
+ logger.info("Converted PDB file: %s", converted_pdb_path)
274
+
275
+ plddt_file = list(pred_cif.parent.glob("plddt_*.npz"))[0]
276
+ logger.info("plddt file: %s", plddt_file)
277
+ plddt_vals = np.load(plddt_file)["plddt"]
278
+
279
+ return converted_pdb_path, add_plddt_plot(plddt_vals=plddt_vals)
folding_studio_demo/protein.py ADDED
File without changes
pyproject.toml CHANGED
@@ -1,12 +1,25 @@
1
  [project]
2
- name = "fs"
3
  version = "0.1.0"
4
  description = "Add your description here"
5
  readme = "README.md"
6
  requires-python = ">=3.11"
7
  dependencies = [
8
  "gradio==5.30.0",
 
9
  "ipython>=9.2.0",
10
  "numpy>=2.2.6",
11
  "plotly>=6.1.1",
 
 
12
  ]
 
 
 
 
 
 
 
 
 
 
 
1
  [project]
2
+ name = "folding-studio-demo"
3
  version = "0.1.0"
4
  description = "Add your description here"
5
  readme = "README.md"
6
  requires-python = ">=3.11"
7
  dependencies = [
8
  "gradio==5.30.0",
9
+ "gradio-molecule3d>=0.0.7",
10
  "ipython>=9.2.0",
11
  "numpy>=2.2.6",
12
  "plotly>=6.1.1",
13
+ "folding-studio",
14
+ "biopython>=1.85",
15
  ]
16
+
17
+ [build-system]
18
+ requires = ["hatchling"]
19
+ build-backend = "hatchling.build"
20
+
21
+ [tool.uv.sources]
22
+ folding-studio = { path = "./folding-studio" }
23
+
24
+ [project.scripts]
25
+ folding-studio-demo = "folding_studio_demo.app:__main__"
uv.lock CHANGED
@@ -88,6 +88,45 @@ wheels = [
88
  { url = "https://files.pythonhosted.org/packages/5d/35/be73b6015511aa0173ec595fc579133b797ad532996f2998fd6b8d1bbe6b/audioop_lts-0.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:78bfb3703388c780edf900be66e07de5a3d4105ca8e8720c5c4d67927e0b15d0", size = 23918 },
89
  ]
90
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  [[package]]
92
  name = "certifi"
93
  version = "2025.4.26"
@@ -157,6 +196,20 @@ wheels = [
157
  { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 },
158
  ]
159
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
  [[package]]
161
  name = "colorama"
162
  version = "0.4.6"
@@ -217,11 +270,61 @@ wheels = [
217
  ]
218
 
219
  [[package]]
220
- name = "fs"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
221
  version = "0.1.0"
222
- source = { virtual = "." }
223
  dependencies = [
 
 
224
  { name = "gradio" },
 
225
  { name = "ipython" },
226
  { name = "numpy" },
227
  { name = "plotly" },
@@ -229,7 +332,10 @@ dependencies = [
229
 
230
  [package.metadata]
231
  requires-dist = [
 
 
232
  { name = "gradio", specifier = "==5.30.0" },
 
233
  { name = "ipython", specifier = ">=9.2.0" },
234
  { name = "numpy", specifier = ">=2.2.6" },
235
  { name = "plotly", specifier = ">=6.1.1" },
@@ -244,6 +350,117 @@ wheels = [
244
  { url = "https://files.pythonhosted.org/packages/2c/a9/a7022f58e081149ec0184c31ea81dcee605e1d46380b48122e1ef94ac24e/fsspec-2025.5.0-py3-none-any.whl", hash = "sha256:0ca253eca6b5333d8a2b8bd98c7326fe821f1f0fdbd34e1b445bddde8e804c95", size = 196164 },
245
  ]
246
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
247
  [[package]]
248
  name = "gradio"
249
  version = "5.30.0"
@@ -301,6 +518,18 @@ wheels = [
301
  { url = "https://files.pythonhosted.org/packages/55/6f/03eb8e0e0ec80eced5ed35a63376dabfc7391b1538502f8e85e9dc5bab02/gradio_client-1.10.1-py3-none-any.whl", hash = "sha256:fcff53f6aad3dfa9dd082adedb94256172d6b20666b1ef66480d82023e1907db", size = 323141 },
302
  ]
303
 
 
 
 
 
 
 
 
 
 
 
 
 
304
  [[package]]
305
  name = "groovy"
306
  version = "0.1.2"
@@ -778,6 +1007,32 @@ wheels = [
778
  { url = "https://files.pythonhosted.org/packages/ce/4f/5249960887b1fbe561d9ff265496d170b55a735b76724f10ef19f9e40716/prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07", size = 387810 },
779
  ]
780
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
781
  [[package]]
782
  name = "ptyprocess"
783
  version = "0.7.0"
@@ -796,65 +1051,105 @@ wheels = [
796
  { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842 },
797
  ]
798
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
799
  [[package]]
800
  name = "pydantic"
801
- version = "2.9.2"
802
  source = { registry = "https://pypi.org/simple" }
803
  dependencies = [
804
  { name = "annotated-types" },
805
  { name = "pydantic-core" },
806
  { name = "typing-extensions" },
 
807
  ]
808
- sdist = { url = "https://files.pythonhosted.org/packages/a9/b7/d9e3f12af310e1120c21603644a1cd86f59060e040ec5c3a80b8f05fae30/pydantic-2.9.2.tar.gz", hash = "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f", size = 769917 }
809
  wheels = [
810
- { url = "https://files.pythonhosted.org/packages/df/e4/ba44652d562cbf0bf320e0f3810206149c8a4e99cdbf66da82e97ab53a15/pydantic-2.9.2-py3-none-any.whl", hash = "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12", size = 434928 },
811
  ]
812
 
813
  [[package]]
814
  name = "pydantic-core"
815
- version = "2.23.4"
816
  source = { registry = "https://pypi.org/simple" }
817
  dependencies = [
818
  { name = "typing-extensions" },
819
  ]
820
- sdist = { url = "https://files.pythonhosted.org/packages/e2/aa/6b6a9b9f8537b872f552ddd46dd3da230367754b6f707b8e1e963f515ea3/pydantic_core-2.23.4.tar.gz", hash = "sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863", size = 402156 }
821
- wheels = [
822
- { url = "https://files.pythonhosted.org/packages/5d/30/890a583cd3f2be27ecf32b479d5d615710bb926d92da03e3f7838ff3e58b/pydantic_core-2.23.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:77733e3892bb0a7fa797826361ce8a9184d25c8dffaec60b7ffe928153680ba8", size = 1865160 },
823
- { url = "https://files.pythonhosted.org/packages/1d/9a/b634442e1253bc6889c87afe8bb59447f106ee042140bd57680b3b113ec7/pydantic_core-2.23.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b84d168f6c48fabd1f2027a3d1bdfe62f92cade1fb273a5d68e621da0e44e6d", size = 1776777 },
824
- { url = "https://files.pythonhosted.org/packages/75/9a/7816295124a6b08c24c96f9ce73085032d8bcbaf7e5a781cd41aa910c891/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df49e7a0861a8c36d089c1ed57d308623d60416dab2647a4a17fe050ba85de0e", size = 1799244 },
825
- { url = "https://files.pythonhosted.org/packages/a9/8f/89c1405176903e567c5f99ec53387449e62f1121894aa9fc2c4fdc51a59b/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff02b6d461a6de369f07ec15e465a88895f3223eb75073ffea56b84d9331f607", size = 1805307 },
826
- { url = "https://files.pythonhosted.org/packages/d5/a5/1a194447d0da1ef492e3470680c66048fef56fc1f1a25cafbea4bc1d1c48/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:996a38a83508c54c78a5f41456b0103c30508fed9abcad0a59b876d7398f25fd", size = 2000663 },
827
- { url = "https://files.pythonhosted.org/packages/13/a5/1df8541651de4455e7d587cf556201b4f7997191e110bca3b589218745a5/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d97683ddee4723ae8c95d1eddac7c192e8c552da0c73a925a89fa8649bf13eea", size = 2655941 },
828
- { url = "https://files.pythonhosted.org/packages/44/31/a3899b5ce02c4316865e390107f145089876dff7e1dfc770a231d836aed8/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:216f9b2d7713eb98cb83c80b9c794de1f6b7e3145eef40400c62e86cee5f4e1e", size = 2052105 },
829
- { url = "https://files.pythonhosted.org/packages/1b/aa/98e190f8745d5ec831f6d5449344c48c0627ac5fed4e5340a44b74878f8e/pydantic_core-2.23.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6f783e0ec4803c787bcea93e13e9932edab72068f68ecffdf86a99fd5918878b", size = 1919967 },
830
- { url = "https://files.pythonhosted.org/packages/ae/35/b6e00b6abb2acfee3e8f85558c02a0822e9a8b2f2d812ea8b9079b118ba0/pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d0776dea117cf5272382634bd2a5c1b6eb16767c223c6a5317cd3e2a757c61a0", size = 1964291 },
831
- { url = "https://files.pythonhosted.org/packages/13/46/7bee6d32b69191cd649bbbd2361af79c472d72cb29bb2024f0b6e350ba06/pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d5f7a395a8cf1621939692dba2a6b6a830efa6b3cee787d82c7de1ad2930de64", size = 2109666 },
832
- { url = "https://files.pythonhosted.org/packages/39/ef/7b34f1b122a81b68ed0a7d0e564da9ccdc9a2924c8d6c6b5b11fa3a56970/pydantic_core-2.23.4-cp311-none-win32.whl", hash = "sha256:74b9127ffea03643e998e0c5ad9bd3811d3dac8c676e47db17b0ee7c3c3bf35f", size = 1732940 },
833
- { url = "https://files.pythonhosted.org/packages/2f/76/37b7e76c645843ff46c1d73e046207311ef298d3f7b2f7d8f6ac60113071/pydantic_core-2.23.4-cp311-none-win_amd64.whl", hash = "sha256:98d134c954828488b153d88ba1f34e14259284f256180ce659e8d83e9c05eaa3", size = 1916804 },
834
- { url = "https://files.pythonhosted.org/packages/74/7b/8e315f80666194b354966ec84b7d567da77ad927ed6323db4006cf915f3f/pydantic_core-2.23.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f3e0da4ebaef65158d4dfd7d3678aad692f7666877df0002b8a522cdf088f231", size = 1856459 },
835
- { url = "https://files.pythonhosted.org/packages/14/de/866bdce10ed808323d437612aca1ec9971b981e1c52e5e42ad9b8e17a6f6/pydantic_core-2.23.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f69a8e0b033b747bb3e36a44e7732f0c99f7edd5cea723d45bc0d6e95377ffee", size = 1770007 },
836
- { url = "https://files.pythonhosted.org/packages/dc/69/8edd5c3cd48bb833a3f7ef9b81d7666ccddd3c9a635225214e044b6e8281/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:723314c1d51722ab28bfcd5240d858512ffd3116449c557a1336cbe3919beb87", size = 1790245 },
837
- { url = "https://files.pythonhosted.org/packages/80/33/9c24334e3af796ce80d2274940aae38dd4e5676298b4398eff103a79e02d/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb2802e667b7051a1bebbfe93684841cc9351004e2badbd6411bf357ab8d5ac8", size = 1801260 },
838
- { url = "https://files.pythonhosted.org/packages/a5/6f/e9567fd90104b79b101ca9d120219644d3314962caa7948dd8b965e9f83e/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d18ca8148bebe1b0a382a27a8ee60350091a6ddaf475fa05ef50dc35b5df6327", size = 1996872 },
839
- { url = "https://files.pythonhosted.org/packages/2d/ad/b5f0fe9e6cfee915dd144edbd10b6e9c9c9c9d7a56b69256d124b8ac682e/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33e3d65a85a2a4a0dc3b092b938a4062b1a05f3a9abde65ea93b233bca0e03f2", size = 2661617 },
840
- { 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", hash = "sha256:128585782e5bfa515c590ccee4b727fb76925dd04a98864182b22e89a4e6ed36", size = 2071831 },
841
- { url = "https://files.pythonhosted.org/packages/89/4d/3079d00c47f22c9a9a8220db088b309ad6e600a73d7a69473e3a8e5e3ea3/pydantic_core-2.23.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:68665f4c17edcceecc112dfed5dbe6f92261fb9d6054b47d01bf6371a6196126", size = 1917453 },
842
- { url = "https://files.pythonhosted.org/packages/e9/88/9df5b7ce880a4703fcc2d76c8c2d8eb9f861f79d0c56f4b8f5f2607ccec8/pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:20152074317d9bed6b7a95ade3b7d6054845d70584216160860425f4fbd5ee9e", size = 1968793 },
843
- { url = "https://files.pythonhosted.org/packages/e3/b9/41f7efe80f6ce2ed3ee3c2dcfe10ab7adc1172f778cc9659509a79518c43/pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9261d3ce84fa1d38ed649c3638feefeae23d32ba9182963e465d58d62203bd24", size = 2116872 },
844
- { url = "https://files.pythonhosted.org/packages/63/08/b59b7a92e03dd25554b0436554bf23e7c29abae7cce4b1c459cd92746811/pydantic_core-2.23.4-cp312-none-win32.whl", hash = "sha256:4ba762ed58e8d68657fc1281e9bb72e1c3e79cc5d464be146e260c541ec12d84", size = 1738535 },
845
- { url = "https://files.pythonhosted.org/packages/88/8d/479293e4d39ab409747926eec4329de5b7129beaedc3786eca070605d07f/pydantic_core-2.23.4-cp312-none-win_amd64.whl", hash = "sha256:97df63000f4fea395b2824da80e169731088656d1818a11b95f3b173747b6cd9", size = 1917992 },
846
- { url = "https://files.pythonhosted.org/packages/ad/ef/16ee2df472bf0e419b6bc68c05bf0145c49247a1095e85cee1463c6a44a1/pydantic_core-2.23.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7530e201d10d7d14abce4fb54cfe5b94a0aefc87da539d0346a484ead376c3cc", size = 1856143 },
847
- { url = "https://files.pythonhosted.org/packages/da/fa/bc3dbb83605669a34a93308e297ab22be82dfb9dcf88c6cf4b4f264e0a42/pydantic_core-2.23.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df933278128ea1cd77772673c73954e53a1c95a4fdf41eef97c2b779271bd0bd", size = 1770063 },
848
- { url = "https://files.pythonhosted.org/packages/4e/48/e813f3bbd257a712303ebdf55c8dc46f9589ec74b384c9f652597df3288d/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cb3da3fd1b6a5d0279a01877713dbda118a2a4fc6f0d821a57da2e464793f05", size = 1790013 },
849
- { url = "https://files.pythonhosted.org/packages/b4/e0/56eda3a37929a1d297fcab1966db8c339023bcca0b64c5a84896db3fcc5c/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c6dcb030aefb668a2b7009c85b27f90e51e6a3b4d5c9bc4c57631292015b0d", size = 1801077 },
850
- { url = "https://files.pythonhosted.org/packages/04/be/5e49376769bfbf82486da6c5c1683b891809365c20d7c7e52792ce4c71f3/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:696dd8d674d6ce621ab9d45b205df149399e4bb9aa34102c970b721554828510", size = 1996782 },
851
- { url = "https://files.pythonhosted.org/packages/bc/24/e3ee6c04f1d58cc15f37bcc62f32c7478ff55142b7b3e6d42ea374ea427c/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2971bb5ffe72cc0f555c13e19b23c85b654dd2a8f7ab493c262071377bfce9f6", size = 2661375 },
852
- { url = "https://files.pythonhosted.org/packages/c1/f8/11a9006de4e89d016b8de74ebb1db727dc100608bb1e6bbe9d56a3cbbcce/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8394d940e5d400d04cad4f75c0598665cbb81aecefaca82ca85bd28264af7f9b", size = 2071635 },
853
- { url = "https://files.pythonhosted.org/packages/7c/45/bdce5779b59f468bdf262a5bc9eecbae87f271c51aef628d8c073b4b4b4c/pydantic_core-2.23.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0dff76e0602ca7d4cdaacc1ac4c005e0ce0dcfe095d5b5259163a80d3a10d327", size = 1916994 },
854
- { url = "https://files.pythonhosted.org/packages/d8/fa/c648308fe711ee1f88192cad6026ab4f925396d1293e8356de7e55be89b5/pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7d32706badfe136888bdea71c0def994644e09fff0bfe47441deaed8e96fdbc6", size = 1968877 },
855
- { url = "https://files.pythonhosted.org/packages/16/16/b805c74b35607d24d37103007f899abc4880923b04929547ae68d478b7f4/pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f", size = 2116814 },
856
- { url = "https://files.pythonhosted.org/packages/d1/58/5305e723d9fcdf1c5a655e6a4cc2a07128bf644ff4b1d98daf7a9dbf57da/pydantic_core-2.23.4-cp313-none-win32.whl", hash = "sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769", size = 1738360 },
857
- { url = "https://files.pythonhosted.org/packages/a5/ae/e14b0ff8b3f48e02394d8acd911376b7b66e164535687ef7dc24ea03072f/pydantic_core-2.23.4-cp313-none-win_amd64.whl", hash = "sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5", size = 1919411 },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
858
  ]
859
 
860
  [[package]]
@@ -887,6 +1182,15 @@ wheels = [
887
  { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 },
888
  ]
889
 
 
 
 
 
 
 
 
 
 
890
  [[package]]
891
  name = "python-multipart"
892
  version = "0.0.20"
@@ -968,6 +1272,18 @@ wheels = [
968
  { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229 },
969
  ]
970
 
 
 
 
 
 
 
 
 
 
 
 
 
971
  [[package]]
972
  name = "ruff"
973
  version = "0.11.10"
@@ -1099,7 +1415,7 @@ wheels = [
1099
 
1100
  [[package]]
1101
  name = "typer"
1102
- version = "0.12.5"
1103
  source = { registry = "https://pypi.org/simple" }
1104
  dependencies = [
1105
  { name = "click" },
@@ -1107,9 +1423,9 @@ dependencies = [
1107
  { name = "shellingham" },
1108
  { name = "typing-extensions" },
1109
  ]
1110
- sdist = { url = "https://files.pythonhosted.org/packages/c5/58/a79003b91ac2c6890fc5d90145c662fd5771c6f11447f116b63300436bc9/typer-0.12.5.tar.gz", hash = "sha256:f592f089bedcc8ec1b974125d64851029c3b1af145f04aca64d69410f0c9b722", size = 98953 }
1111
  wheels = [
1112
- { url = "https://files.pythonhosted.org/packages/a8/2b/886d13e742e514f704c33c4caa7df0f3b89e5a25ef8db02aa9ca3d9535d5/typer-0.12.5-py3-none-any.whl", hash = "sha256:62fe4e471711b147e3365034133904df3e235698399bc4de2b36c8579298d52b", size = 47288 },
1113
  ]
1114
 
1115
  [[package]]
@@ -1121,6 +1437,18 @@ wheels = [
1121
  { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806 },
1122
  ]
1123
 
 
 
 
 
 
 
 
 
 
 
 
 
1124
  [[package]]
1125
  name = "tzdata"
1126
  version = "2025.2"
 
88
  { url = "https://files.pythonhosted.org/packages/5d/35/be73b6015511aa0173ec595fc579133b797ad532996f2998fd6b8d1bbe6b/audioop_lts-0.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:78bfb3703388c780edf900be66e07de5a3d4105ca8e8720c5c4d67927e0b15d0", size = 23918 },
89
  ]
90
 
91
+ [[package]]
92
+ name = "biopython"
93
+ version = "1.85"
94
+ source = { registry = "https://pypi.org/simple" }
95
+ dependencies = [
96
+ { name = "numpy" },
97
+ ]
98
+ sdist = { url = "https://files.pythonhosted.org/packages/db/ca/1d5fab0fedaf5c2f376d9746d447cdce04241c433602c3861693361ce54c/biopython-1.85.tar.gz", hash = "sha256:5dafab74059de4e78f49f6b5684eddae6e7ce46f09cfa059c1d1339e8b1ea0a6", size = 19909902 }
99
+ wheels = [
100
+ { url = "https://files.pythonhosted.org/packages/c3/73/c3a1323a3fe0d07212b09c04fb903e2cbb98aebfbb58e55e8717473e1bc0/biopython-1.85-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:db8822adab0cd75a6e6ae845acf312addd8eab5f9b731c191454b961fc2c2cdc", size = 2787585 },
101
+ { url = "https://files.pythonhosted.org/packages/ff/cf/299524e896fa49beb7588143e1509cce4848572215ebafb8eea83a861820/biopython-1.85-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8e2bbe58cc1a592b239ef6d68396745d3fbfaafc668ce38283871d8ff070dbab", size = 2765608 },
102
+ { url = "https://files.pythonhosted.org/packages/b1/dd/be3e95b72a35ee6d52c84412e15af828951e5c69175080d4619985fd54ce/biopython-1.85-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5916eb56df7ecd4a3babc07a48d4894c40cfb45dc18ccda1c148d0131017ce04", size = 3237552 },
103
+ { url = "https://files.pythonhosted.org/packages/25/9c/612821b946930b6caa5d795cfe4169ed6a522562eced9776914be7efaf21/biopython-1.85-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0cec8833bf3036049129944ee5382dd576dac9670c3814ff2314b52aa94f199", size = 3257287 },
104
+ { url = "https://files.pythonhosted.org/packages/a1/77/316e51dd42fd8225429574a268bdc627ce4f42067a3976c4c8c457a42023/biopython-1.85-cp311-cp311-win32.whl", hash = "sha256:cf88a4c8d8af13138be115949639a5e4a201618185a72ff09adbe175b7946b28", size = 2786411 },
105
+ { url = "https://files.pythonhosted.org/packages/f2/11/3c4e8c049b91998bbbd51ddebc6f790b1aa66211babfbf5ff008a72fb1f9/biopython-1.85-cp311-cp311-win_amd64.whl", hash = "sha256:d3c99db65d57ae4fc5034e42ac6cd8ddce069e664903f04c8a4f684d7609d6fa", size = 2820912 },
106
+ { url = "https://files.pythonhosted.org/packages/a3/25/e46f05359df7f0049c3adc5eaeb9aee0f5fbde1d959d05c78eb1de8f4d12/biopython-1.85-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cc5b981b9e3060db7c355b6145dfe3ce0b6572e1601b31211f6d742b10543874", size = 2789327 },
107
+ { url = "https://files.pythonhosted.org/packages/54/5b/8b3b029c94c63ab4c1781d141615b4a837e658422381d460c5573d5d8262/biopython-1.85-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6fe47d704c2d3afac99aeb461219ec5f00273120d2d99835dc0a9a617f520141", size = 2765805 },
108
+ { url = "https://files.pythonhosted.org/packages/69/0a/9a8a38eff03c4607b9cec8d0e08c76b346b1cee1f77bc6d00efebfc7ec83/biopython-1.85-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e54e495239e623660ad367498c2f7a1a294b1997ba603f2ceafb36fd18f0eba6", size = 3249276 },
109
+ { url = "https://files.pythonhosted.org/packages/b4/0d/b7a0f10f5100dcf51ae36ba31490169bfa45617323bd82af43e1fb0098fb/biopython-1.85-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d024ad48997ad53d53a77da24b072aaba8a550bd816af8f2e7e606a9918a3b43", size = 3268869 },
110
+ { url = "https://files.pythonhosted.org/packages/07/51/646a4b7bdb4c1153786a70d33588ed09178bfcdda0542dfdc976294f4312/biopython-1.85-cp312-cp312-win32.whl", hash = "sha256:6985e17a2911defcbd30275a12f5ed5de2765e4bc91a60439740d572fdbfdf43", size = 2787011 },
111
+ { url = "https://files.pythonhosted.org/packages/c1/84/c583fa2ac6e7d392d24ebdc5c99e95e517507de22cf143efb6cf1fc93ff5/biopython-1.85-cp312-cp312-win_amd64.whl", hash = "sha256:d6f8efb2db03f2ec115c5e8c764dbadf635e0c9ecd4c0e91fc8216c1b62f85f5", size = 2820972 },
112
+ { url = "https://files.pythonhosted.org/packages/c3/3f/65814bf221f0bfdd2633830e573ac8594794686f54110571dce98cc32fd3/biopython-1.85-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bae6f17262f20e9587d419ba5683e9dc93a31ee1858b98fa0cff203694d5b786", size = 2789844 },
113
+ { url = "https://files.pythonhosted.org/packages/be/61/1443ce34226e261c20ae4a154b2bab72c109cf31415c92c54c1aada36b00/biopython-1.85-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b1e4918e6399ab0183dd863527fec18b53b7c61b6f0ef95db84b4adfa430ce75", size = 2765767 },
114
+ { url = "https://files.pythonhosted.org/packages/53/51/bec4c763c704e2715691bb087cfab5907804a1bbef5873588698cece1a30/biopython-1.85-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:86e487b6fe0f20a2b0138cb53f3d4dc26a7e4060ac4cb6fb1d19e194d445ef46", size = 3248867 },
115
+ { url = "https://files.pythonhosted.org/packages/9e/a4/552f20253a7c95988067c4955831bd17dd9b864fd5c215d15c2f63f2d415/biopython-1.85-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:327184048b5a50634ae0970119bcb8a35b76d7cefb2658a01f772915f2fb7686", size = 3268479 },
116
+ { url = "https://files.pythonhosted.org/packages/97/f4/6dfc6ef3e0997f792f93893551d4a9bf7f6052a70d34f4e1b1e5e4a359f6/biopython-1.85-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66b08905fb1b3f5194f908aaf04bbad474c4e3eaebad6d9f889a04e64dd1faf4", size = 3192338 },
117
+ { url = "https://files.pythonhosted.org/packages/9d/8c/73da6588fab3a7d734118f21ac619c0a949f51d3cf7b1b8343d175c33460/biopython-1.85-cp313-cp313-win32.whl", hash = "sha256:5a236ab1e2797c7dcf1577d80fdaafabead2908bc338eaed0aa1509dab769fef", size = 2787017 },
118
+ { url = "https://files.pythonhosted.org/packages/60/ff/fe8f03ac0ccc7219e0959010750c6ac1a5b1411c91c7f4ec3a37d8313673/biopython-1.85-cp313-cp313-win_amd64.whl", hash = "sha256:1b61593765e9ebdb71d73307d55fd4b46eb976608d329ae6803c084d90ed34c7", size = 2821012 },
119
+ ]
120
+
121
+ [[package]]
122
+ name = "cachetools"
123
+ version = "5.5.2"
124
+ source = { registry = "https://pypi.org/simple" }
125
+ sdist = { url = "https://files.pythonhosted.org/packages/6c/81/3747dad6b14fa2cf53fcf10548cf5aea6913e96fab41a3c198676f8948a5/cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4", size = 28380 }
126
+ wheels = [
127
+ { url = "https://files.pythonhosted.org/packages/72/76/20fa66124dbe6be5cafeb312ece67de6b61dd91a0247d1ea13db4ebb33c2/cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a", size = 10080 },
128
+ ]
129
+
130
  [[package]]
131
  name = "certifi"
132
  version = "2025.4.26"
 
196
  { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 },
197
  ]
198
 
199
+ [[package]]
200
+ name = "cloudpathlib"
201
+ version = "0.19.0"
202
+ source = { registry = "https://pypi.org/simple" }
203
+ sdist = { url = "https://files.pythonhosted.org/packages/10/c9/90cb80cf70669ed9a978ef46d8a8b39bad8ca768c4ce4ae086f0d05aeeb7/cloudpathlib-0.19.0.tar.gz", hash = "sha256:919edbfd9a4e935d2423da210b143df89cb0ec6d378366a0dffa2e9fd0664fe8", size = 41875 }
204
+ wheels = [
205
+ { url = "https://files.pythonhosted.org/packages/4d/4e/f83794cb311019c385d061d9b7a9dc444c7023c5523c3f4161191221429c/cloudpathlib-0.19.0-py3-none-any.whl", hash = "sha256:eb7758648812d5906af44f14cf9a6a64f687342a6f547a1c20deb7241d769dcb", size = 49433 },
206
+ ]
207
+
208
+ [package.optional-dependencies]
209
+ gs = [
210
+ { name = "google-cloud-storage" },
211
+ ]
212
+
213
  [[package]]
214
  name = "colorama"
215
  version = "0.4.6"
 
270
  ]
271
 
272
  [[package]]
273
+ name = "folding-studio"
274
+ version = "3.3.1"
275
+ source = { directory = "folding-studio" }
276
+ dependencies = [
277
+ { name = "folding-studio-data-models" },
278
+ { name = "pydantic" },
279
+ { name = "python-dotenv" },
280
+ { name = "pyyaml" },
281
+ { name = "requests" },
282
+ { name = "tqdm" },
283
+ { name = "typer" },
284
+ ]
285
+
286
+ [package.metadata]
287
+ requires-dist = [
288
+ { name = "folding-studio-data-models", path = "folding-studio/offline-packages/folding_studio_data_models-0.13.3-py3-none-any.whl" },
289
+ { name = "pydantic", specifier = ">=2.11.4,<3.0.0" },
290
+ { name = "python-dotenv", specifier = ">=1.0.1,<2.0.0" },
291
+ { name = "pyyaml", specifier = ">=6.0.2,<7.0.0" },
292
+ { name = "requests", specifier = ">=2.32.3,<3.0.0" },
293
+ { name = "tqdm", specifier = ">=4.67.0,<5.0.0" },
294
+ { name = "typer", specifier = ">=0.15.4,<0.16.0" },
295
+ ]
296
+
297
+ [[package]]
298
+ name = "folding-studio-data-models"
299
+ version = "0.13.3"
300
+ source = { path = "folding-studio/offline-packages/folding_studio_data_models-0.13.3-py3-none-any.whl" }
301
+ dependencies = [
302
+ { name = "biopython" },
303
+ { name = "cloudpathlib", extra = ["gs"] },
304
+ { name = "pydantic" },
305
+ { name = "typing-extensions" },
306
+ ]
307
+ wheels = [
308
+ { filename = "folding_studio_data_models-0.13.3-py3-none-any.whl", hash = "sha256:2dcf684bd47c255566f90631499bd1f5755077e74dbe18c98ecc32c77e9b1390" },
309
+ ]
310
+
311
+ [package.metadata]
312
+ requires-dist = [
313
+ { name = "biopython", specifier = ">=1.81,<2.0" },
314
+ { name = "cloudpathlib", extras = ["gs"], specifier = ">=0.19.0,<0.20.0" },
315
+ { name = "pydantic", specifier = ">=2.9.2,<3.0.0" },
316
+ { name = "typing-extensions", specifier = ">=4.12.2,<5.0.0" },
317
+ ]
318
+
319
+ [[package]]
320
+ name = "folding-studio-demo"
321
  version = "0.1.0"
322
+ source = { editable = "." }
323
  dependencies = [
324
+ { name = "biopython" },
325
+ { name = "folding-studio" },
326
  { name = "gradio" },
327
+ { name = "gradio-molecule3d" },
328
  { name = "ipython" },
329
  { name = "numpy" },
330
  { name = "plotly" },
 
332
 
333
  [package.metadata]
334
  requires-dist = [
335
+ { name = "biopython", specifier = ">=1.85" },
336
+ { name = "folding-studio", directory = "folding-studio" },
337
  { name = "gradio", specifier = "==5.30.0" },
338
+ { name = "gradio-molecule3d", specifier = ">=0.0.7" },
339
  { name = "ipython", specifier = ">=9.2.0" },
340
  { name = "numpy", specifier = ">=2.2.6" },
341
  { name = "plotly", specifier = ">=6.1.1" },
 
350
  { url = "https://files.pythonhosted.org/packages/2c/a9/a7022f58e081149ec0184c31ea81dcee605e1d46380b48122e1ef94ac24e/fsspec-2025.5.0-py3-none-any.whl", hash = "sha256:0ca253eca6b5333d8a2b8bd98c7326fe821f1f0fdbd34e1b445bddde8e804c95", size = 196164 },
351
  ]
352
 
353
+ [[package]]
354
+ name = "google-api-core"
355
+ version = "2.24.2"
356
+ source = { registry = "https://pypi.org/simple" }
357
+ dependencies = [
358
+ { name = "google-auth" },
359
+ { name = "googleapis-common-protos" },
360
+ { name = "proto-plus" },
361
+ { name = "protobuf" },
362
+ { name = "requests" },
363
+ ]
364
+ sdist = { url = "https://files.pythonhosted.org/packages/09/5c/085bcb872556934bb119e5e09de54daa07873f6866b8f0303c49e72287f7/google_api_core-2.24.2.tar.gz", hash = "sha256:81718493daf06d96d6bc76a91c23874dbf2fac0adbbf542831b805ee6e974696", size = 163516 }
365
+ wheels = [
366
+ { url = "https://files.pythonhosted.org/packages/46/95/f472d85adab6e538da2025dfca9e976a0d125cc0af2301f190e77b76e51c/google_api_core-2.24.2-py3-none-any.whl", hash = "sha256:810a63ac95f3c441b7c0e43d344e372887f62ce9071ba972eacf32672e072de9", size = 160061 },
367
+ ]
368
+
369
+ [[package]]
370
+ name = "google-auth"
371
+ version = "2.40.2"
372
+ source = { registry = "https://pypi.org/simple" }
373
+ dependencies = [
374
+ { name = "cachetools" },
375
+ { name = "pyasn1-modules" },
376
+ { name = "rsa" },
377
+ ]
378
+ sdist = { url = "https://files.pythonhosted.org/packages/66/84/f67f53c505a6b2c5da05c988e2a5483f5ba9eee4b1841d2e3ff22f547cd5/google_auth-2.40.2.tar.gz", hash = "sha256:a33cde547a2134273226fa4b853883559947ebe9207521f7afc707efbf690f58", size = 280990 }
379
+ wheels = [
380
+ { url = "https://files.pythonhosted.org/packages/6a/c7/e2d82e6702e2a9e2311c138f8e1100f21d08aed0231290872b229ae57a86/google_auth-2.40.2-py2.py3-none-any.whl", hash = "sha256:f7e568d42eedfded58734f6a60c58321896a621f7c116c411550a4b4a13da90b", size = 216102 },
381
+ ]
382
+
383
+ [[package]]
384
+ name = "google-cloud-core"
385
+ version = "2.4.3"
386
+ source = { registry = "https://pypi.org/simple" }
387
+ dependencies = [
388
+ { name = "google-api-core" },
389
+ { name = "google-auth" },
390
+ ]
391
+ sdist = { url = "https://files.pythonhosted.org/packages/d6/b8/2b53838d2acd6ec6168fd284a990c76695e84c65deee79c9f3a4276f6b4f/google_cloud_core-2.4.3.tar.gz", hash = "sha256:1fab62d7102844b278fe6dead3af32408b1df3eb06f5c7e8634cbd40edc4da53", size = 35861 }
392
+ wheels = [
393
+ { url = "https://files.pythonhosted.org/packages/40/86/bda7241a8da2d28a754aad2ba0f6776e35b67e37c36ae0c45d49370f1014/google_cloud_core-2.4.3-py2.py3-none-any.whl", hash = "sha256:5130f9f4c14b4fafdff75c79448f9495cfade0d8775facf1b09c3bf67e027f6e", size = 29348 },
394
+ ]
395
+
396
+ [[package]]
397
+ name = "google-cloud-storage"
398
+ version = "3.1.0"
399
+ source = { registry = "https://pypi.org/simple" }
400
+ dependencies = [
401
+ { name = "google-api-core" },
402
+ { name = "google-auth" },
403
+ { name = "google-cloud-core" },
404
+ { name = "google-crc32c" },
405
+ { name = "google-resumable-media" },
406
+ { name = "requests" },
407
+ ]
408
+ sdist = { url = "https://files.pythonhosted.org/packages/f3/08/52143124415a889bbab60a8ecede1e31ea0e8d992ca078317886f26dc3be/google_cloud_storage-3.1.0.tar.gz", hash = "sha256:944273179897c7c8a07ee15f2e6466a02da0c7c4b9ecceac2a26017cb2972049", size = 7666527 }
409
+ wheels = [
410
+ { url = "https://files.pythonhosted.org/packages/13/b8/c99c965659f45efa73080477c49ffddf7b9aecb00806be8422560bb5b824/google_cloud_storage-3.1.0-py2.py3-none-any.whl", hash = "sha256:eaf36966b68660a9633f03b067e4a10ce09f1377cae3ff9f2c699f69a81c66c6", size = 174861 },
411
+ ]
412
+
413
+ [[package]]
414
+ name = "google-crc32c"
415
+ version = "1.7.1"
416
+ source = { registry = "https://pypi.org/simple" }
417
+ sdist = { url = "https://files.pythonhosted.org/packages/19/ae/87802e6d9f9d69adfaedfcfd599266bf386a54d0be058b532d04c794f76d/google_crc32c-1.7.1.tar.gz", hash = "sha256:2bff2305f98846f3e825dbeec9ee406f89da7962accdb29356e4eadc251bd472", size = 14495 }
418
+ wheels = [
419
+ { url = "https://files.pythonhosted.org/packages/f7/94/220139ea87822b6fdfdab4fb9ba81b3fff7ea2c82e2af34adc726085bffc/google_crc32c-1.7.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:6fbab4b935989e2c3610371963ba1b86afb09537fd0c633049be82afe153ac06", size = 30468 },
420
+ { url = "https://files.pythonhosted.org/packages/94/97/789b23bdeeb9d15dc2904660463ad539d0318286d7633fe2760c10ed0c1c/google_crc32c-1.7.1-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:ed66cbe1ed9cbaaad9392b5259b3eba4a9e565420d734e6238813c428c3336c9", size = 30313 },
421
+ { url = "https://files.pythonhosted.org/packages/81/b8/976a2b843610c211e7ccb3e248996a61e87dbb2c09b1499847e295080aec/google_crc32c-1.7.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee6547b657621b6cbed3562ea7826c3e11cab01cd33b74e1f677690652883e77", size = 33048 },
422
+ { url = "https://files.pythonhosted.org/packages/c9/16/a3842c2cf591093b111d4a5e2bfb478ac6692d02f1b386d2a33283a19dc9/google_crc32c-1.7.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d68e17bad8f7dd9a49181a1f5a8f4b251c6dbc8cc96fb79f1d321dfd57d66f53", size = 32669 },
423
+ { url = "https://files.pythonhosted.org/packages/04/17/ed9aba495916fcf5fe4ecb2267ceb851fc5f273c4e4625ae453350cfd564/google_crc32c-1.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:6335de12921f06e1f774d0dd1fbea6bf610abe0887a1638f64d694013138be5d", size = 33476 },
424
+ { url = "https://files.pythonhosted.org/packages/dd/b7/787e2453cf8639c94b3d06c9d61f512234a82e1d12d13d18584bd3049904/google_crc32c-1.7.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:2d73a68a653c57281401871dd4aeebbb6af3191dcac751a76ce430df4d403194", size = 30470 },
425
+ { url = "https://files.pythonhosted.org/packages/ed/b4/6042c2b0cbac3ec3a69bb4c49b28d2f517b7a0f4a0232603c42c58e22b44/google_crc32c-1.7.1-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:22beacf83baaf59f9d3ab2bbb4db0fb018da8e5aebdce07ef9f09fce8220285e", size = 30315 },
426
+ { url = "https://files.pythonhosted.org/packages/29/ad/01e7a61a5d059bc57b702d9ff6a18b2585ad97f720bd0a0dbe215df1ab0e/google_crc32c-1.7.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19eafa0e4af11b0a4eb3974483d55d2d77ad1911e6cf6f832e1574f6781fd337", size = 33180 },
427
+ { url = "https://files.pythonhosted.org/packages/3b/a5/7279055cf004561894ed3a7bfdf5bf90a53f28fadd01af7cd166e88ddf16/google_crc32c-1.7.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6d86616faaea68101195c6bdc40c494e4d76f41e07a37ffdef270879c15fb65", size = 32794 },
428
+ { url = "https://files.pythonhosted.org/packages/0f/d6/77060dbd140c624e42ae3ece3df53b9d811000729a5c821b9fd671ceaac6/google_crc32c-1.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:b7491bdc0c7564fcf48c0179d2048ab2f7c7ba36b84ccd3a3e1c3f7a72d3bba6", size = 33477 },
429
+ { url = "https://files.pythonhosted.org/packages/8b/72/b8d785e9184ba6297a8620c8a37cf6e39b81a8ca01bb0796d7cbb28b3386/google_crc32c-1.7.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:df8b38bdaf1629d62d51be8bdd04888f37c451564c2042d36e5812da9eff3c35", size = 30467 },
430
+ { url = "https://files.pythonhosted.org/packages/34/25/5f18076968212067c4e8ea95bf3b69669f9fc698476e5f5eb97d5b37999f/google_crc32c-1.7.1-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:e42e20a83a29aa2709a0cf271c7f8aefaa23b7ab52e53b322585297bb94d4638", size = 30309 },
431
+ { url = "https://files.pythonhosted.org/packages/92/83/9228fe65bf70e93e419f38bdf6c5ca5083fc6d32886ee79b450ceefd1dbd/google_crc32c-1.7.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:905a385140bf492ac300026717af339790921f411c0dfd9aa5a9e69a08ed32eb", size = 33133 },
432
+ { url = "https://files.pythonhosted.org/packages/c3/ca/1ea2fd13ff9f8955b85e7956872fdb7050c4ace8a2306a6d177edb9cf7fe/google_crc32c-1.7.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b211ddaf20f7ebeec5c333448582c224a7c90a9d98826fbab82c0ddc11348e6", size = 32773 },
433
+ { url = "https://files.pythonhosted.org/packages/89/32/a22a281806e3ef21b72db16f948cad22ec68e4bdd384139291e00ff82fe2/google_crc32c-1.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:0f99eaa09a9a7e642a61e06742856eec8b19fc0037832e03f941fe7cf0c8e4db", size = 33475 },
434
+ { url = "https://files.pythonhosted.org/packages/b8/c5/002975aff514e57fc084ba155697a049b3f9b52225ec3bc0f542871dd524/google_crc32c-1.7.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32d1da0d74ec5634a05f53ef7df18fc646666a25efaaca9fc7dcfd4caf1d98c3", size = 33243 },
435
+ { url = "https://files.pythonhosted.org/packages/61/cb/c585282a03a0cea70fcaa1bf55d5d702d0f2351094d663ec3be1c6c67c52/google_crc32c-1.7.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e10554d4abc5238823112c2ad7e4560f96c7bf3820b202660373d769d9e6e4c9", size = 32870 },
436
+ { url = "https://files.pythonhosted.org/packages/16/1b/1693372bf423ada422f80fd88260dbfd140754adb15cbc4d7e9a68b1cb8e/google_crc32c-1.7.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85fef7fae11494e747c9fd1359a527e5970fc9603c90764843caabd3a16a0a48", size = 28241 },
437
+ { url = "https://files.pythonhosted.org/packages/fd/3c/2a19a60a473de48717b4efb19398c3f914795b64a96cf3fbe82588044f78/google_crc32c-1.7.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6efb97eb4369d52593ad6f75e7e10d053cf00c48983f7a973105bc70b0ac4d82", size = 28048 },
438
+ ]
439
+
440
+ [[package]]
441
+ name = "google-resumable-media"
442
+ version = "2.7.2"
443
+ source = { registry = "https://pypi.org/simple" }
444
+ dependencies = [
445
+ { name = "google-crc32c" },
446
+ ]
447
+ sdist = { url = "https://files.pythonhosted.org/packages/58/5a/0efdc02665dca14e0837b62c8a1a93132c264bd02054a15abb2218afe0ae/google_resumable_media-2.7.2.tar.gz", hash = "sha256:5280aed4629f2b60b847b0d42f9857fd4935c11af266744df33d8074cae92fe0", size = 2163099 }
448
+ wheels = [
449
+ { url = "https://files.pythonhosted.org/packages/82/35/b8d3baf8c46695858cb9d8835a53baa1eeb9906ddaf2f728a5f5b640fd1e/google_resumable_media-2.7.2-py2.py3-none-any.whl", hash = "sha256:3ce7551e9fe6d99e9a126101d2536612bb73486721951e9562fee0f90c6ababa", size = 81251 },
450
+ ]
451
+
452
+ [[package]]
453
+ name = "googleapis-common-protos"
454
+ version = "1.70.0"
455
+ source = { registry = "https://pypi.org/simple" }
456
+ dependencies = [
457
+ { name = "protobuf" },
458
+ ]
459
+ sdist = { url = "https://files.pythonhosted.org/packages/39/24/33db22342cf4a2ea27c9955e6713140fedd51e8b141b5ce5260897020f1a/googleapis_common_protos-1.70.0.tar.gz", hash = "sha256:0e1b44e0ea153e6594f9f394fef15193a68aaaea2d843f83e2742717ca753257", size = 145903 }
460
+ wheels = [
461
+ { url = "https://files.pythonhosted.org/packages/86/f1/62a193f0227cf15a920390abe675f386dec35f7ae3ffe6da582d3ade42c7/googleapis_common_protos-1.70.0-py3-none-any.whl", hash = "sha256:b8bfcca8c25a2bb253e0e0b0adaf8c00773e5e6af6fd92397576680b807e0fd8", size = 294530 },
462
+ ]
463
+
464
  [[package]]
465
  name = "gradio"
466
  version = "5.30.0"
 
518
  { url = "https://files.pythonhosted.org/packages/55/6f/03eb8e0e0ec80eced5ed35a63376dabfc7391b1538502f8e85e9dc5bab02/gradio_client-1.10.1-py3-none-any.whl", hash = "sha256:fcff53f6aad3dfa9dd082adedb94256172d6b20666b1ef66480d82023e1907db", size = 323141 },
519
  ]
520
 
521
+ [[package]]
522
+ name = "gradio-molecule3d"
523
+ version = "0.0.7"
524
+ source = { registry = "https://pypi.org/simple" }
525
+ dependencies = [
526
+ { name = "gradio" },
527
+ ]
528
+ sdist = { url = "https://files.pythonhosted.org/packages/4a/01/7ee3f6909385210fda7129bc3330545537ec814cc3b6e101bdb8704bf5ab/gradio_molecule3d-0.0.7.tar.gz", hash = "sha256:ab3a99e806c012c02c5a081085d442944b61fefc1ad952a0ff3844dcffc7dc42", size = 1468341 }
529
+ wheels = [
530
+ { url = "https://files.pythonhosted.org/packages/ad/04/5883e077a8ff08b353de4be28bd8517aeaba14ad80ccb4d8d9ab7fe4291d/gradio_molecule3d-0.0.7-py3-none-any.whl", hash = "sha256:e40721d68c085f66c5594bf4b758ed31a32b0745f0f3d21d0c4adc248d1a7ed0", size = 1397339 },
531
+ ]
532
+
533
  [[package]]
534
  name = "groovy"
535
  version = "0.1.2"
 
1007
  { url = "https://files.pythonhosted.org/packages/ce/4f/5249960887b1fbe561d9ff265496d170b55a735b76724f10ef19f9e40716/prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07", size = 387810 },
1008
  ]
1009
 
1010
+ [[package]]
1011
+ name = "proto-plus"
1012
+ version = "1.26.1"
1013
+ source = { registry = "https://pypi.org/simple" }
1014
+ dependencies = [
1015
+ { name = "protobuf" },
1016
+ ]
1017
+ sdist = { url = "https://files.pythonhosted.org/packages/f4/ac/87285f15f7cce6d4a008f33f1757fb5a13611ea8914eb58c3d0d26243468/proto_plus-1.26.1.tar.gz", hash = "sha256:21a515a4c4c0088a773899e23c7bbade3d18f9c66c73edd4c7ee3816bc96a012", size = 56142 }
1018
+ wheels = [
1019
+ { url = "https://files.pythonhosted.org/packages/4e/6d/280c4c2ce28b1593a19ad5239c8b826871fc6ec275c21afc8e1820108039/proto_plus-1.26.1-py3-none-any.whl", hash = "sha256:13285478c2dcf2abb829db158e1047e2f1e8d63a077d94263c2b88b043c75a66", size = 50163 },
1020
+ ]
1021
+
1022
+ [[package]]
1023
+ name = "protobuf"
1024
+ version = "6.31.0"
1025
+ source = { registry = "https://pypi.org/simple" }
1026
+ sdist = { url = "https://files.pythonhosted.org/packages/13/48/718c1e104a2e89970a8ff3b06d87e152834b576c570a6908f8c17ba88d65/protobuf-6.31.0.tar.gz", hash = "sha256:314fab1a6a316469dc2dd46f993cbbe95c861ea6807da910becfe7475bc26ffe", size = 441644 }
1027
+ wheels = [
1028
+ { url = "https://files.pythonhosted.org/packages/b6/77/8671682038b08237c927215fa3296bc1c54e4086fe542c87017c1b626663/protobuf-6.31.0-cp310-abi3-win32.whl", hash = "sha256:10bd62802dfa0588649740a59354090eaf54b8322f772fbdcca19bc78d27f0d6", size = 423437 },
1029
+ { url = "https://files.pythonhosted.org/packages/e4/07/cc9b0cbf7593f6ef8cf87fa9b0e55cd74c5cb526dd89ad84aa7d6547ef8d/protobuf-6.31.0-cp310-abi3-win_amd64.whl", hash = "sha256:3e987c99fd634be8347246a02123250f394ba20573c953de133dc8b2c107dd71", size = 435118 },
1030
+ { url = "https://files.pythonhosted.org/packages/21/46/33f884aa8bc59114dc97e0d954ca4618c556483670236008c88fbb7e834f/protobuf-6.31.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:2c812f0f96ceb6b514448cefeb1df54ec06dde456783f5099c0e2f8a0f2caa89", size = 425439 },
1031
+ { url = "https://files.pythonhosted.org/packages/9b/f2/9a676b50229ce37b12777d7b21de90ae7bc0f9505d07e72e2e8d47b8d165/protobuf-6.31.0-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:67ce50195e4e584275623b8e6bc6d3d3dfd93924bf6116b86b3b8975ab9e4571", size = 321950 },
1032
+ { url = "https://files.pythonhosted.org/packages/a1/a7/243fa2d3c1b7675d54744b32dacf30356f4c27c0d3ad940ca8745a1c6b2c/protobuf-6.31.0-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:5353e38844168a327acd2b2aa440044411cd8d1b6774d5701008bd1dba067c79", size = 320904 },
1033
+ { url = "https://files.pythonhosted.org/packages/ee/01/1ed1d482960a5718fd99c82f6d79120181947cfd4667ec3944d448ed44a3/protobuf-6.31.0-py3-none-any.whl", hash = "sha256:6ac2e82556e822c17a8d23aa1190bbc1d06efb9c261981da95c71c9da09e9e23", size = 168558 },
1034
+ ]
1035
+
1036
  [[package]]
1037
  name = "ptyprocess"
1038
  version = "0.7.0"
 
1051
  { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842 },
1052
  ]
1053
 
1054
+ [[package]]
1055
+ name = "pyasn1"
1056
+ version = "0.6.1"
1057
+ source = { registry = "https://pypi.org/simple" }
1058
+ sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322 }
1059
+ wheels = [
1060
+ { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135 },
1061
+ ]
1062
+
1063
+ [[package]]
1064
+ name = "pyasn1-modules"
1065
+ version = "0.4.2"
1066
+ source = { registry = "https://pypi.org/simple" }
1067
+ dependencies = [
1068
+ { name = "pyasn1" },
1069
+ ]
1070
+ sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892 }
1071
+ wheels = [
1072
+ { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259 },
1073
+ ]
1074
+
1075
  [[package]]
1076
  name = "pydantic"
1077
+ version = "2.11.5"
1078
  source = { registry = "https://pypi.org/simple" }
1079
  dependencies = [
1080
  { name = "annotated-types" },
1081
  { name = "pydantic-core" },
1082
  { name = "typing-extensions" },
1083
+ { name = "typing-inspection" },
1084
  ]
1085
+ sdist = { url = "https://files.pythonhosted.org/packages/f0/86/8ce9040065e8f924d642c58e4a344e33163a07f6b57f836d0d734e0ad3fb/pydantic-2.11.5.tar.gz", hash = "sha256:7f853db3d0ce78ce8bbb148c401c2cdd6431b3473c0cdff2755c7690952a7b7a", size = 787102 }
1086
  wheels = [
1087
+ { url = "https://files.pythonhosted.org/packages/b5/69/831ed22b38ff9b4b64b66569f0e5b7b97cf3638346eb95a2147fdb49ad5f/pydantic-2.11.5-py3-none-any.whl", hash = "sha256:f9c26ba06f9747749ca1e5c94d6a85cb84254577553c8785576fd38fa64dc0f7", size = 444229 },
1088
  ]
1089
 
1090
  [[package]]
1091
  name = "pydantic-core"
1092
+ version = "2.33.2"
1093
  source = { registry = "https://pypi.org/simple" }
1094
  dependencies = [
1095
  { name = "typing-extensions" },
1096
  ]
1097
+ sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195 }
1098
+ wheels = [
1099
+ { url = "https://files.pythonhosted.org/packages/3f/8d/71db63483d518cbbf290261a1fc2839d17ff89fce7089e08cad07ccfce67/pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7", size = 2028584 },
1100
+ { url = "https://files.pythonhosted.org/packages/24/2f/3cfa7244ae292dd850989f328722d2aef313f74ffc471184dc509e1e4e5a/pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246", size = 1855071 },
1101
+ { url = "https://files.pythonhosted.org/packages/b3/d3/4ae42d33f5e3f50dd467761304be2fa0a9417fbf09735bc2cce003480f2a/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f", size = 1897823 },
1102
+ { url = "https://files.pythonhosted.org/packages/f4/f3/aa5976e8352b7695ff808599794b1fba2a9ae2ee954a3426855935799488/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc", size = 1983792 },
1103
+ { url = "https://files.pythonhosted.org/packages/d5/7a/cda9b5a23c552037717f2b2a5257e9b2bfe45e687386df9591eff7b46d28/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de", size = 2136338 },
1104
+ { url = "https://files.pythonhosted.org/packages/2b/9f/b8f9ec8dd1417eb9da784e91e1667d58a2a4a7b7b34cf4af765ef663a7e5/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a", size = 2730998 },
1105
+ { url = "https://files.pythonhosted.org/packages/47/bc/cd720e078576bdb8255d5032c5d63ee5c0bf4b7173dd955185a1d658c456/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef", size = 2003200 },
1106
+ { url = "https://files.pythonhosted.org/packages/ca/22/3602b895ee2cd29d11a2b349372446ae9727c32e78a94b3d588a40fdf187/pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e", size = 2113890 },
1107
+ { url = "https://files.pythonhosted.org/packages/ff/e6/e3c5908c03cf00d629eb38393a98fccc38ee0ce8ecce32f69fc7d7b558a7/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d", size = 2073359 },
1108
+ { url = "https://files.pythonhosted.org/packages/12/e7/6a36a07c59ebefc8777d1ffdaf5ae71b06b21952582e4b07eba88a421c79/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30", size = 2245883 },
1109
+ { url = "https://files.pythonhosted.org/packages/16/3f/59b3187aaa6cc0c1e6616e8045b284de2b6a87b027cce2ffcea073adf1d2/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf", size = 2241074 },
1110
+ { url = "https://files.pythonhosted.org/packages/e0/ed/55532bb88f674d5d8f67ab121a2a13c385df382de2a1677f30ad385f7438/pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51", size = 1910538 },
1111
+ { url = "https://files.pythonhosted.org/packages/fe/1b/25b7cccd4519c0b23c2dd636ad39d381abf113085ce4f7bec2b0dc755eb1/pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab", size = 1952909 },
1112
+ { url = "https://files.pythonhosted.org/packages/49/a9/d809358e49126438055884c4366a1f6227f0f84f635a9014e2deb9b9de54/pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65", size = 1897786 },
1113
+ { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000 },
1114
+ { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996 },
1115
+ { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957 },
1116
+ { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199 },
1117
+ { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296 },
1118
+ { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109 },
1119
+ { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028 },
1120
+ { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044 },
1121
+ { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881 },
1122
+ { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034 },
1123
+ { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187 },
1124
+ { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628 },
1125
+ { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866 },
1126
+ { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894 },
1127
+ { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688 },
1128
+ { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808 },
1129
+ { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580 },
1130
+ { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859 },
1131
+ { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810 },
1132
+ { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498 },
1133
+ { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611 },
1134
+ { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924 },
1135
+ { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196 },
1136
+ { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389 },
1137
+ { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223 },
1138
+ { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473 },
1139
+ { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269 },
1140
+ { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921 },
1141
+ { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162 },
1142
+ { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560 },
1143
+ { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777 },
1144
+ { url = "https://files.pythonhosted.org/packages/7b/27/d4ae6487d73948d6f20dddcd94be4ea43e74349b56eba82e9bdee2d7494c/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8", size = 2025200 },
1145
+ { url = "https://files.pythonhosted.org/packages/f1/b8/b3cb95375f05d33801024079b9392a5ab45267a63400bf1866e7ce0f0de4/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593", size = 1859123 },
1146
+ { url = "https://files.pythonhosted.org/packages/05/bc/0d0b5adeda59a261cd30a1235a445bf55c7e46ae44aea28f7bd6ed46e091/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612", size = 1892852 },
1147
+ { url = "https://files.pythonhosted.org/packages/3e/11/d37bdebbda2e449cb3f519f6ce950927b56d62f0b84fd9cb9e372a26a3d5/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7", size = 2067484 },
1148
+ { url = "https://files.pythonhosted.org/packages/8c/55/1f95f0a05ce72ecb02a8a8a1c3be0579bbc29b1d5ab68f1378b7bebc5057/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e", size = 2108896 },
1149
+ { url = "https://files.pythonhosted.org/packages/53/89/2b2de6c81fa131f423246a9109d7b2a375e83968ad0800d6e57d0574629b/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8", size = 2069475 },
1150
+ { url = "https://files.pythonhosted.org/packages/b8/e9/1f7efbe20d0b2b10f6718944b5d8ece9152390904f29a78e68d4e7961159/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf", size = 2239013 },
1151
+ { url = "https://files.pythonhosted.org/packages/3c/b2/5309c905a93811524a49b4e031e9851a6b00ff0fb668794472ea7746b448/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb", size = 2238715 },
1152
+ { url = "https://files.pythonhosted.org/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", size = 2066757 },
1153
  ]
1154
 
1155
  [[package]]
 
1182
  { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 },
1183
  ]
1184
 
1185
+ [[package]]
1186
+ name = "python-dotenv"
1187
+ version = "1.1.0"
1188
+ source = { registry = "https://pypi.org/simple" }
1189
+ sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920 }
1190
+ wheels = [
1191
+ { url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256 },
1192
+ ]
1193
+
1194
  [[package]]
1195
  name = "python-multipart"
1196
  version = "0.0.20"
 
1272
  { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229 },
1273
  ]
1274
 
1275
+ [[package]]
1276
+ name = "rsa"
1277
+ version = "4.9.1"
1278
+ source = { registry = "https://pypi.org/simple" }
1279
+ dependencies = [
1280
+ { name = "pyasn1" },
1281
+ ]
1282
+ sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034 }
1283
+ wheels = [
1284
+ { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696 },
1285
+ ]
1286
+
1287
  [[package]]
1288
  name = "ruff"
1289
  version = "0.11.10"
 
1415
 
1416
  [[package]]
1417
  name = "typer"
1418
+ version = "0.15.4"
1419
  source = { registry = "https://pypi.org/simple" }
1420
  dependencies = [
1421
  { name = "click" },
 
1423
  { name = "shellingham" },
1424
  { name = "typing-extensions" },
1425
  ]
1426
+ sdist = { url = "https://files.pythonhosted.org/packages/6c/89/c527e6c848739be8ceb5c44eb8208c52ea3515c6cf6406aa61932887bf58/typer-0.15.4.tar.gz", hash = "sha256:89507b104f9b6a0730354f27c39fae5b63ccd0c95b1ce1f1a6ba0cfd329997c3", size = 101559 }
1427
  wheels = [
1428
+ { url = "https://files.pythonhosted.org/packages/c9/62/d4ba7afe2096d5659ec3db8b15d8665bdcb92a3c6ff0b95e99895b335a9c/typer-0.15.4-py3-none-any.whl", hash = "sha256:eb0651654dcdea706780c466cf06d8f174405a659ffff8f163cfbfee98c0e173", size = 45258 },
1429
  ]
1430
 
1431
  [[package]]
 
1437
  { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806 },
1438
  ]
1439
 
1440
+ [[package]]
1441
+ name = "typing-inspection"
1442
+ version = "0.4.1"
1443
+ source = { registry = "https://pypi.org/simple" }
1444
+ dependencies = [
1445
+ { name = "typing-extensions" },
1446
+ ]
1447
+ sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726 }
1448
+ wheels = [
1449
+ { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552 },
1450
+ ]
1451
+
1452
  [[package]]
1453
  name = "tzdata"
1454
  version = "2025.2"