rowan-python 0.0.5__py3-none-any.whl → 1.0.0__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 CHANGED
@@ -1,4 +1,10 @@
1
1
  # ruff: noqa
2
2
 
3
3
  from . import utils
4
- from .client import Client
4
+ from .client import compute
5
+
6
+ from . import constants
7
+
8
+ from .folder import Folder
9
+ from .workflow import Workflow
10
+ from .calculation import Calculation
rowan/calculation.py ADDED
@@ -0,0 +1,11 @@
1
+ import stjames
2
+
3
+ from .utils import api_client
4
+
5
+ class Calculation:
6
+ @classmethod
7
+ def retrieve(cls, uuid: stjames.UUID) -> dict:
8
+ with api_client() as client:
9
+ response = client.get(f"/calculation/{uuid}/stjames")
10
+ response.raise_for_status()
11
+ return response.json()
rowan/client.py CHANGED
@@ -1,155 +1,50 @@
1
- from __future__ import annotations
2
-
3
1
  import cctk
4
- import httpx
5
- from typing import Optional
6
2
  import stjames
7
- from dataclasses import dataclass, field
8
3
  import time
4
+ from typing import Optional
9
5
 
10
- import rowan
11
-
12
- API_URL = "https://api.rowansci.com"
13
-
14
-
15
- @dataclass
16
- class Client:
17
- blocking: bool = True
18
- print: bool = True
19
- ping_interval: int = 5
20
- delete_when_finished: bool = False
21
-
22
- headers: dict = field(init=False)
23
-
24
- def __post_init__(self):
25
- self.headers = {"X-API-Key": rowan.utils.get_api_key()}
26
-
27
- if not self.blocking and self.delete_when_finished:
28
- print("Warning: ``delete_when_finished`` has no effect when ``blocking`` is False. To delete calculations, call ``Client.delete()`` manually.")
29
-
30
- def compute(
31
- self,
32
- type: Optional[str] = "calculation",
33
- input_mol: Optional[cctk.Molecule] = None,
34
- input_smiles: Optional[str] = None,
35
- name: Optional[str] = None,
36
- folder_uuid: Optional[str] = None,
37
- engine: str = "peregrine",
38
- **options,
39
- ) -> dict | str:
40
- if (input_mol is None) == (input_smiles is None):
41
- raise ValueError("Must specify exactly one of ``input_smiles`` and ``input_mol``!")
42
-
43
- if input_mol is None:
44
- input_mol = cctk.Molecule.new_from_smiles(input_smiles)
45
-
46
- molecule = rowan.utils.cctk_to_stjames(input_mol)
47
-
48
- with httpx.Client() as client:
49
- if type == "calculation":
50
- settings = stjames.Settings(**options)
51
- calc = stjames.Calculation(molecules=[molecule], name=name, engine=engine, settings=settings)
52
-
53
- response = client.post(
54
- f"{API_URL}/calculation",
55
- headers=self.headers,
56
- json={
57
- "json_data": calc.model_dump(mode="json"),
58
- "folder_uuid": folder_uuid,
59
- },
60
- )
61
-
62
- else:
63
- response = client.post(
64
- f"{API_URL}/workflow",
65
- headers=self.headers,
66
- json={
67
- "initial_molecule": molecule.model_dump(mode="json"),
68
- "workflow_type": type,
69
- "name": name,
70
- "folder_uuid": folder_uuid,
71
- "workflow_data": options,
72
- },
73
- )
74
-
75
- response.raise_for_status()
76
- response_dict = response.json()
77
- calc_uuid = response_dict["uuid"]
78
-
79
- if self.blocking:
80
- while not self.is_finished(calc_uuid, type):
81
- time.sleep(self.ping_interval)
82
- result = self.get(calc_uuid, type)
83
-
84
- if self.delete_when_finished:
85
- self.delete(calc_uuid, type)
86
-
87
- return result
88
-
89
- else:
90
- return calc_uuid
91
-
92
- def is_finished(self, calc_uuid: str, type: str = "calculation") -> bool:
93
- with httpx.Client() as client:
94
- if type == "calculation":
95
- response = client.get(f"{API_URL}/calculation/{calc_uuid}", headers=self.headers)
96
- response.raise_for_status()
97
- response_dict = response.json()
98
- status = response_dict["status"]
99
-
100
- else:
101
- response = client.get(f"{API_URL}/workflow/{calc_uuid}", headers=self.headers)
102
- response.raise_for_status()
103
- response_dict = response.json()
104
- status = response_dict["object_status"]
105
-
106
- return status in [2, 3, 4]
107
-
108
- def get(self, calc_uuid: str, type: str = "calculation") -> dict:
109
- with httpx.Client() as client:
110
- if type == "calculation":
111
- stj_response = client.get(f"{API_URL}/calculation/{calc_uuid}/stjames", headers=self.headers)
112
- stj_response.raise_for_status()
113
- stj_dict = stj_response.json()
114
-
115
- response = client.get(f"{API_URL}/calculation/{calc_uuid}", headers=self.headers)
116
- response.raise_for_status()
117
- response_dict = response.json()
118
-
119
- # reformat
120
- del response_dict["settings"]
121
- response_dict["data"] = stj_dict
122
-
123
- return response_dict
124
-
125
- else:
126
- response = client.get(f"{API_URL}/workflow/{calc_uuid}", headers=self.headers)
127
- response.raise_for_status()
128
- response_dict = response.json()
129
-
130
- # reformat
131
- response_dict["data"] = response_dict["object_data"]
132
- del response_dict["object_data"]
133
- del response_dict["status"]
134
-
135
- return response_dict
136
-
137
- def stop(self, calc_uuid: str, type: str = "calculation") -> None:
138
- with httpx.Client() as client:
139
- if type == "calculation":
140
- response = client.post(f"{API_URL}/calculation/{calc_uuid}/stop", headers=self.headers)
141
- response.raise_for_status()
142
-
143
- else:
144
- response = client.post(f"{API_URL}/workflow/{calc_uuid}/stop", headers=self.headers)
145
- response.raise_for_status()
146
-
147
- def delete(self, calc_uuid: str, type: str = "calculation") -> None:
148
- with httpx.Client() as client:
149
- if type == "calculation":
150
- response = client.delete(f"{API_URL}/calculation/{calc_uuid}", headers=self.headers)
151
- response.raise_for_status()
152
-
153
- else:
154
- response = client.delete(f"{API_URL}/folder/{calc_uuid}", headers=self.headers)
155
- response.raise_for_status()
6
+ from .utils import cctk_to_stjames, smiles_to_stjames
7
+ from .workflow import Workflow
8
+
9
+ """ A high-level interface to submitting a calculation. """
10
+
11
+
12
+ def compute(
13
+ molecule: str | cctk.Molecule | stjames.Molecule,
14
+ workflow_type: str,
15
+ name: str = "",
16
+ folder_uuid: Optional[stjames.UUID] = None,
17
+ blocking: bool = True,
18
+ ping_interval: int = 5,
19
+ **workflow_data,
20
+ ) -> dict:
21
+ """High-level function to compute and return workflows."""
22
+
23
+ if isinstance(molecule, str):
24
+ stjmol = smiles_to_stjames(molecule)
25
+ elif isinstance(molecule, cctk.Molecule):
26
+ stjmol = cctk_to_stjames(molecule)
27
+ elif isinstance(molecule, stjames.Molecule):
28
+ stjmol = molecule
29
+ else:
30
+ raise ValueError("Invalid type for `molecule`!")
31
+
32
+ result = Workflow.submit(
33
+ initial_molecule=stjmol,
34
+ workflow_type=workflow_type,
35
+ name=name,
36
+ folder_uuid=folder_uuid,
37
+ workflow_data=workflow_data,
38
+ )
39
+
40
+ if blocking:
41
+ uuid = result["uuid"]
42
+
43
+ while not Workflow.is_finished(uuid):
44
+ time.sleep(ping_interval)
45
+
46
+ completed_result = Workflow.retrieve(uuid)
47
+ return completed_result
48
+
49
+ else:
50
+ return result
rowan/constants.py ADDED
@@ -0,0 +1 @@
1
+ API_URL = "https://api.rowansci.com"
rowan/folder.py ADDED
@@ -0,0 +1,97 @@
1
+ import stjames
2
+ from typing import Optional
3
+
4
+ from .utils import api_client
5
+
6
+
7
+ class Folder:
8
+ @classmethod
9
+ def create(
10
+ cls,
11
+ name: str,
12
+ parent_uuid: Optional[stjames.UUID] = None,
13
+ notes: str = "",
14
+ starred: bool = False,
15
+ public: bool = False,
16
+ ) -> dict:
17
+ with api_client() as client:
18
+ response = client.post(
19
+ "/folder",
20
+ json={
21
+ "name": name,
22
+ "parent_uuid": parent_uuid,
23
+ "notes": notes,
24
+ "starred": starred,
25
+ "public": public,
26
+ },
27
+ )
28
+ response.raise_for_status()
29
+ return response.json()
30
+
31
+ @classmethod
32
+ def retrieve(cls, uuid: stjames.UUID) -> dict:
33
+ with api_client() as client:
34
+ response = client.get(f"/folder/{uuid}")
35
+ response.raise_for_status()
36
+ return response.json()
37
+
38
+ @classmethod
39
+ def update(
40
+ cls,
41
+ uuid: stjames.UUID,
42
+ name: Optional[str] = None,
43
+ parent_uuid: Optional[stjames.UUID] = None,
44
+ notes: Optional[str] = None,
45
+ starred: Optional[bool] = None,
46
+ public: Optional[bool] = None,
47
+ ) -> None:
48
+ old_data = cls.retrieve(uuid)
49
+
50
+ new_data = {}
51
+ new_data["name"] = name if name is not None else old_data["name"]
52
+ new_data["parent_uuid"] = (
53
+ parent_uuid if parent_uuid is not None else old_data["parent_uuid"]
54
+ )
55
+ new_data["notes"] = notes if notes is not None else old_data["notes"]
56
+ new_data["starred"] = starred if starred is not None else old_data["starred"]
57
+ new_data["public"] = public if public is not None else old_data["public"]
58
+
59
+ with api_client() as client:
60
+ response = client.post(f"/folder/{uuid}", json=new_data)
61
+ response.raise_for_status()
62
+ return response.json()
63
+
64
+ @classmethod
65
+ def delete(cls, uuid: stjames.UUID) -> None:
66
+ with api_client() as client:
67
+ response = client.delete(f"/folder/{uuid}")
68
+ response.raise_for_status()
69
+
70
+ @classmethod
71
+ def list(
72
+ cls,
73
+ parent_uuid: Optional[stjames.UUID] = None,
74
+ name_contains: Optional[str] = None,
75
+ public: Optional[bool] = None,
76
+ starred: Optional[bool] = None,
77
+ page: int = 0,
78
+ size: int = 10,
79
+ ):
80
+ params = {"page": page, "size": size}
81
+
82
+ if parent_uuid is not None:
83
+ params["parent_uuid"] = parent_uuid
84
+
85
+ if name_contains is not None:
86
+ params["name_contains"] = name_contains
87
+
88
+ if public is not None:
89
+ params["public"] = public
90
+
91
+ if starred is not None:
92
+ params["starred"] = starred
93
+
94
+ with api_client() as client:
95
+ response = client.get("/folder", params=params)
96
+ response.raise_for_status()
97
+ return response.json()
rowan/utils.py CHANGED
@@ -2,9 +2,13 @@ import os
2
2
  import cctk
