rowan-python 2.1.11__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 ADDED
@@ -0,0 +1,9 @@
1
+ # ruff: noqa
2
+ from . import constants
3
+
4
+ from .folder import *
5
+ from .workflow import *
6
+ from .project import *
7
+ from .protein import *
8
+ from .user import *
9
+ from .utils import *
rowan/constants.py ADDED
@@ -0,0 +1,3 @@
1
+ import os
2
+
3
+ API_URL = os.getenv("ROWAN_API_URL", default="https://api.rowansci.com")
rowan/folder.py ADDED
@@ -0,0 +1,228 @@
1
+ from datetime import datetime
2
+ from typing import Any, Self
3
+
4
+ from pydantic import BaseModel
5
+
6
+ from .utils import api_client
7
+
8
+
9
+ class Folder(BaseModel):
10
+ """
11
+ A class representing a folder in the Rowan API.
12
+
13
+ :ivar uuid: The UUID of the folder.
14
+ :ivar name: The name of the folder.
15
+ :ivar parent_uuid: The UUID of the parent folder.
16
+ :ivar notes: Folder notes.
17
+ :ivar starred: Whether the folder is starred.
18
+ :ivar public: Whether the folder is public.
19
+ :ivar created_at: The date and time the folder was created.
20
+ """
21
+
22
+ uuid: str
23
+ name: str | None = None
24
+ parent_uuid: str | None = None
25
+ notes: str = ""
26
+ starred: bool = False
27
+ public: bool = False
28
+ created_at: datetime | None = None
29
+
30
+ def __repr__(self) -> str:
31
+ return f"<Folder name='{self.name}' created_at='{self.created_at}' uuid='{self.uuid}'>"
32
+
33
+ def fetch_latest(self, in_place: bool = False) -> Self:
34
+ """
35
+ Fetch the latest folder data from the API.
36
+
37
+ This method refreshes the folder object with the latest data from the API.
38
+
39
+ :param in_place: Whether to update the current instance in-place.
40
+ :return: The updated instance (self).
41
+ :raises HTTPError: If the API request fails.
42
+ """
43
+ with api_client() as client:
44
+ response = client.get(f"/folder/{self.uuid}")
45
+ response.raise_for_status()
46
+ data = response.json()
47
+
48
+ if not in_place:
49
+ return self.__class__.model_validate(data)
50
+
51
+ updated_folder = self.model_validate(data)
52
+
53
+ # Update current instance with new data using class-level model_fields
54
+ for field_name in self.__class__.model_fields:
55
+ setattr(self, field_name, getattr(updated_folder, field_name))
56
+
57
+ self.model_rebuild()
58
+
59
+ return self
60
+
61
+
62
+ def update(
63
+ self,
64
+ name: str | None = None,
65
+ parent_uuid: str | None = None,
66
+ notes: str | None = None,
67
+ starred: bool | None = None,
68
+ public: bool | None = None,
69
+ ) -> Self:
70
+ """
71
+ Update a folder.
72
+
73
+ :param name: The new name of the folder.
74
+ :param parent_uuid: The UUID of the new parent folder.
75
+ :param notes: A description of the folder.
76
+ :param starred: Whether the folder is starred.
77
+ :param public: Whether the folder is public.
78
+
79
+ :return: The updated folder object.
80
+ """
81
+ payload = {
82
+ "name": name if name is not None else self.name,
83
+ "parent_uuid": parent_uuid if parent_uuid is not None else self.parent_uuid,
84
+ "notes": notes if notes is not None else self.notes,
85
+ "starred": starred if starred is not None else self.starred,
86
+ "public": public if public is not None else self.public,
87
+ }
88
+
89
+ with api_client() as client:
90
+ response = client.post(f"/folder/{self.uuid}", json=payload)
91
+ response.raise_for_status()
92
+ updated_data = response.json()
93
+
94
+ self.name = updated_data.get("name")
95
+ self.parent_uuid = updated_data.get("parent_uuid")
96
+ self.notes = updated_data.get("notes")
97
+ self.starred = updated_data.get("starred")
98
+ self.public = updated_data.get("public")
99
+ return self
100
+
101
+ def delete(self) -> None:
102
+ """
103
+ Delete the folder and all its contents.
104
+
105
+ This is a destructive action, it will delete all the folders and
106
+ workflows that are inside this folder.
107
+
108
+ :raises requests.HTTPError: if the request to the API fails.
109
+ """
110
+ with api_client() as client:
111
+ response = client.delete(f"/folder/{self.uuid}")
112
+ response.raise_for_status()
113
+
114
+ def print_folder_tree(self, max_depth: int = 10, show_uuids: bool = False) -> None:
115
+ """
116
+ Retrieves a folder tree from the API.
117
+
118
+ :param max_depth: The maximum depth of the folder tree.
119
+ :param show_uuids: Whether to show the UUIDs of the folders.
120
+ :raises HTTPError: If the API request fails.
121
+ """
122
+ print_folder_tree(self.uuid, max_depth, show_uuids)
123
+
124
+ def retrieve_folder(uuid: str) -> Folder:
125
+ """
126
+ Retrieves a folder from the API by UUID. Folder UUID can be found in the folder's URL.
127
+
128
+ :param uuid: The UUID of the folder to retrieve.
129
+ :return: A Folder object representing the retrieved folder.
130
+ :raises HTTPError: If the API request fails.
131
+ """
132
+ with api_client() as client:
133
+ response = client.get(f"/folder/{uuid}")
134
+ response.raise_for_status()
135
+ return Folder(**response.json())
136
+
137
+
138
+ def list_folders(
139
+ parent_uuid: str | None = None,
140
+ name_contains: str | None = None,
141
+ public: bool | None = None,
142
+ starred: bool | None = None,
143
+ page: int = 0,
144
+ size: int = 10,
145
+ ) -> list[Folder]:
146
+ """
147
+ Retrieve a list of folders based on the specified criteria.
148
+
149
+ :param parent_uuid: UUID of the parent folder to filter by.
150
+ :param name_contains: Substring to search for in folder names.
151
+ :param public: Filter folders by their public status.
152
+ :param starred: Filter folders by their starred status.
153
+ :param page: Pagination parameter to specify the page number.
154
+ :param size: Pagination parameter to specify the number of items per page.
155
+ :return: A list of Folder objects that match the search criteria.
156
+ :raises requests.HTTPError: if the request to the API fails.
157
+ """
158
+
159
+ params: dict[str, Any] = {
160
+ "page": page,
161
+ "size": size,
162
+ }
163
+
164
+ if parent_uuid is not None:
165
+ params["parent_uuid"] = parent_uuid
166
+ if name_contains is not None:
167
+ params["name_contains"] = name_contains
168
+ if public is not None:
169
+ params["public"] = public
170
+ if starred is not None:
171
+ params["starred"] = starred
172
+
173
+ with api_client() as client:
174
+ response = client.get("/folder", params=params)
175
+ response.raise_for_status()
176
+ items = response.json()["folders"]
177
+
178
+ return [Folder(**item) for item in items]
179
+
180
+
181
+ def create_folder(
182
+ name: str,
183
+ parent_uuid: str | None = None,
184
+ notes: str = "",
185
+ starred: bool = False,
186
+ public: bool = False,
187
+ ) -> Folder:
188
+ """
189
+ Create a new folder.
190
+
191
+ :param name: The name of the folder.
192
+ :param parent_uuid: The UUID of the parent folder.
193
+ :param notes: A description of the folder.
194
+ :param starred: Whether the folder is starred.
195
+ :param public: Whether the folder is public.
196
+ :return: The newly created folder.
197
+ """
198
+ data = {
199
+ "name": name,
200
+ "parent_uuid": parent_uuid,
201
+ "notes": notes,
202
+ "starred": starred,
203
+ "public": public,
204
+ }
205
+ with api_client() as client:
206
+ response = client.post("/folder", json=data)
207
+ response.raise_for_status()
208
+ folder_data = response.json()
209
+ return Folder(**folder_data)
210
+
211
+ def print_folder_tree(uuid: str, max_depth: int = 10, show_uuids: bool = False) -> None:
212
+ """
213
+ Retrieves a folder tree from the API.
214
+
215
+ :param uuid: The UUID of the root of the folder tree.
216
+ :param max_depth: The maximum depth of the folder tree.
217
+ :param show_uuids: Whether to show the UUIDs of the folders.
218
+ :raises HTTPError: If the API request fails.
219
+ """
220
+ params: dict[str, Any] = {
221
+ "max_depth": max_depth,
222
+ "show_uuids": show_uuids,
223
+ }
224
+ with api_client() as client:
225
+ response = client.get(f"/folder/{uuid}/folder_tree", params=params)
226
+ response.raise_for_status()
227
+ folder_data = response.json()
228
+ print(folder_data)
rowan/project.py ADDED
@@ -0,0 +1,136 @@
1
+ from datetime import datetime
2
+ from typing import Any, Self
3
+
4
+ from pydantic import BaseModel
5
+
6
+ from .utils import api_client
7
+
8
+
9
+ class Project(BaseModel):
10
+ """
11
+ A class representing a project in the Rowan API.
12
+
13
+ :ivar uuid: The UUID of the project.
14
+ :ivar name: The name of the project.
15
+ :ivar created_at: The date and time the project was created.
16
+ """
17
+
18
+ uuid: str
19
+ name: str | None = None
20
+ created_at: datetime | None = None
21
+
22
+ def __repr__(self) -> str:
23
+ return f"<Project name='{self.name}' created_at='{self.created_at}' uuid='{self.uuid}'>"
24
+
25
+ def update(
26
+ self,
27
+ name: str | None = None,
28
+ ) -> Self:
29
+ """
30
+ Update a project.
31
+
32
+ :param name: The new name of the project.
33
+
34
+ :return: The updated project object.
35
+ """
36
+ payload = {
37
+ "name": name if name is not None else self.name,
38
+ }
39
+
40
+ with api_client() as client:
41
+ response = client.post(f"/project/{self.uuid}", json=payload)
42
+ response.raise_for_status()
43
+ updated_data = response.json()
44
+
45
+ self.name = updated_data.get("name")
46
+ return self
47
+
48
+ def delete(self) -> None:
49
+ """
50
+ Delete the project.
51
+
52
+ This is a destructive action, it will delete all the folders and
53
+ workflows that are inside this project.
54
+
55
+ :raises requests.HTTPError: if the request to the API fails.
56
+ """
57
+ with api_client() as client:
58
+ response = client.delete(f"/project/{self.uuid}")
59
+ response.raise_for_status()
60
+
61
+
62
+ def retrieve_project(uuid: str) -> Project:
63
+ """
64
+ Retrieves a project from the API by UUID. Project UUID can be found in the project's URL.
65
+
66
+ :param uuid: The UUID of the project to retrieve.
67
+ :return: A Project object representing the retrieved project.
68
+ :raises HTTPError: If the API request fails.
69
+ """
70
+ with api_client() as client:
71
+ response = client.get(f"/project/{uuid}")
72
+ response.raise_for_status()
73
+ return Project(**response.json())
74
+
75
+
76
+ def list_projects(
77
+ name_contains: str | None = None,
78
+ page: int = 0,
79
+ size: int = 10,
80
+ ) -> list[Project]:
81
+ """
82
+ Retrieve a list of projects based on the specified criteria.
83
+
84
+ :param name_contains: Substring to search for in project names.
85
+ :param page: Pagination parameter to specify the page number.
86
+ :param size: Pagination parameter to specify the number of items per page.
87
+ :return: A list of Folder objects that match the search criteria.
88
+ :raises requests.HTTPError: if the request to the API fails.
89
+ """
90
+
91
+ params: dict[str, Any] = {
92
+ "page": page,
93
+ "size": size,
94
+ }
95
+
96
+ if name_contains is not None:
97
+ params["name_contains"] = name_contains
98
+
99
+ with api_client() as client:
100
+ response = client.get("/project", params=params)
101
+ response.raise_for_status()
102
+ items = response.json()
103
+
104
+ return [Project(**item) for item in items]
105
+
106
+
107
+ def create_project(
108
+ name: str,
109
+ ) -> Project:
110
+ """
111
+ Create a new project.
112
+
113
+ :param name: The name of the project.
114
+ :return: The newly created project.
115
+ """
116
+ data = {
117
+ "name": name,
118
+ }
119
+ with api_client() as client:
120
+ response = client.post("/project", json=data)
121
+ response.raise_for_status()
122
+ project_data = response.json()
123
+ return Project(**project_data)
124
+
125
+
126
+ def default_project() -> Project:
127
+ """
128
+ Retrieves the default project from the API.
129
+
130
+ :return: A Project object representing the default project.
131
+ :raises HTTPError: If the API request fails.
132
+ """
133
+ with api_client() as client:
134
+ response = client.get("/user/me/default_project")
135
+ response.raise_for_status()
136
+ return Project(**response.json())
rowan/protein.py ADDED
@@ -0,0 +1,245 @@
1
+ from datetime import datetime
2
+ from pathlib import Path
3
+ from typing import Any, Self
4
+
5
+ from pydantic import BaseModel
6
+
7
+ from .utils import api_client
8
+
9
+
10
+ class Protein(BaseModel):
11
+ """
12
+ A Rowan protein.
13
+
14
+ Data is not loaded by default to avoid unnecessary downloads that could impact performance.
15
+ Call `load_data()` to fetch and attach the protein data to this `Protein` object.
16
+
17
+ :ivar uuid: The UUID of the protein
18
+ :ivar created_at: The creation date of the protein
19
+ :ivar used_in_workflow: Whether the protein is used in a workflow
20
+ :ivar ancestor_uuid: The UUID of the ancestor protein
21
+ :ivar sanitized: Whether the protein is sanitized
22
+ :ivar name: The name of the protein
23
+ :ivar data: The data of the protein
24
+ :ivar public: Whether the protein is public
25
+ """
26
+
27
+ uuid: str
28
+ created_at: datetime | None = None
29
+ used_in_workflow: bool | None = None
30
+ ancestor_uuid: str | None = None
31
+ sanitized: int | None = None
32
+ name: str | None = None
33
+ data: dict | None = None
34
+ public: bool | None = None
35
+ pocket: list[list[float]] | None = None
36
+
37
+ def __repr__(self):
38
+ return f"<Protein name='{self.name}' created_at='{self.created_at}' uuid='{self.uuid}'>"
39
+
40
+ def refresh(self, in_place: bool = True) -> Self:
41
+ """
42
+ Loads protein data
43
+
44
+ :return: protein with loaded data
45
+ """
46
+ with api_client() as client:
47
+ response = client.get(f"/protein/{self.uuid}")
48
+ response.raise_for_status()
49
+ protein_data = response.json()
50
+ if not in_place:
51
+ return self.__class__.model_validate(protein_data)
52
+
53
+ self.name = protein_data.get("name")
54
+ self.data = protein_data.get("data")
55
+ self.public = protein_data.get("public")
56
+ self.pocket = protein_data.get("pocket")
57
+ self.sanitized = protein_data.get("sanitized")
58
+ self.used_in_workflow = protein_data.get("used_in_workflow")
59
+ return self
60
+
61
+ def update(
62
+ self,
63
+ name: str | None = None,
64
+ data: dict | None = None,
65
+ public: bool | None = None,
66
+ pocket: list[list[float]] | None = None,
67
+ ) -> Self:
68
+ # Use current values unless new ones are passed in
69
+ """
70
+ Updates protein data
71
+
72
+ :param name: The new name of the protein
73
+ :param data: The new data of the protein
74
+ :param public: Whether the protein is public
75
+ :param pocket: The new pocket of the protein
76
+ :return: The updated protein object
77
+ """
78
+ updated_payload = {
79
+ "name": name if name is not None else self.name,
80
+ "data": data if data is not None else self.data,
81
+ "public": public if public is not None else self.public,
82
+ "pocket": pocket if pocket is not None else self.pocket,
83
+ }
84
+
85
+ with api_client() as client:
86
+ response = client.post(f"/protein/{self.uuid}", json=updated_payload)
87
+ response.raise_for_status()
88
+ updated_data = response.json()
89
+
90
+ # Update attributes
91
+ self.name = updated_data.get("name")
92
+ self.data = updated_data.get("data")
93
+ self.public = updated_data.get("public")
94
+ self.pocket = updated_data.get("pocket")
95
+ return self
96
+
97
+ def delete(self) -> None:
98
+ """
99
+ Deletes a protein
100
+
101
+ :raises requests.HTTPError: if the request to the API fails
102
+ """
103
+ with api_client() as client:
104
+ response = client.delete(f"/protein/{self.uuid}")
105
+ response.raise_for_status()
106
+
107
+ def sanitize(self) -> None:
108
+ """
109
+ Sanitizes a protein
110
+
111
+ :raises requests.HTTPError: if the request to the API fails
112
+ """
113
+ with api_client() as client:
114
+ response = client.post(f"/protein/sanitize/{self.uuid}")
115
+ response.raise_for_status()
116
+
117
+ def download_pdb_file(self, path: Path | None = None, name: str | None = None) -> None:
118
+ """
119
+ Downloads the PDB file for a protein
120
+
121
+ :param path: Directory to save the file to (defaults to current directory)
122
+ :param name: Optional custom name for the file (defaults to protein name)
123
+ :raises requests.HTTPError: if the request to the API fails
124
+ """
125
+ if path is None:
126
+ path = Path.cwd()
127
+
128
+ path.mkdir(parents=True, exist_ok=True)
129
+
130
+ with api_client() as client:
131
+ response = client.get(f"/protein/{self.uuid}/get_pdb_file")
132
+ response.raise_for_status()
133
+
134
+ file_path = path / f"{name or self.name}.pdb"
135
+ with open(file_path, "wb") as f:
136
+ f.write(response.content)
137
+
138
+
139
+ def retrieve_protein(uuid: str) -> Protein:
140
+ """
141
+ Retrieves a protein from the API using its UUID.
142
+
143
+ :param uuid: The UUID of the protein to retrieve.
144
+ :return: A Protein object representing the retrieved protein.
145
+ :raises requests.HTTPError: if the request to the API fails.
146
+ """
147
+
148
+ with api_client() as client:
149
+ response = client.get(f"/protein/{uuid}")
150
+ response.raise_for_status()
151
+ protein_data = response.json()
152
+
153
+ return Protein(**protein_data)
154
+
155
+
156
+ def list_proteins(
157
+ ancestor_uuid: str | None = None,
158
+ name_contains: str | None = None,
159
+ page: int = 0,
160
+ size: int = 20,
161
+ ) -> list[Protein]:
162
+ """
163
+ List proteins
164
+
165
+ :param ancestor_uuid: The UUID of the ancestor protein to filter by
166
+ :param name_contains: Substring to search for in protein names
167
+ :param page: The page number to retrieve
168
+ :param size: The number of items per page
169
+ :return: A list of Protein objects that match the search criteria
170
+ :raises requests.HTTPError: if the request to the API fails
171
+ """
172
+ params: dict[str, Any] = {"page": page, "size": size}
173
+ if ancestor_uuid is not None:
174
+ params["ancestor_uuid"] = ancestor_uuid
175
+ if name_contains is not None:
176
+ params["name_contains"] = name_contains
177
+
178
+ with api_client() as client:
179
+ response = client.get("/protein", params=params)
180
+ response.raise_for_status()
181
+ results = response.json()["proteins"]
182
+
183
+ return [Protein(**item) for item in results]
184
+
185
+
186
+ def upload_protein(name: str, file_path: Path, project_uuid: str | None = None) -> Protein:
187
+ """
188
+ Uploads a protein from a PDB file to the API.
189
+
190
+ :param name: The name of the protein to create
191
+ :param file_path: The path to the PDB file to upload
192
+ :return: A Protein object representing the uploaded protein
193
+ :raises requests.HTTPError: if the request to the API fails
194
+ """
195
+ with api_client() as client:
196
+ # Step 1: Read the file and post it to the conversion endpoint.
197
+ conversion_payload = {"name": name, "text": file_path.read_text()}
198
+ conversion_response = client.post("/convert/pdb_file_to_protein", json=conversion_payload)
199
+ conversion_response.raise_for_status() # Ensure the request was successful
200
+
201
+ # Extract the JSON data from the conversion response.
202
+ protein_data = conversion_response.json()
203
+
204
+ # Step 2: Use the converted data to create the final protein object.
205
+ creation_payload = {
206
+ "name": name,
207
+ "protein_data": protein_data,
208
+ "project_uuid": project_uuid,
209
+ }
210
+ final_response = client.post("/protein", json=creation_payload)
211
+ final_response.raise_for_status()
212
+
213
+ # Deserialize the final JSON response into a Protein object and return it.
214
+ return Protein(**final_response.json())
215
+
216
+
217
+ def create_protein_from_pdb_id(name: str, code: str, project_uuid: str | None = None) -> Protein:
218
+ """
219
+ Creates a protein from a PDB ID.
220
+
221
+ :param name: The name of the protein to create
222
+ :param code: The PDB ID of the protein to create
223
+ :param project_uuid: The UUID of the project to create the protein in
224
+ :return: A Protein object representing the created protein
225
+ :raises requests.HTTPError: if the request to the API fails
226
+ """
227
+ with api_client() as client:
228
+ # Step 1: Read the file and post it to the conversion endpoint.
229
+ conversion_response = client.post(f"/convert/pdb_id_to_protein?pdb_id={code}")
230
+ conversion_response.raise_for_status() # Ensure the request was successful
231
+
232
+ # Extract the JSON data from the conversion response.
233
+ protein_data = conversion_response.json()
234
+
235
+ # Step 2: Use the converted data to create the final protein object.
236
+ creation_payload = {
237
+ "name": name,
238
+ "protein_data": protein_data,
239
+ "project_uuid": project_uuid,
240
+ }
241
+ final_response = client.post("/protein", json=creation_payload)
242
+ final_response.raise_for_status()
243
+
244
+ # Deserialize the final JSON response into a Protein object and return it.
245
+ return Protein(**final_response.json())
rowan/py.typed ADDED
File without changes
@@ -0,0 +1,29 @@
1
+ from .chem_utils import (
2
+ batch_charges,
3
+ batch_conformers,
4
+ batch_energy,
5
+ batch_optimize,
6
+ batch_pka,
7
+ batch_tautomers,
8
+ run_charges,
9
+ run_conformers,
10
+ run_energy,
11
+ run_optimize,
12
+ run_pka,
13
+ run_tautomers,
14
+ )
15
+
16
+ __all__ = [
17
+ "batch_charges",
18
+ "batch_conformers",
19
+ "batch_energy",
20
+ "batch_optimize",
21
+ "batch_pka",
22
+ "batch_tautomers",
23
+ "run_charges",
24
+ "run_conformers",
25
+ "run_energy",
26
+ "run_optimize",
27
+ "run_pka",
28
+ "run_tautomers",
29
+ ]