rowan-python 3.0.0__py3-none-any.whl → 3.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rowan/__init__.py +17 -0
- rowan/protein.py +10 -2
- rowan/rowan_rdkit/chem_utils.py +20 -20
- rowan/types.py +1 -1
- rowan/user.py +65 -0
- rowan/workflows/admet.py +5 -2
- rowan/workflows/analogue_docking.py +5 -2
- rowan/workflows/base.py +22 -9
- rowan/workflows/basic_calculation.py +125 -29
- rowan/workflows/batch_docking.py +5 -2
- rowan/workflows/bde.py +5 -2
- rowan/workflows/conformer_search.py +5 -2
- rowan/workflows/descriptors.py +5 -2
- rowan/workflows/docking.py +5 -2
- rowan/workflows/double_ended_ts_search.py +17 -11
- rowan/workflows/electronic_properties.py +5 -2
- rowan/workflows/fukui.py +5 -2
- rowan/workflows/hydrogen_bond_donor_acceptor_strength.py +5 -2
- rowan/workflows/interaction_energy_decomposition.py +6 -3
- rowan/workflows/ion_mobility.py +5 -2
- rowan/workflows/irc.py +6 -3
- rowan/workflows/macropka.py +5 -2
- rowan/workflows/membrane_permeability.py +4 -1
- rowan/workflows/msa.py +5 -2
- rowan/workflows/multistage_optimization.py +6 -3
- rowan/workflows/nmr.py +5 -2
- rowan/workflows/pka.py +5 -2
- rowan/workflows/pose_analysis_md.py +5 -2
- rowan/workflows/protein_binder_design.py +5 -2
- rowan/workflows/protein_cofolding.py +5 -2
- rowan/workflows/protein_md.py +5 -2
- rowan/workflows/rbfe_graph.py +5 -2
- rowan/workflows/redox_potential.py +5 -2
- rowan/workflows/relative_binding_free_energy_perturbation.py +5 -2
- rowan/workflows/scan.py +5 -2
- rowan/workflows/solubility.py +5 -2
- rowan/workflows/solvent_dependent_conformers.py +5 -2
- rowan/workflows/spin_states.py +5 -2
- rowan/workflows/strain.py +5 -2
- rowan/workflows/tautomer_search.py +5 -2
- {rowan_python-3.0.0.dist-info → rowan_python-3.0.1.dist-info}/METADATA +2 -2
- rowan_python-3.0.1.dist-info/RECORD +55 -0
- rowan_python-3.0.0.dist-info/RECORD +0 -55
- {rowan_python-3.0.0.dist-info → rowan_python-3.0.1.dist-info}/WHEEL +0 -0
- {rowan_python-3.0.0.dist-info → rowan_python-3.0.1.dist-info}/licenses/LICENSE +0 -0
rowan/__init__.py
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# ruff: noqa
|
|
2
2
|
from . import constants
|
|
3
|
+
from stjames import (
|
|
4
|
+
Constraint,
|
|
5
|
+
ConstraintType,
|
|
6
|
+
Correction,
|
|
7
|
+
Engine,
|
|
8
|
+
Method,
|
|
9
|
+
Mode,
|
|
10
|
+
OptimizationSettings,
|
|
11
|
+
Settings,
|
|
12
|
+
SolventSettings,
|
|
13
|
+
Task,
|
|
14
|
+
)
|
|
15
|
+
from stjames.optimization.freezing_string_method import (
|
|
16
|
+
FSMInterpolation,
|
|
17
|
+
FSMOptimizationCoordinates,
|
|
18
|
+
FSMSettings,
|
|
19
|
+
)
|
|
3
20
|
|
|
4
21
|
api_key: str | None = None
|
|
5
22
|
project_uuid: str | None = None
|
rowan/protein.py
CHANGED
|
@@ -211,7 +211,7 @@ class Protein(BaseModel):
|
|
|
211
211
|
|
|
212
212
|
raise RuntimeError(f"Protein preparation timed out after {timeout:.0f}s for {self.uuid}.")
|
|
213
213
|
|
|
214
|
-
def validate_protein_forcefield(self) -> None:
|
|
214
|
+
def validate_protein_forcefield(self, exclude_residue_names: list[str] | None = None) -> None:
|
|
215
215
|
"""
|
|
216
216
|
Validate that this protein can be parameterized with the MD forcefield.
|
|
217
217
|
|
|
@@ -219,13 +219,21 @@ class Protein(BaseModel):
|
|
|
219
219
|
recognized by OpenMM and that there are no clashing atoms. Call this
|
|
220
220
|
before submitting any MD workflow to catch preparation issues early.
|
|
221
221
|
|
|
222
|
+
Ligand residues (``LIG``) are always excluded — they are parameterized
|
|
223
|
+
separately by the MD workflow from the provided SMILES.
|
|
224
|
+
|
|
222
225
|
If validation fails, try re-preparing with ``remove_invalid_hydrogens=True``:
|
|
223
226
|
``protein.prepare(remove_invalid_hydrogens=True)``
|
|
224
227
|
|
|
228
|
+
:param exclude_residue_names: Additional residue names to skip during validation.
|
|
225
229
|
:raises requests.HTTPError: if validation fails or the API request fails.
|
|
226
230
|
"""
|
|
231
|
+
excluded = list({"LIG"} | {name.upper() for name in (exclude_residue_names or [])})
|
|
227
232
|
with api_client() as client:
|
|
228
|
-
response = client.post(
|
|
233
|
+
response = client.post(
|
|
234
|
+
f"/protein/{self.uuid}/validate_forcefield",
|
|
235
|
+
json=excluded,
|
|
236
|
+
)
|
|
229
237
|
response.raise_for_status()
|
|
230
238
|
|
|
231
239
|
def download_pdb_file(self, path: Path | str | None = None, name: str | None = None) -> None:
|
rowan/rowan_rdkit/chem_utils.py
CHANGED
|
@@ -98,24 +98,24 @@ def _get_rdkit_mol_from_uuid(calculation_uuid: str) -> RdkitMol:
|
|
|
98
98
|
calc = rowan.retrieve_calculation(calculation_uuid)
|
|
99
99
|
if calc.molecule is None:
|
|
100
100
|
raise ValueError(f"Calculation {calculation_uuid} has no molecules")
|
|
101
|
-
return Chem.MolFromXYZBlock(calc.molecule.to_xyz())
|
|
101
|
+
return Chem.MolFromXYZBlock(calc.molecule.to_xyz()) # type: ignore[attr-defined]
|
|
102
102
|
|
|
103
103
|
|
|
104
104
|
def _embed_rdkit_mol(rdkm: RdkitMol) -> RdkitMol:
|
|
105
105
|
try:
|
|
106
|
-
AllChem.SanitizeMol(rdkm) # type: ignore
|
|
106
|
+
AllChem.SanitizeMol(rdkm) # type: ignore[attr-defined]
|
|
107
107
|
except Exception as e:
|
|
108
108
|
raise ValueError("Molecule could not be generated -- invalid chemistry!") from e
|
|
109
109
|
|
|
110
|
-
rdkm = AllChem.AddHs(rdkm) # type: ignore
|
|
110
|
+
rdkm = AllChem.AddHs(rdkm) # type: ignore[attr-defined]
|
|
111
111
|
try:
|
|
112
|
-
assert AllChem.EmbedMolecule(rdkm, maxAttempts=200) >= 0 # type: ignore
|
|
112
|
+
assert AllChem.EmbedMolecule(rdkm, maxAttempts=200) >= 0 # type: ignore[attr-defined]
|
|
113
113
|
except AssertionError as e:
|
|
114
|
-
status1 = AllChem.EmbedMolecule(rdkm, maxAttempts=200, useRandomCoords=True) # type: ignore
|
|
114
|
+
status1 = AllChem.EmbedMolecule(rdkm, maxAttempts=200, useRandomCoords=True) # type: ignore[attr-defined]
|
|
115
115
|
if status1 < 0:
|
|
116
116
|
raise ValueError("Cannot embed molecule!") from e
|
|
117
117
|
try:
|
|
118
|
-
assert AllChem.MMFFOptimizeMolecule(rdkm, maxIters=200) >= 0 # type: ignore
|
|
118
|
+
assert AllChem.MMFFOptimizeMolecule(rdkm, maxIters=200) >= 0 # type: ignore[attr-defined, call-arg]
|
|
119
119
|
except AssertionError:
|
|
120
120
|
pass
|
|
121
121
|
|
|
@@ -478,9 +478,9 @@ async def _single_energy(
|
|
|
478
478
|
get_api_key()
|
|
479
479
|
method = stjames.Method(method)
|
|
480
480
|
|
|
481
|
-
if mol.GetNumConformers() == 0:
|
|
481
|
+
if mol.GetNumConformers() == 0: # type: ignore[call-arg]
|
|
482
482
|
mol = _embed_rdkit_mol(mol)
|
|
483
|
-
if mol.GetNumConformers() == 0:
|
|
483
|
+
if mol.GetNumConformers() == 0: # type: ignore[call-arg]
|
|
484
484
|
raise NoConformersError("This molecule has no conformers")
|
|
485
485
|
|
|
486
486
|
if method not in FAST_METHODS:
|
|
@@ -490,7 +490,7 @@ async def _single_energy(
|
|
|
490
490
|
|
|
491
491
|
workflow_uuids = []
|
|
492
492
|
for conformer in mol.GetConformers():
|
|
493
|
-
cid = conformer.GetId()
|
|
493
|
+
cid = conformer.GetId() # type: ignore[call-arg]
|
|
494
494
|
stjames_mol = _rdkit_to_stjames(mol, cid)
|
|
495
495
|
post = rowan.submit_workflow(
|
|
496
496
|
name=name,
|
|
@@ -625,9 +625,9 @@ async def _single_optimize(
|
|
|
625
625
|
get_api_key()
|
|
626
626
|
method = stjames.Method(method)
|
|
627
627
|
|
|
628
|
-
if mol.GetNumConformers() == 0:
|
|
628
|
+
if mol.GetNumConformers() == 0: # type: ignore[call-arg]
|
|
629
629
|
mol = _embed_rdkit_mol(mol)
|
|
630
|
-
if mol.GetNumConformers() == 0:
|
|
630
|
+
if mol.GetNumConformers() == 0: # type: ignore[call-arg]
|
|
631
631
|
raise NoConformersError("This molecule has no conformers")
|
|
632
632
|
|
|
633
633
|
if method not in FAST_METHODS:
|
|
@@ -639,7 +639,7 @@ async def _single_optimize(
|
|
|
639
639
|
|
|
640
640
|
workflow_uuids = []
|
|
641
641
|
for conformer in mol.GetConformers():
|
|
642
|
-
cid = conformer.GetId()
|
|
642
|
+
cid = conformer.GetId() # type: ignore[call-arg]
|
|
643
643
|
stjames_mol = _rdkit_to_stjames(mol, cid)
|
|
644
644
|
|
|
645
645
|
post = rowan.submit_workflow(
|
|
@@ -680,7 +680,7 @@ async def _single_optimize(
|
|
|
680
680
|
energies.append(calc.energy)
|
|
681
681
|
|
|
682
682
|
for i, conformer in enumerate(optimized_mol.GetConformers()):
|
|
683
|
-
conformer.SetPositions(np.array(optimized_positions[i]))
|
|
683
|
+
conformer.SetPositions(np.array(optimized_positions[i])) # type: ignore[attr-defined]
|
|
684
684
|
|
|
685
685
|
return {
|
|
686
686
|
"molecule": mol,
|
|
@@ -799,9 +799,9 @@ async def _single_conformers(
|
|
|
799
799
|
get_api_key()
|
|
800
800
|
method = stjames.Method(method)
|
|
801
801
|
|
|
802
|
-
if mol.GetNumConformers() == 0:
|
|
802
|
+
if mol.GetNumConformers() == 0: # type: ignore[call-arg]
|
|
803
803
|
mol = _embed_rdkit_mol(mol)
|
|
804
|
-
if mol.GetNumConformers() == 0:
|
|
804
|
+
if mol.GetNumConformers() == 0: # type: ignore[call-arg]
|
|
805
805
|
raise NoConformersError("This molecule has no conformers")
|
|
806
806
|
|
|
807
807
|
if method not in FAST_METHODS:
|
|
@@ -861,14 +861,14 @@ async def _single_conformers(
|
|
|
861
861
|
lowest_n_uuids = [item[1][0] for item in sorted_data[:num_conformers]]
|
|
862
862
|
lowest_energies = [item[0] for item in sorted_data[:num_conformers]]
|
|
863
863
|
|
|
864
|
-
AllChem.EmbedMultipleConfs(mol, numConfs=num_conformers) # type: ignore
|
|
864
|
+
AllChem.EmbedMultipleConfs(mol, numConfs=num_conformers) # type: ignore[attr-defined]
|
|
865
865
|
|
|
866
866
|
for i, conformer in enumerate(mol.GetConformers()):
|
|
867
867
|
calc = rowan.retrieve_calculation(lowest_n_uuids[i])
|
|
868
868
|
if calc.molecule is None:
|
|
869
869
|
raise ValueError(f"Calculation {lowest_n_uuids[i]} has no molecules")
|
|
870
870
|
pos = [atom.position for atom in calc.molecule.atoms]
|
|
871
|
-
conformer.SetPositions(np.array(pos))
|
|
871
|
+
conformer.SetPositions(np.array(pos)) # type: ignore[attr-defined]
|
|
872
872
|
|
|
873
873
|
return {
|
|
874
874
|
"molecule": mol,
|
|
@@ -964,9 +964,9 @@ async def _single_charges(
|
|
|
964
964
|
get_api_key()
|
|
965
965
|
method = stjames.Method(method)
|
|
966
966
|
|
|
967
|
-
if mol.GetNumConformers() == 0:
|
|
967
|
+
if mol.GetNumConformers() == 0: # type: ignore[call-arg]
|
|
968
968
|
mol = _embed_rdkit_mol(mol)
|
|
969
|
-
if mol.GetNumConformers() == 0:
|
|
969
|
+
if mol.GetNumConformers() == 0: # type: ignore[call-arg]
|
|
970
970
|
raise NoConformersError("This molecule has no conformers")
|
|
971
971
|
|
|
972
972
|
if method not in FAST_METHODS:
|
|
@@ -976,7 +976,7 @@ async def _single_charges(
|
|
|
976
976
|
|
|
977
977
|
workflow_uuids = []
|
|
978
978
|
for conformer in mol.GetConformers():
|
|
979
|
-
cid = conformer.GetId()
|
|
979
|
+
cid = conformer.GetId() # type: ignore[call-arg]
|
|
980
980
|
|
|
981
981
|
post = rowan.submit_workflow(
|
|
982
982
|
name=name,
|
rowan/types.py
CHANGED
|
@@ -9,7 +9,7 @@ from .molecule import Molecule as RowanMolecule
|
|
|
9
9
|
|
|
10
10
|
RdkitMol: TypeAlias = Chem.rdchem.Mol | Chem.rdchem.RWMol
|
|
11
11
|
StJamesMolecule: TypeAlias = stjames.Molecule
|
|
12
|
-
MoleculeInput: TypeAlias = dict[str, Any] | RowanMolecule | StJamesMolecule | RdkitMol
|
|
12
|
+
MoleculeInput: TypeAlias = dict[str, Any] | RowanMolecule | StJamesMolecule | RdkitMol | str
|
|
13
13
|
SolventInput: TypeAlias = stjames.Solvent | str | None
|
|
14
14
|
SMILES: TypeAlias = str
|
|
15
15
|
UUID: TypeAlias = str
|
rowan/user.py
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import hmac
|
|
3
|
+
import time
|
|
4
|
+
|
|
1
5
|
from pydantic import BaseModel
|
|
2
6
|
|
|
3
7
|
from .utils import api_client
|
|
@@ -135,3 +139,64 @@ def whoami() -> User:
|
|
|
135
139
|
response.raise_for_status()
|
|
136
140
|
|
|
137
141
|
return User(**response.json())
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def get_webhook_secret() -> str | None:
|
|
145
|
+
"""Get current webhook secret, or None if not set."""
|
|
146
|
+
with api_client() as client:
|
|
147
|
+
response = client.get("/user/me/webhook_secret")
|
|
148
|
+
response.raise_for_status()
|
|
149
|
+
return response.json().get("webhook_secret")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def create_webhook_secret() -> str:
|
|
153
|
+
"""Create a webhook secret if one doesn't exist, return it."""
|
|
154
|
+
with api_client() as client:
|
|
155
|
+
response = client.post("/user/me/webhook_secret")
|
|
156
|
+
response.raise_for_status()
|
|
157
|
+
return response.json()["webhook_secret"]
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def rotate_webhook_secret() -> str:
|
|
161
|
+
"""Generate a new webhook secret, invalidating the old one."""
|
|
162
|
+
with api_client() as client:
|
|
163
|
+
response = client.post("/user/me/webhook_secret/rotate")
|
|
164
|
+
response.raise_for_status()
|
|
165
|
+
return response.json()["webhook_secret"]
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def verify_webhook_secret(
|
|
169
|
+
raw_body: bytes,
|
|
170
|
+
signature_header: str,
|
|
171
|
+
secret: str,
|
|
172
|
+
max_age_seconds: int = 300,
|
|
173
|
+
) -> bool:
|
|
174
|
+
"""Verify an incoming webhook request from Rowan.
|
|
175
|
+
|
|
176
|
+
:param raw_body: The raw (unparsed) request body bytes.
|
|
177
|
+
:param signature_header: Value of the X-Rowan-Signature header.
|
|
178
|
+
:param secret: Your webhook secret (from :func:`create_webhook_secret` or
|
|
179
|
+
:func:`rotate_webhook_secret`).
|
|
180
|
+
:param max_age_seconds: Reject requests older than this many seconds (default 5 min).
|
|
181
|
+
:returns: True if the signature is valid and the request is fresh.
|
|
182
|
+
"""
|
|
183
|
+
try:
|
|
184
|
+
parts = dict(part.split("=", 1) for part in signature_header.split(","))
|
|
185
|
+
except ValueError:
|
|
186
|
+
return False
|
|
187
|
+
timestamp = parts.get("t", "")
|
|
188
|
+
received_digest = parts.get("sha256", "")
|
|
189
|
+
|
|
190
|
+
if not timestamp or not received_digest:
|
|
191
|
+
return False
|
|
192
|
+
|
|
193
|
+
if abs(time.time() - int(timestamp)) > max_age_seconds:
|
|
194
|
+
return False
|
|
195
|
+
|
|
196
|
+
expected_digest = hmac.new(
|
|
197
|
+
secret.encode(),
|
|
198
|
+
f"{timestamp}.".encode() + raw_body,
|
|
199
|
+
hashlib.sha256,
|
|
200
|
+
).hexdigest()
|
|
201
|
+
|
|
202
|
+
return hmac.compare_digest(expected_digest, received_digest)
|
rowan/workflows/admet.py
CHANGED
|
@@ -31,6 +31,7 @@ def submit_admet_workflow(
|
|
|
31
31
|
folder_uuid: str | None = None,
|
|
32
32
|
folder: Folder | None = None,
|
|
33
33
|
max_credits: int | None = None,
|
|
34
|
+
webhook_url: str | None = None,
|
|
34
35
|
) -> Workflow:
|
|
35
36
|
"""
|
|
36
37
|
Submits an ADMET workflow to predict drug-likeness properties.
|
|
@@ -43,6 +44,7 @@ def submit_admet_workflow(
|
|
|
43
44
|
:param folder_uuid: UUID of the folder to store the workflow in.
|
|
44
45
|
:param folder: Folder object to store the workflow in.
|
|
45
46
|
:param max_credits: Maximum number of credits to use for the workflow.
|
|
47
|
+
:param webhook_url: URL that Rowan will POST to when the workflow completes.
|
|
46
48
|
:returns: Workflow object representing the submitted workflow.
|
|
47
49
|
:raises ValueError: If the molecule has no SMILES associated with it.
|
|
48
50
|
:raises requests.HTTPError: if the request to the API fails.
|
|
@@ -55,12 +57,13 @@ def submit_admet_workflow(
|
|
|
55
57
|
workflow = stjames.ADMETWorkflow(initial_smiles=initial_smiles)
|
|
56
58
|
|
|
57
59
|
data = {
|
|
58
|
-
"name": name,
|
|
59
|
-
"folder_uuid": folder_uuid,
|
|
60
60
|
"workflow_type": "admet",
|
|
61
61
|
"workflow_data": workflow.model_dump(mode="json"),
|
|
62
62
|
"initial_smiles": initial_smiles,
|
|
63
|
+
"name": name,
|
|
64
|
+
"folder_uuid": folder_uuid,
|
|
63
65
|
"max_credits": max_credits,
|
|
66
|
+
"webhook_url": webhook_url,
|
|
64
67
|
}
|
|
65
68
|
|
|
66
69
|
with api_client() as client:
|
|
@@ -166,6 +166,7 @@ def submit_analogue_docking_workflow(
|
|
|
166
166
|
folder_uuid: str | None = None,
|
|
167
167
|
folder: Folder | None = None,
|
|
168
168
|
max_credits: int | None = None,
|
|
169
|
+
webhook_url: str | None = None,
|
|
169
170
|
) -> Workflow:
|
|
170
171
|
"""
|
|
171
172
|
Submits an analogue-docking workflow to the API.
|
|
@@ -180,6 +181,7 @@ def submit_analogue_docking_workflow(
|
|
|
180
181
|
:param folder_uuid: UUID of the folder to place the workflow in.
|
|
181
182
|
:param folder: Folder object to store the workflow in.
|
|
182
183
|
:param max_credits: Maximum number of credits to use for the workflow.
|
|
184
|
+
:param webhook_url: URL that Rowan will POST to when the workflow completes.
|
|
183
185
|
:returns: Workflow object representing the submitted analogue-docking workflow.
|
|
184
186
|
:raises requests.HTTPError: if the request to the API fails.
|
|
185
187
|
"""
|
|
@@ -206,12 +208,13 @@ def submit_analogue_docking_workflow(
|
|
|
206
208
|
)
|
|
207
209
|
|
|
208
210
|
data = {
|
|
209
|
-
"name": name,
|
|
210
|
-
"folder_uuid": folder_uuid,
|
|
211
211
|
"initial_molecule": initial_molecule,
|
|
212
212
|
"workflow_type": "analogue_docking",
|
|
213
213
|
"workflow_data": workflow.model_dump(serialize_as_any=True, mode="json"),
|
|
214
|
+
"name": name,
|
|
215
|
+
"folder_uuid": folder_uuid,
|
|
214
216
|
"max_credits": max_credits,
|
|
217
|
+
"webhook_url": webhook_url,
|
|
215
218
|
}
|
|
216
219
|
|
|
217
220
|
with api_client() as client:
|
rowan/workflows/base.py
CHANGED
|
@@ -10,7 +10,7 @@ from pathlib import Path
|
|
|
10
10
|
from typing import Any, Callable, ClassVar, Self
|
|
11
11
|
|
|
12
12
|
import stjames
|
|
13
|
-
from pydantic import BaseModel, ConfigDict, Field
|
|
13
|
+
from pydantic import BaseModel, ConfigDict, Field, ValidationError
|
|
14
14
|
from rdkit import Chem
|
|
15
15
|
|
|
16
16
|
from ..folder import Folder
|
|
@@ -71,7 +71,7 @@ class WorkflowResult:
|
|
|
71
71
|
workflow_data: dict[str, Any]
|
|
72
72
|
workflow_type: str
|
|
73
73
|
workflow_uuid: str
|
|
74
|
-
|
|
74
|
+
complete: bool = field(default=True, repr=False)
|
|
75
75
|
_workflow: Any = field(default=None, init=False)
|
|
76
76
|
_cache: dict[str, Any] = field(default_factory=dict, init=False)
|
|
77
77
|
|
|
@@ -83,8 +83,11 @@ class WorkflowResult:
|
|
|
83
83
|
def __post_init__(self) -> None:
|
|
84
84
|
"""Parse workflow data into stjames object for typed access."""
|
|
85
85
|
if self._stjames_class is not None:
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
try:
|
|
87
|
+
obj = self._stjames_class.model_validate(self.workflow_data) # type: ignore[attr-defined]
|
|
88
|
+
object.__setattr__(self, "_workflow", obj)
|
|
89
|
+
except ValidationError:
|
|
90
|
+
pass # incomplete or invalid data; _workflow stays None
|
|
88
91
|
|
|
89
92
|
@property
|
|
90
93
|
def data(self) -> dict[str, Any]:
|
|
@@ -115,7 +118,7 @@ def register_result(workflow_type: str) -> Callable[[type[WorkflowResult]], type
|
|
|
115
118
|
|
|
116
119
|
|
|
117
120
|
def create_result(
|
|
118
|
-
workflow_data: dict[str, Any], workflow_type: str, workflow_uuid: str,
|
|
121
|
+
workflow_data: dict[str, Any], workflow_type: str, workflow_uuid: str, complete: bool = True
|
|
119
122
|
) -> WorkflowResult:
|
|
120
123
|
"""
|
|
121
124
|
Factory function to create the appropriate result type for a workflow.
|
|
@@ -123,7 +126,7 @@ def create_result(
|
|
|
123
126
|
:param workflow_data: Raw data dict from the workflow.
|
|
124
127
|
:param workflow_type: Workflow type string.
|
|
125
128
|
:param workflow_uuid: UUID of the parent workflow.
|
|
126
|
-
:param
|
|
129
|
+
:param complete: If True (default), eagerly fetch related data (e.g. calculations)
|
|
127
130
|
in ``__post_init__``. Set to False when polling partial results with
|
|
128
131
|
``result(wait=False)`` to avoid unnecessary API calls.
|
|
129
132
|
:returns: Typed WorkflowResult subclass, or base WorkflowResult if unknown.
|
|
@@ -133,7 +136,7 @@ def create_result(
|
|
|
133
136
|
workflow_data=workflow_data,
|
|
134
137
|
workflow_type=workflow_type,
|
|
135
138
|
workflow_uuid=workflow_uuid,
|
|
136
|
-
|
|
139
|
+
complete=complete,
|
|
137
140
|
)
|
|
138
141
|
|
|
139
142
|
|
|
@@ -158,6 +161,7 @@ class Workflow(BaseModel):
|
|
|
158
161
|
:param data: Data of the workflow.
|
|
159
162
|
:param email_when_complete: Whether to send an email when the workflow completes.
|
|
160
163
|
:param max_credits: Maximum number of credits to use for the workflow.
|
|
164
|
+
:param webhook_url: URL that Rowan will POST to when the workflow completes.
|
|
161
165
|
:param elapsed: Elapsed time of the workflow.
|
|
162
166
|
:param credits_charged: Number of credits charged for the workflow.
|
|
163
167
|
:param logfile: Workflow logfile.
|
|
@@ -323,8 +327,8 @@ Workflow: {self.name}
|
|
|
323
327
|
raise WorkflowError(
|
|
324
328
|
f"Workflow '{self.name}' has no results yet (status={status}, uuid={self.uuid})"
|
|
325
329
|
)
|
|
326
|
-
|
|
327
|
-
return create_result(self.data, self.workflow_type, self.uuid,
|
|
330
|
+
complete = self.status == stjames.Status.COMPLETED_OK
|
|
331
|
+
return create_result(self.data, self.workflow_type, self.uuid, complete=complete)
|
|
328
332
|
|
|
329
333
|
def stream_result(self, poll_interval: int = 5) -> Iterator["WorkflowResult"]:
|
|
330
334
|
"""
|
|
@@ -525,6 +529,8 @@ def molecule_to_dict(mol: MoleculeInput) -> dict[str, Any]:
|
|
|
525
529
|
:returns: Dict representation suitable for API submission.
|
|
526
530
|
"""
|
|
527
531
|
match mol:
|
|
532
|
+
case str():
|
|
533
|
+
return RowanMolecule.from_smiles(mol).to_stjames().model_dump(mode="json")
|
|
528
534
|
case RowanMolecule():
|
|
529
535
|
return mol.to_stjames().model_dump(mode="json")
|
|
530
536
|
case stjames.Molecule():
|
|
@@ -545,6 +551,7 @@ def submit_workflow(
|
|
|
545
551
|
name: str | None = None,
|
|
546
552
|
folder_uuid: str | Folder | None = None,
|
|
547
553
|
max_credits: int | None = None,
|
|
554
|
+
webhook_url: str | None = None,
|
|
548
555
|
) -> Workflow:
|
|
549
556
|
"""
|
|
550
557
|
Submits a workflow to the API.
|
|
@@ -556,6 +563,7 @@ def submit_workflow(
|
|
|
556
563
|
:param name: Name for the workflow.
|
|
557
564
|
:param folder_uuid: UUID of the folder to store the workflow in, or a Folder object.
|
|
558
565
|
:param max_credits: Maximum number of credits to use for the workflow.
|
|
566
|
+
:param webhook_url: URL that Rowan will POST to when the workflow completes.
|
|
559
567
|
:returns: Workflow object representing the submitted workflow.
|
|
560
568
|
:raises ValueError: If neither `initial_smiles` nor a valid `initial_molecule` is provided.
|
|
561
569
|
:raises HTTPError: If the API request fails.
|
|
@@ -573,6 +581,7 @@ def submit_workflow(
|
|
|
573
581
|
"workflow_type": workflow_type,
|
|
574
582
|
"workflow_data": workflow_data,
|
|
575
583
|
"max_credits": max_credits,
|
|
584
|
+
"webhook_url": webhook_url,
|
|
576
585
|
}
|
|
577
586
|
|
|
578
587
|
if initial_smiles is not None:
|
|
@@ -709,6 +718,7 @@ def batch_submit_workflow(
|
|
|
709
718
|
names: list[str] | None = None,
|
|
710
719
|
folder_uuid: str | Folder | None = None,
|
|
711
720
|
max_credits: int | None = None,
|
|
721
|
+
webhook_url: str | None = None,
|
|
712
722
|
) -> list[Workflow]:
|
|
713
723
|
"""
|
|
714
724
|
Submits a batch of workflows to the API.
|
|
@@ -723,6 +733,7 @@ def batch_submit_workflow(
|
|
|
723
733
|
:param names: Names for the submitted workflows.
|
|
724
734
|
:param folder_uuid: UUID of the folder to store the workflows in.
|
|
725
735
|
:param max_credits: Maximum number of credits to use per workflow.
|
|
736
|
+
:param webhook_url: URL to call when each workflow completes.
|
|
726
737
|
:returns: List of Workflow objects representing the submitted workflows.
|
|
727
738
|
"""
|
|
728
739
|
if names is not None:
|
|
@@ -745,6 +756,7 @@ def batch_submit_workflow(
|
|
|
745
756
|
name=name,
|
|
746
757
|
folder_uuid=folder_uuid,
|
|
747
758
|
max_credits=max_credits,
|
|
759
|
+
webhook_url=webhook_url,
|
|
748
760
|
)
|
|
749
761
|
)
|
|
750
762
|
elif initial_molecules is not None:
|
|
@@ -758,6 +770,7 @@ def batch_submit_workflow(
|
|
|
758
770
|
name=name,
|
|
759
771
|
folder_uuid=folder_uuid,
|
|
760
772
|
max_credits=max_credits,
|
|
773
|
+
webhook_url=webhook_url,
|
|
761
774
|
)
|
|
762
775
|
)
|
|
763
776
|
else:
|