rowan-python 1.1.13__py3-none-any.whl → 2.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,12 +1,10 @@
1
1
  # ruff: noqa
2
2
 
3
3
  from . import utils
4
- from .client import compute
5
4
 
6
5
  from . import constants
7
6
 
8
- from .folder import Folder
9
- from .workflow import Workflow
10
- from .calculation import Calculation
11
- from .protein import Protein
12
- from .protein_cofolding import *
7
+ from .folder import *
8
+ from .workflow import *
9
+ from .protein import *
10
+ from .user import *
rowan/folder.py CHANGED
@@ -1,95 +1,212 @@
1
- from typing import Any, Optional
1
+ from datetime import datetime
2
+ from typing import Any, Self
2
3
 
3
- import stjames
4
+ from pydantic import BaseModel
4
5
 
5
6
  from .utils import api_client
6
7
 
7
8
 
8
- class Folder:
9
- @classmethod
10
- def create(
11
- cls,
12
- name: str,
13
- parent_uuid: Optional[stjames.UUID] = None,
14
- notes: str = "",
15
- starred: bool = False,
16
- public: bool = False,
17
- ) -> dict[str, Any]:
18
- data = {
19
- "name": name,
20
- "parent_uuid": parent_uuid,
21
- "notes": notes,
22
- "starred": starred,
23
- "public": public,
24
- }
25
- with api_client() as client:
26
- response = client.post("/folder", json=data)
27
- response.raise_for_status()
28
- return response.json()
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
29
 
30
- @classmethod
31
- def retrieve(cls, uuid: stjames.UUID) -> dict[str, Any]:
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
+ """
32
43
  with api_client() as client:
33
- response = client.get(f"/folder/{uuid}")
44
+ response = client.get(f"/folder/{self.uuid}")
34
45
  response.raise_for_status()
35
- return response.json()
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
36
60
 
37
- @classmethod
38
61
  def update(
39
- cls,
40
- uuid: stjames.UUID,
41
- name: Optional[str] = None,
42
- parent_uuid: Optional[stjames.UUID] = None,
43
- notes: Optional[str] = None,
44
- starred: Optional[bool] = None,
45
- public: Optional[bool] = None,
46
- ) -> None:
47
- old_data = cls.retrieve(uuid)
48
-
49
- new_data = {
50
- "name": name if name is not None else old_data["name"],
51
- "parent_uuid": parent_uuid if parent_uuid is not None else old_data["parent_uuid"],
52
- "notes": notes if notes is not None else old_data["notes"],
53
- "starred": starred if starred is not None else old_data["starred"],
54
- "public": public if public is not None else old_data["public"],
62
+ self,
63
+ name: str | None = None,
64
+ parent_uuid: str | None = None,
65
+ notes: str | None = None,
66
+ starred: bool | None = None,
67
+ public: bool | None = None,
68
+ ) -> Self:
69
+ """
70
+ Update a folder.
71
+
72
+ :param name: The new name of the folder.
73
+ :param parent_uuid: The UUID of the new parent folder.
74
+ :param notes: A description of the folder.
75
+ :param starred: Whether the folder is starred.
76
+ :param public: Whether the folder is public.
77
+
78
+ :return: The updated folder object.
79
+ """
80
+ payload = {
81
+ "name": name if name is not None else self.name,
82
+ "parent_uuid": parent_uuid if parent_uuid is not None else self.parent_uuid,
83
+ "notes": notes if notes is not None else self.notes,
84
+ "starred": starred if starred is not None else self.starred,
85
+ "public": public if public is not None else self.public,
55
86
  }
56
87
 
57
88
  with api_client() as client:
58
- response = client.post(f"/folder/{uuid}", json=new_data)
89
+ response = client.post(f"/folder/{self.uuid}", json=payload)
59
90
  response.raise_for_status()
60
- return response.json()
91
+ updated_data = response.json()
61
92
 
62
- @classmethod
63
- def delete(cls, uuid: stjames.UUID) -> None:
64
- with api_client() as client:
65
- response = client.delete(f"/folder/{uuid}")
66
- response.raise_for_status()
93
+ self.name = updated_data.get("name")
94
+ self.parent_uuid = updated_data.get("parent_uuid")
95
+ self.notes = updated_data.get("notes")
96
+ self.starred = updated_data.get("starred")
97
+ self.public = updated_data.get("public")
98
+ return self
67
99
 
68
- @classmethod
69
- def list(
70
- cls,
71
- parent_uuid: Optional[stjames.UUID] = None,
72
- name_contains: Optional[str] = None,
73
- public: Optional[bool] = None,
74
- starred: Optional[bool] = None,
75
- page: int = 0,
76
- size: int = 10,
77
- ) -> dict[str, Any]:
78
- params: dict[str, Any] = {
79
- "page": page,
80
- "size": size,
81
- }
82
- # Only add optional parameters if they are not None
83
- if parent_uuid is not None:
84
- params["parent_uuid"] = parent_uuid
85
- if name_contains is not None:
86
- params["name_contains"] = name_contains
87
- if public is not None:
88
- params["public"] = public
89
- if starred is not None:
90
- params["starred"] = starred
100
+ def delete(self) -> None:
101
+ """
102
+ Delete the folder and all its contents.
91
103
 