3
3
  import stjames
4
4
  import numpy as np
5
+ from contextlib import contextmanager
6
+ import httpx
5
7
 
6
8
  import rowan
7
9
 
10
+ from .constants import API_URL
11
+
8
12
 
9
13
  def get_api_key() -> str:
10
14
  api_key = os.environ.get("ROWAN_API_KEY")
@@ -24,10 +28,27 @@ def cctk_to_stjames(molecule: cctk.Molecule) -> stjames.Molecule:
24
28
 
25
29
  atoms = list()
26
30
  for i in range(molecule.num_atoms()):
27
- atoms.append(stjames.Atom(atomic_number=atomic_numbers[i], position=geometry[i]))
31
+ atoms.append(
32
+ stjames.Atom(atomic_number=atomic_numbers[i], position=geometry[i])
33
+ )
28
34
 
29
35
  return stjames.Molecule(
30
36
  atoms=atoms,
31
37
  charge=molecule.charge,
32
38
  multiplicity=molecule.multiplicity,
33
39
  )
40
+
41
+
42
+ def smiles_to_stjames(smiles: str) -> stjames.Molecule:
43
+ cmol = cctk.Molecule.new_from_smiles(smiles)
44
+ return cctk_to_stjames(cmol)
45
+
46
+
47
+ @contextmanager
48
+ def api_client():
49
+ """Wraps `httpx.Client` with Rowan-specific kwargs."""
50
+ with httpx.Client(
51
+ base_url=API_URL,
52
+ headers={"X-API-Key": get_api_key()},
53
+ ) as client:
54
+ yield client
rowan/workflow.py ADDED
@@ -0,0 +1,139 @@
1
+ import stjames
2
+ from typing import Optional
3
+
4
+
5
+ from .utils import api_client
6
+
7
+
8
+ class Workflow:
9
+ @classmethod
10
+ def submit(
11
+ cls,
12
+ workflow_type: str,
13
+ initial_molecule: dict | stjames.Molecule,
14
+ workflow_data: dict,
15
+ name: Optional[str] = None,
16
+ folder_uuid: Optional[stjames.UUID] = None,
17
+ ) -> dict:
18
+ if isinstance(initial_molecule, stjames.Molecule):
19
+ molecule_dict = initial_molecule.model_dump()
20
+ elif isinstance(initial_molecule, dict):
21
+ molecule_dict = initial_molecule
22
+ else:
23
+ raise ValueError("Invalid type for `initial_molecule`")
24
+
25
+ with api_client() as client:
26
+ response = client.post(
27
+ "/workflow",
28
+ json={
29
+ "name": name,
30
+ "folder_uuid": folder_uuid,
31
+ "initial_molecule": molecule_dict,
32
+ "workflow_type": workflow_type,
33
+ "workflow_data": workflow_data,
34
+ },
35
+ )
36
+ response.raise_for_status()
37
+ return response.json()
38
+
39
+ @classmethod
40
+ def retrieve(cls, uuid: stjames.UUID) -> dict:
41
+ with api_client() as client:
42
+ response = client.get(f"/workflow/{uuid}")
43
+ response.raise_for_status()
44
+ return response.json()
45
+
46
+ @classmethod
47
+ def update(
48
+ cls,
49
+ uuid: stjames.UUID,
50
+ name: Optional[str] = None,
51
+ parent_uuid: Optional[stjames.UUID] = None,
52
+ notes: Optional[str] = None,
53
+ starred: Optional[bool] = None,
54
+ email_when_complete: Optional[bool] = None,
55
+ public: Optional[bool] = None,
56
+ ) -> None:
57
+ old_data = cls.retrieve(uuid)
58
+
59
+ new_data = {}
60
+ new_data["name"] = name if name is not None else old_data["name"]
61
+ new_data["parent_uuid"] = (
62
+ parent_uuid if parent_uuid is not None else old_data["parent_uuid"]
63
+ )
64
+ new_data["notes"] = notes if notes is not None else old_data["notes"]
65
+ new_data["starred"] = starred if starred is not None else old_data["starred"]
66
+ new_data["email_when_complete"] = (
67
+ email_when_complete
68
+ if email_when_complete is not None
69
+ else old_data["email_when_complete"]
70
+ )
71
+ new_data["public"] = public if public is not None else old_data["public"]
72
+
73
+ with api_client() as client:
74
+ response = client.post(f"/workflow/{uuid}", json=new_data)
75
+ response.raise_for_status()
76
+ return response.json()
77
+
78
+ @classmethod
79
+ def status(cls, uuid: stjames.UUID) -> int:
80
+ data = cls.retrieve(uuid)
81
+ return data["object_status"]
82
+
83
+ @classmethod
84
+ def is_finished(cls, uuid: stjames.UUID) -> bool:
85
+ status = cls.status(uuid)
86
+ return status in {
87
+ stjames.Status.COMPLETED_OK.value,
88
+ stjames.Status.FAILED.value,
89
+ stjames.Status.STOPPED.value,
90
+ }
91
+
92
+ @classmethod
93
+ def stop(cls, uuid: stjames.UUID) -> None:
94
+ with api_client() as client:
95
+ response = client.post(f"/workflow/{uuid}/stop")
96
+ response.raise_for_status()
97
+
98
+ @classmethod
99
+ def delete(cls, uuid: stjames.UUID) -> None:
100
+ with api_client() as client:
101
+ response = client.delete(f"/workflow/{uuid}")
102
+ response.raise_for_status()
103
+
104
+ @classmethod
105
+ def list(
106
+ cls,
107
+ parent_uuid: Optional[stjames.UUID] = None,
108
+ name_contains: Optional[str] = None,
109
+ public: Optional[bool] = None,
110
+ starred: Optional[bool] = None,
111
+ object_status: Optional[int] = None,
112
+ object_type: Optional[str] = None,
113
+ page: int = 0,
114
+ size: int = 10,
115
+ ):
116
+ params = {"page": page, "size": size}
117
+
118
+ if parent_uuid is not None:
119
+ params["parent_uuid"] = parent_uuid
120
+
121
+ if name_contains is not None:
122
+ params["name_contains"] = name_contains
123
+
124
+ if public is not None:
125
+ params["public"] = public
126
+
127
+ if starred is not None:
128
+ params["starred"] = starred
129
+
130
+ if object_status is not None:
131
+ params["object_status"] = object_status
132
+
133
+ if object_type is not None:
134
+ params["object_type"] = object_type
135
+
136
+ with api_client() as client:
137
+ response = client.get("/workflow", params=params)
138
+ response.raise_for_status()
139
+ return response.json()
@@ -1,22 +1,19 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: rowan-python
3
- Version: 0.0.5
3
+ Version: 1.0.0
4
4
  Summary: Rowan Python Library
5
- Author-email: Corin Wagen <corin@rowansci.com>
6
5
  Project-URL: Homepage, https://github.com/rowansci/rowan-client
7
6
  Project-URL: Bug Tracker, https://github.com/rowansci/rowan-client/issues
7
+ Author-email: Corin Wagen <corin@rowansci.com>
8
+ License-File: LICENSE
8
9
  Requires-Python: >=3.8
9
10
  Description-Content-Type: text/markdown
10
- License-File: LICENSE
11
- Requires-Dist: cctk >=0.2.18
12
- Requires-Dist: httpx >=0.25
13
- Requires-Dist: numpy >=1.24
14
- Requires-Dist: stjames >=0.0.23
15
11
 
16
12
  # Rowan Python Library
17
13
 
18
14
  [![pypi](https://img.shields.io/pypi/v/rowan-python.svg)](https://pypi.python.org/pypi/rowan-python)
19
- [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v1.json)](https://github.com/charliermarsh/ruff)
15
+ [![pixi](https://img.shields.io/badge/Powered_by-Pixi-facc15)](https://pixi.sh)
16
+ [![ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v1.json)](https://github.com/charliermarsh/ruff)
20
17
 
21
18
  The Rowan Python library provides convenient access to the Rowan API from applications written in the Python language.
22
19
 
@@ -0,0 +1,11 @@
1
+ rowan/__init__.py,sha256=9JmdQXDkbru5WDq4Slt-XWH2OP4888rYyhIoLQfKqGw,183
2
+ rowan/calculation.py,sha256=fdWbVF97bZ2BQOBEBERLmU8DpeFUrwkZttWDSMYqwk8,312
3
+ rowan/client.py,sha256=GQYwqZ3pZv7vH3QvfHKEWqLPklbr1QHbZtjKRpRmiIs,1282
4
+ rowan/constants.py,sha256=ZZvv3L0b2y3dMGlWGeaRmx40J5tBrpxNxvJgjP1TNjg,37
5
+ rowan/folder.py,sha256=W7-YnPxugqzIdw-t1sr-WjeSQa-x4IjZ2mV2DwIq3II,2965
6
+ rowan/utils.py,sha256=IMACnRJpjFns_DF-FZQDu8p8fbgu4C2dbDaxdGcSZQs,1405
7
+ rowan/workflow.py,sha256=An3CW9LlHxYByE4mRl1iYThYcIGry8TwTi5rgbAsEBc,4467
8
+ rowan_python-1.0.0.dist-info/METADATA,sha256=IgayeoNx2nNIrsHExXgGMUW7hJDArdRdK5rkZ4ro_Xs,1030
9
+ rowan_python-1.0.0.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
10
+ rowan_python-1.0.0.dist-info/licenses/LICENSE,sha256=i7ehYBS-6gGmbTcgU4mgk28pyOx2kScJ0kcx8n7bWLM,1084
11
+ rowan_python-1.0.0.dist-info/RECORD,,
@@ -1,5 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.43.0)
2
+ Generator: hatchling 1.25.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
-
@@ -1,8 +0,0 @@
1
- rowan/__init__.py,sha256=0O7Cmo2Io01Uqqx9iL8xcWW0t0YfgwBX7RKxDPXnbVU,61
2
- rowan/client.py,sha256=HoJ7DWxpeHyMdnokKK9M3jTCqEGcwIwHmpa5EFkHg0g,5591
3
- rowan/utils.py,sha256=AVldYWowm7g1Ffs7JhJY7X85goNuFHvMUel0zXguDrk,932
4
- rowan_python-0.0.5.dist-info/LICENSE,sha256=i7ehYBS-6gGmbTcgU4mgk28pyOx2kScJ0kcx8n7bWLM,1084
5
- rowan_python-0.0.5.dist-info/METADATA,sha256=Adimw59X6Bd1-Kp_B9qM-hEWwHtxame5-epgi1MG0ec,1067
6
- rowan_python-0.0.5.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
7
- rowan_python-0.0.5.dist-info/top_level.txt,sha256=0B0BJ1GvTwD5rxpsHctrermP7PVk4SFaQb2syJpGQl8,6
8
- rowan_python-0.0.5.dist-info/RECORD,,
@@ -1 +0,0 @@
1
- rowan