104
+ This is a destructive action, it will delete all the folders and
105
+ workflows that are inside this folder.
106
+
107
+ :raises requests.HTTPError: if the request to the API fails.
108
+ """
92
109
  with api_client() as client:
93
- response = client.get("/folder", params=params)
110
+ response = client.delete(f"/folder/{self.uuid}")
94
111
  response.raise_for_status()
95
- return response.json()
112
+
113
+
114
+ def retrieve_folder(uuid: str) -> Folder:
115
+ """
116
+ Retrieves a folder from the API by UUID. Folder UUID can be found in the folder's URL.
117
+
118
+ :param uuid: The UUID of the folder to retrieve.
119
+ :return: A Folder object representing the retrieved folder.
120
+ :raises HTTPError: If the API request fails.
121
+ """
122
+ with api_client() as client:
123
+ response = client.get(f"/folder/{uuid}")
124
+ response.raise_for_status()
125
+ return Folder(**response.json())
126
+
127
+
128
+ def home_folder() -> Folder:
129
+ """
130
+ Retrieves the home folder from the API.
131
+
132
+ :return: A Folder object representing the home folder.
133
+ :raises HTTPError: If the API request fails.
134
+ """
135
+ with api_client() as client:
136
+ response = client.get("/user/me/root_folders")
137
+ response.raise_for_status()
138
+ return Folder(**response.json()["user_root"])
139
+
140
+
141
+ def list_folders(
142
+ parent_uuid: str | None = None,
143
+ name_contains: str | None = None,
144
+ public: bool | None = None,
145
+ starred: bool | None = None,
146
+ page: int = 0,
147
+ size: int = 10,
148
+ ) -> list[Folder]:
149
+ """
150
+ Retrieve a list of folders based on the specified criteria.
151
+
152
+ :param parent_uuid: UUID of the parent folder to filter by.
153
+ :param name_contains: Substring to search for in folder names.
154
+ :param public: Filter folders by their public status.
155
+ :param starred: Filter folders by their starred status.
156
+ :param page: Pagination parameter to specify the page number.
157
+ :param size: Pagination parameter to specify the number of items per page.
158
+ :return: A list of Folder objects that match the search criteria.
159
+ :raises requests.HTTPError: if the request to the API fails.
160
+ """
161
+
162
+ params: dict[str, Any] = {
163
+ "page": page,
164
+ "size": size,
165
+ }
166
+
167
+ if parent_uuid is not None:
168
+ params["parent_uuid"] = parent_uuid
169
+ if name_contains is not None:
170
+ params["name_contains"] = name_contains
171
+ if public is not None:
172
+ params["public"] = public
173
+ if starred is not None:
174
+ params["starred"] = starred
175
+
176
+ with api_client() as client:
177
+ response = client.get("/folder", params=params)
178
+ response.raise_for_status()
179
+ items = response.json()["folders"]
180
+
181
+ return [Folder(**item) for item in items]
182
+
183
+
184
+ def create_folder(
185
+ name: str,
186
+ parent_uuid: str | None = None,
187
+ notes: str = "",
188
+ starred: bool = False,
189
+ public: bool = False,
190
+ ) -> Folder:
191
+ """
192
+ Create a new folder.
193
+
194
+ :param name: The name of the folder.
195
+ :param parent_uuid: The UUID of the parent folder.
196
+ :param notes: A description of the folder.
197
+ :param starred: Whether the folder is starred.
198
+ :param public: Whether the folder is public.
199
+ :return: The newly created folder.
200
+ """
201
+ data = {
202
+ "name": name,
203
+ "parent_uuid": parent_uuid,
204
+ "notes": notes,
205
+ "starred": starred,
206
+ "public": public,
207
+ }
208
+ with api_client() as client:
209
+ response = client.post("/folder", json=data)
210
+ response.raise_for_status()
211
+ folder_data = response.json()
212
+ return Folder(**folder_data)
rowan/protein.py CHANGED
@@ -1,63 +1,215 @@
1
- from typing import Any, Optional
1
+ from datetime import datetime
2
+ from pathlib import Path
3
+ from typing import Any, Self
2
4
 
3
- import stjames
5
+ from pydantic import BaseModel
4
6
 
5
7
  from .utils import api_client
6
8
 
7
9
 
8
- class Protein:
9
- @classmethod
10
- def retrieve(cls, uuid: stjames.UUID) -> dict:
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
+ """
11
46
  with api_client() as client:
12
- response = client.get(f"/protein/{uuid}")
47
+ response = client.get(f"/protein/{self.uuid}")
13
48
  response.raise_for_status()
14
- return response.json()
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
15
60
 
16
- @classmethod
17
61
  def update(
18
- cls,
19
- uuid: stjames.UUID,
20
- name: Optional[str] = None,
21
- data: Optional[dict] = None,
22
- public: Optional[bool] = None,
23
- pocket: Optional[list[list[float]]] = None,
24
- ) -> None:
25
- old_data = cls.retrieve(uuid)
26
-
27
- new_data = {}
28
- new_data["name"] = name if name is not None else old_data["name"]
29
- new_data["data"] = data if data is not None else old_data["data"]
30
- new_data["public"] = public if public is not None else old_data["public"]
31
- new_data["pocket"] = pocket if pocket is not None else old_data["pocket"]
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
32
71
 
33
- with api_client() as client:
34
- response = client.post(f"/protein/{uuid}", json=new_data)
35
- response.raise_for_status()
36
- return response.json()
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
+ }
37
84
 
38
- @classmethod
39
- def delete(cls, uuid: stjames.UUID) -> None:
40
85
  with api_client() as client:
41
- response = client.delete(f"/protein/{uuid}")
86
+ response = client.post(f"/protein/{self.uuid}", json=updated_payload)
42
87
  response.raise_for_status()
88
+ updated_data = response.json()
43
89
 
44
- @classmethod
45
- def list(
46
- cls,
47
- ancestor_uuid: Optional[stjames.UUID] = None,
48
- name_contains: Optional[str] = None,
49
- page: int = 0,
50
- size: int = 20,
51
- ):
52
- params: dict[str, Any] = {"page": page, "size": size}
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
53
96
 
54
- if ancestor_uuid is not None:
55
- params["ancestor_uuid"] = ancestor_uuid
97
+ def delete(self) -> None:
98
+ """
99
+ Deletes a protein
56
100
 
57
- if name_contains is not None:
58
- params["name_contains"] = name_contains
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()
59
106
 
107
+ def sanitize(self) -> None:
108
+ """
109
+ Sanitizes a protein
110
+
111
+ :raises requests.HTTPError: if the request to the API fails
112
+ """
60
113
  with api_client() as client:
61
- response = client.get("/protein", params=params)
114
+ response = client.post(f"/protein/sanitize/{self.uuid}")
62
115
  response.raise_for_status()
63
- return response.json()
116
+
117
+
118
+ def retrieve_protein(uuid: str) -> Protein:
119
+ """
120
+ Retrieves a protein from the API using its UUID.
121
+
122
+ :param uuid: The UUID of the protein to retrieve.
123
+ :return: A Protein object representing the retrieved protein.
124
+ :raises requests.HTTPError: if the request to the API fails.
125
+ """
126
+
127
+ with api_client() as client:
128
+ response = client.get(f"/protein/{uuid}")
129
+ response.raise_for_status()
130
+ protein_data = response.json()
131
+
132
+ return Protein(**protein_data)
133
+
134
+
135
+ def list_proteins(
136
+ ancestor_uuid: str | None = None,
137
+ name_contains: str | None = None,
138
+ page: int = 0,
139
+ size: int = 20,
140
+ ) -> list[Protein]:
141
+ """
142
+ List proteins
143
+
144
+ :param ancestor_uuid: The UUID of the ancestor protein to filter by
145
+ :param name_contains: Substring to search for in protein names
146
+ :param page: The page number to retrieve
147
+ :param size: The number of items per page
148
+ :return: A list of Protein objects that match the search criteria
149
+ :raises requests.HTTPError: if the request to the API fails
150
+ """
151
+ params: dict[str, Any] = {"page": page, "size": size}
152
+ if ancestor_uuid is not None:
153
+ params["ancestor_uuid"] = ancestor_uuid
154
+ if name_contains is not None:
155
+ params["name_contains"] = name_contains
156
+
157
+ with api_client() as client:
158
+ response = client.get("/protein", params=params)
159
+ response.raise_for_status()
160
+ results = response.json()["proteins"]
161
+
162
+ return [Protein(**item) for item in results]
163
+
164
+
165
+ def upload_protein(name: str, file_path: Path) -> Protein:
166
+ """
167
+ Uploads a protein from a PDB file to the API.
168
+
169
+ :param name: The name of the protein to create
170
+ :param file_path: The path to the PDB file to upload
171
+ :return: A Protein object representing the uploaded protein
172
+ :raises requests.HTTPError: if the request to the API fails
173
+ """
174
+ with api_client() as client:
175
+ # Step 1: Read the file and post it to the conversion endpoint.
176
+ conversion_payload = {"name": name, "text": file_path.read_text()}
177
+ conversion_response = client.post("/convert/pdb_file_to_protein", json=conversion_payload)
178
+ conversion_response.raise_for_status() # Ensure the request was successful
179
+
180
+ # Extract the JSON data from the conversion response.
181
+ protein_data = conversion_response.json()
182
+
183
+ # Step 2: Use the converted data to create the final protein object.
184
+ creation_payload = {"name": name, "protein_data": protein_data, "ancestor_uuid": None}
185
+ final_response = client.post("/protein", json=creation_payload)
186
+ final_response.raise_for_status()
187
+
188
+ # Deserialize the final JSON response into a Protein object and return it.
189
+ return Protein(**final_response.json())
190
+
191
+
192
+ def create_protein_from_pdb_id(name: str, code: str) -> Protein:
193
+ """
194
+ Creates a protein from a PDB ID.
195
+
196
+ :param name: The name of the protein to create
197
+ :param code: The PDB ID of the protein to create
198
+ :return: A Protein object representing the created protein
199
+ :raises requests.HTTPError: if the request to the API fails
200
+ """
201
+ with api_client() as client:
202
+ # Step 1: Read the file and post it to the conversion endpoint.
203
+ conversion_response = client.post(f"/convert/pdb_id_to_protein?pdb_id={code}")
204
+ conversion_response.raise_for_status() # Ensure the request was successful
205
+
206
+ # Extract the JSON data from the conversion response.
207
+ protein_data = conversion_response.json()
208
+
209
+ # Step 2: Use the converted data to create the final protein object.
210
+ creation_payload = {"name": name, "protein_data": protein_data, "ancestor_uuid": None}
211
+ final_response = client.post("/protein", json=creation_payload)
212
+ final_response.raise_for_status()
213
+
214
+ # Deserialize the final JSON response into a Protein object and return it.
215
+ return Protein(**final_response.json())