pybioos 0.0.15__py3-none-any.whl → 0.0.19__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.
@@ -3,7 +3,7 @@ import os
3
3
  import zipfile
4
4
  from datetime import datetime
5
5
  from io import BytesIO
6
- from typing import List
6
+ from typing import List, Dict, Optional, Any
7
7
 
8
8
  import pandas as pd
9
9
  from cachetools import TTLCache, cached
@@ -339,6 +339,13 @@ class Submission(metaclass=SingletonType): # 与run class行为相同
339
339
  if not item.get("Status") in ("Running", "Pending"):
340
340
  self._finish_time = item.get("FinishTime")
341
341
 
342
+ def delete(self):
343
+ """Delete this submission from the workspace."""
344
+ Config.service().delete_submission({
345
+ "WorkspaceID": self.workspace_id,
346
+ "ID": self.id,
347
+ })
348
+
342
349
 
343
350
  class WorkflowResource(metaclass=SingletonType):
344
351
 
@@ -548,17 +555,62 @@ class WorkflowResource(metaclass=SingletonType):
548
555
 
549
556
 
550
557
  class Workflow(metaclass=SingletonType):
558
+ """Represents a workflow in Bio-OS.
559
+
560
+ This class encapsulates all the information and operations related to a workflow,
561
+ including its metadata, inputs, outputs, and execution capabilities.
562
+ """
551
563
 
552
564
  def __init__(self,
553
565
  name: str,
554
566
  workspace_id: str,
555
567
  bucket: str,
556
568
  check: bool = False):
569
+ """Initialize a workflow instance.
570
+
571
+ Args:
572
+ name: The name of the workflow
573
+ workspace_id: The ID of the workspace containing this workflow
574
+ bucket: The S3 bucket associated with this workflow
575
+ check: Whether to check the workflow existence immediately
576
+ """
557
577
  self.name = name
558
578
  self.workspace_id = workspace_id
559
579
  self.bucket = bucket
580
+ self._description: str = ""
581
+ self._create_time: int = 0
582
+ self._update_time: int = 0
583
+ self._language: str = "WDL"
584
+ self._source: str = ""
585
+ self._tag: str = ""
586
+ self._token: Optional[str] = None
587
+ self._main_workflow_path: str = ""
588
+ self._status: Dict[str, Optional[str]] = {"Phase": "", "Message": None}
589
+ self._inputs: List[Dict[str, Any]] = []
590
+ self._outputs: List[Dict[str, Any]] = []
591
+ self._owner_name: str = ""
592
+ self._graph: str = ""
593
+ self._source_type: str = ""
594
+
560
595
  if check:
561
- self.id
596
+ self.sync()
597
+
598
+ def __repr__(self):
599
+ """Return a string representation of the workflow."""
600
+ info_dict = dict_str({
601
+ "id": self.id,
602
+ "name": self.name,
603
+ "description": self.description,
604
+ "language": self.language,
605
+ "source": self.source,
606
+ "tag": self.tag,
607
+ "main_workflow_path": self.main_workflow_path,
608
+ "status": self.status,
609
+ "owner_name": self.owner_name,
610
+ "create_time": self.create_time,
611
+ "update_time": self.update_time
612
+ })
613
+ return f"Workflow:\n{info_dict}"
562
614
 
563
615
  @property
564
616
  @cached(cache=TTLCache(maxsize=100, ttl=1))
@@ -574,6 +626,215 @@ class Workflow(metaclass=SingletonType):
574
626
  raise ParameterError("name")
575
627
  return res["ID"].iloc[0]
576
628
 
629
+ @property
630
+ def description(self) -> str:
631
+ """Get the workflow description."""
632
+ if not self._description:
633
+ self.sync()
634
+ return self._description
635
+
636
+ @property
637
+ def create_time(self) -> int:
638
+ """Get the workflow creation timestamp."""
639
+ if not self._create_time:
640
+ self.sync()
641
+ return self._create_time
642
+
643
+ @property
644
+ def update_time(self) -> int:
645
+ """Get the workflow last update timestamp."""
646
+ if not self._update_time:
647
+ self.sync()
648
+ return self._update_time
649
+
650
+ @property
651
+ def language(self) -> str:
652
+ """Get the workflow language (e.g., WDL)."""
653
+ if not self._language:
654
+ self.sync()
655
+ return self._language
656
+
657
+ @property
658
+ def source(self) -> str:
659
+ """Get the workflow source location."""
660
+ if not self._source:
661
+ self.sync()
662
+ return self._source
663
+
664
+ @property
665
+ def tag(self) -> str:
666
+ """Get the workflow version tag."""
667
+ if not self._tag:
668
+ self.sync()
669
+ return self._tag
670
+
671
+ @property
672
+ def token(self) -> Optional[str]:
673
+ """Get the workflow access token if any."""
674
+ if not self._token:
675
+ self.sync()
676
+ return self._token
677
+
678
+ @property
679
+ def main_workflow_path(self) -> str:
680
+ """Get the main workflow file path."""
681
+ if not self._main_workflow_path:
682
+ self.sync()
683
+ return self._main_workflow_path
684
+
685
+ @property
686
+ def status(self) -> Dict[str, Optional[str]]:
687
+ """Get the workflow status information."""
688
+ if not self._status["Phase"]:
689
+ self.sync()
690
+ return self._status
691
+ @property
692
+ def inputs(self) -> List[Dict[str, Any]]:
693
+ """Get the workflow input parameters."""
694
+ if not self._inputs:
695
+ self.sync()
696
+ return self._inputs
697
+
698
+ @property
699
+ def outputs(self) -> List[Dict[str, Any]]:
700
+ """Get the workflow output parameters."""
701
+ if not self._outputs:
702
+ self.sync()
703
+ return self._outputs
704
+
705
+ @property
706
+ def owner_name(self) -> str:
707
+ """Get the workflow owner's name."""
708
+ if not self._owner_name:
709
+ self.sync()
710
+ return self._owner_name
711
+
712
+ @property
713
+ def graph(self) -> str:
714
+ """Get the workflow graph representation."""
715
+ if not self._graph:
716
+ self.sync()
717
+ return self._graph
718
+
719
+ @property
720
+ def source_type(self) -> str:
721
+ """Get the workflow source type."""
722
+ if not self._source_type:
723
+ self.sync()
724
+ return self._source_type
725
+
726
+ @cached(cache=TTLCache(maxsize=100, ttl=1))
727
+ def sync(self):
728
+ """Synchronize workflow information with the remote service."""
729
+ res = WorkflowResource(self.workspace_id). \
730
+ list().query(f"Name=='{self.name}'")
731
+ if res.empty:
732
+ raise ParameterError("name")
733
+
734
+ # Get detailed workflow information
735
+ params = {
736
+ 'WorkspaceID': self.workspace_id,
737
+ 'Filter': {
738
+ 'IDs': [res["ID"].iloc[0]]
739
+ }
740
+ }
741
+ workflows = Config.service().list_workflows(params).get('Items')
742
+ if len(workflows) != 1:
743
+ raise NotFoundError("workflow", self.name)
744
+
745
+ detail = workflows[0]
746
+
747
+ # Update all properties
748
+ self._description = detail.get("Description", "")
749
+ self._create_time = detail.get("CreateTime", 0)
750
+ self._update_time = detail.get("UpdateTime", 0)
751
+ self._language = detail.get("Language", "WDL")
752
+ self._source = detail.get("Source", "")
753
+ self._tag = detail.get("Tag", "")
754
+ self._token = detail.get("Token")
755
+ self._main_workflow_path = detail.get("MainWorkflowPath", "")
756
+ self._status = detail.get("Status", {"Phase": "", "Message": None})
757
+ self._inputs = detail.get("Inputs", [])
758
+ self._outputs = detail.get("Outputs", [])
759
+ self._owner_name = detail.get("OwnerName", "")
760
+ self._graph = detail.get("Graph", "")
761
+ self._source_type = detail.get("SourceType", "")
762
+
763
+ def get_input_template(self) -> Dict[str, str]:
764
+ """Return a readable template of input parameters.
765
+
766
+ Each entry maps parameter name to a human-friendly description,
767
+ e.g. "String (optional, default = \"abc\")" or "Int".
768
+ """
769
+ result: Dict[str, str] = {}
770
+ inputs = self.inputs or []
771
+ for item in inputs:
772
+ name = item.get("Name", "")
773
+ if not name:
774
+ continue
775
+ type_str = item.get("Type", "")
776
+ optional = item.get("Optional", False)
777
+ default = self._fmt_default(item.get("Default"))
778
+ if optional:
779
+ value = f"{type_str} (optional" + (f", default = {default})" if default is not None else ")")
780
+ else:
781
+ value = type_str
782
+ result[name] = value
783
+ return result
784
+
785
+ def get_output_types(self) -> Dict[str, str]:
786
+ """Return a mapping of output parameter name to its type."""
787
+ outputs = self.outputs or []
788
+ return {item.get("Name", ""): item.get("Type", "") for item in outputs if item.get("Name")}
789
+
790
+ def get_metadata(self) -> Dict[str, Any]:
791
+ """Return workflow metadata in a flat dictionary."""
792
+ return {
793
+ "name": self.name,
794
+ "description": self.description,
795
+ "language": self.language,
796
+ "source": self.source,
797
+ "tag": self.tag,
798
+ "status": self.status,
799
+ "owner_name": self.owner_name,
800
+ "create_time": self.create_time,
801
+ "update_time": self.update_time,
802
+ "main_workflow_path": self.main_workflow_path,
803
+ "source_type": self.source_type,
804
+ }
805
+
806
+ @staticmethod
807
+ def _fmt_default(raw: Any) -> Optional[str]:
808
+ """Format default values into a readable string.
809
+
810
+ - None -> None
811
+ - bool -> "true" / "false"
812
+ - int/float -> numeric string
813
+ - other strings -> quoted string
814
+ """
815
+ if raw is None:
816
+ return None
817
+ if isinstance(raw, bool):
818
+ return str(raw).lower()
819
+ if isinstance(raw, (int, float)):
820
+ return str(raw)
821
+ if isinstance(raw, str):
822
+ lo = raw.lower()
823
+ if lo in {"true", "false"}:
824
+ return lo
825
+ try:
826
+ int(raw)
827
+ return raw
828
+ except ValueError:
829
+ pass
830
+ try:
831
+ float(raw)
832
+ return raw
833
+ except ValueError:
834
+ pass
835
+ return f'"{raw}"'
836
+ return str(raw)
837
+
577
838
  @property
578
839
  @cached(cache=TTLCache(maxsize=100, ttl=1))
579
840
  def get_cluster(self):
@@ -594,11 +855,14 @@ class Workflow(metaclass=SingletonType):
594
855
  return info['ID']
595
856
  raise NotFoundError("cluster", "workflow")
596
857
 
597
- def query_data_model_id(self, name: str) -> "":
858
+ def query_data_model_id(self, name: str) -> str:
598
859
  """Gets the id of given data_models among those accessible
599
860
 
600
- :param name:
601
- :return:
861
+ Args:
862
+ name: The name of the data model
863
+
864
+ Returns:
865
+ str: The ID of the data model, or empty string if not found
602
866
  """
603
867
  res = DataModelResource(self.workspace_id).list(). \
604
868
  query(f"Name=='{name}'")
@@ -613,7 +877,8 @@ class Workflow(metaclass=SingletonType):
613
877
  call_caching: bool,
614
878
  submission_name_suffix: str = "",
615
879
  row_ids: List[str] = [],
616
- data_model_name: str = '') -> List[Run]:
880
+ data_model_name: str = '',
881
+ mount_tos: bool = False) -> List[Run]:
617
882
  """Submit an existed workflow.
618
883
 
619
884
  *Example*:
@@ -664,6 +929,7 @@ class Workflow(metaclass=SingletonType):
664
929
  'Inputs': inputs,
665
930
  'ExposedOptions': {
666
931
  "ReadFromCache": call_caching,
932
+ "MountTOS": mount_tos,
667
933
  # TODO this may change in the future
668
934
  "ExecutionRootDir": f"s3://{self.bucket}"
669
935
  },
@@ -682,3 +948,4 @@ class Workflow(metaclass=SingletonType):
682
948
  submission_id = Config.service().create_submission(params).get("ID")
683
949
 
684
950
  return Submission(self.workspace_id, submission_id).runs
951
+
@@ -1,4 +1,7 @@
1
1
  from datetime import datetime
2
+ import os
3
+ import time
4
+ import urllib.request
2
5
 
3
6
  import pandas as pd
4
7
  from cachetools import TTLCache, cached
@@ -7,6 +10,7 @@ from bioos.config import Config
7
10
  from bioos.resource.data_models import DataModelResource
8
11
  from bioos.resource.files import FileResource
9
12
  from bioos.resource.workflows import Workflow, WorkflowResource
13
+ from bioos.resource.iesapp import WebInstanceApp, WebInstanceAppResource
10
14
  from bioos.utils.common_tools import SingletonType, dict_str
11
15
 
12
16
 
@@ -110,6 +114,15 @@ class Workspace(metaclass=SingletonType):
110
114
 
111
115
  return FileResource(self._id, self._bucket)
112
116
 
117
+ @property
118
+ def webinstanceapps(self) -> WebInstanceAppResource:
119
+ """Returns WebInstanceAppResource object.
120
+
121
+ :return: WebInstanceAppResource object
122
+ :rtype: WebInstanceAppResource
123
+ """
124
+ return WebInstanceAppResource(self._id)
125
+
113
126
  def workflow(self, name: str) -> Workflow: # 通过这里执行的选择workflow生成wf的操作
114
127
  """Returns the workflow for the given name
115
128
 
@@ -121,3 +134,130 @@ class Workspace(metaclass=SingletonType):
121
134
  if not self._bucket:
122
135
  self._bucket = self.basic_info.get("s3_bucket")
123
136
  return Workflow(name, self._id, self._bucket)
137
+
138
+ def webinstanceapp(self, name: str) -> WebInstanceApp:
139
+ """Returns the webinstanceapp for the given name
140
+
141
+ :param name: WebInstanceApp name
142
+ :type name: str
143
+ :return: Specified webinstanceapp object
144
+ :rtype: WebInstanceApp
145
+ """
146
+ return WebInstanceApp(name, self._id)
147
+
148
+ def bind_cluster(self, cluster_id: str, type_: str = "workflow") -> dict:
149
+ """把当前 Workspace 绑定到指定集群"""
150
+ params = {"ClusterID": cluster_id, "Type": type_, "ID": self._id}
151
+ return Config.service().bind_cluster_to_workspace(params)
152
+
153
+
154
+ def export_workspace_v2(self,
155
+ download_path: str = "./",
156
+ monitor: bool = True,
157
+ monitor_interval: int = 5,
158
+ max_retries: int = 60) -> dict:
159
+ """导出当前 Workspace 的所有元信息并下载到本地
160
+
161
+ :param download_path: 下载文件保存路径,默认当前目录
162
+ :type download_path: str
163
+ :param monitor: 是否监控导出状态直到完成,默认 True
164
+ :type monitor: bool
165
+ :param monitor_interval: 轮询间隔(秒),默认 5 秒
166
+ :type monitor_interval: int
167
+ :param max_retries: 最大重试次数,默认 60 次(5 分钟)
168
+ :type max_retries: int
169
+ :return: 导出结果信息,包含 status、schema_id、file_path 等
170
+ :rtype: dict
171
+ """
172
+ params = {"WorkspaceID": self._id}
173
+ result = Config.service().export_workspace_v2(params)
174
+ schema_id = result.get('ID')
175
+
176
+ if not schema_id:
177
+ raise Exception("Failed to create export task: No schema ID returned")
178
+
179
+ # 如果不监控,直接返回任务 ID
180
+ if not monitor:
181
+ return {
182
+ "status": "submitted",
183
+ "schema_id": schema_id,
184
+ "message": "Export task submitted. Use list_schemas to check status."
185
+ }
186
+
187
+ # 步骤 2: 轮询查询导出状态
188
+ Config.Logger.info(f"Export task created with schema ID: {schema_id}")
189
+ Config.Logger.info(f"Monitoring export status (checking every {monitor_interval}s, max {max_retries} retries)...")
190
+
191
+ retry_count = 0
192
+ schema_key = None
193
+
194
+ while retry_count < max_retries:
195
+ # 查询所有 schemas
196
+ schemas_result = Config.service().list_schemas({"Filter": {}})
197
+ schemas = schemas_result.get("Items", [])
198
+
199
+ # 查找当前导出任务
200
+ for schema in schemas:
201
+ if schema.get("ID") == schema_id:
202
+ phase = schema.get("Status", {}).get("Phase", "")
203
+ message = schema.get("Status", {}).get("Message", "")
204
+
205
+ if phase == "Succeeded":
206
+ Config.Logger.info("Export task succeeded!")
207
+ schema_key = schema.get("SchemaKey")
208
+ break
209
+ elif phase == "Failed":
210
+ error_msg = f"Export task failed: {message}"
211
+ Config.Logger.error(error_msg)
212
+ raise Exception(error_msg)
213
+ else:
214
+ Config.Logger.info(f"Export task status: {phase}")
215
+
216
+ # 如果成功找到文件,跳出循环
217
+ if schema_key:
218
+ break
219
+
220
+ # 继续等待
221
+ time.sleep(monitor_interval)
222
+ retry_count += 1
223
+
224
+ # 超时检查
225
+ if not schema_key:
226
+ raise Exception(f"Export task timeout after {max_retries * monitor_interval} seconds")
227
+
228
+ # 步骤 3: 获取预签名 URL
229
+ Config.Logger.info(f"Getting presigned URL for schema: {schema_id}")
230
+ presigned_params = {
231
+ "ID": schema_id,
232
+ "WorkspaceID": self._id
233
+ }
234
+ presigned_result = Config.service().get_export_workspace_presigned_url(presigned_params)
235
+ presigned_url = presigned_result.get("PreSignedURL")
236
+
237
+ if not presigned_url:
238
+ raise Exception("Failed to get presigned URL for export file")
239
+
240
+ Config.Logger.info(f"Downloading export file from presigned URL...")
241
+
242
+ # 步骤 4: 使用 HTTP 请求下载文件
243
+ # 确保下载目录存在
244
+ os.makedirs(download_path, exist_ok=True)
245
+
246
+ # 从 URL 中提取文件名
247
+ filename = os.path.basename(schema_key)
248
+ local_file_path = os.path.join(download_path, filename)
249
+
250
+ try:
251
+ # 使用 urllib 下载文件
252
+ urllib.request.urlretrieve(presigned_url, local_file_path)
253
+ Config.Logger.info(f"Export file downloaded successfully to: {local_file_path}")
254
+ except Exception as e:
255
+ raise Exception(f"Failed to download export file from presigned URL: {str(e)}")
256
+
257
+ return {
258
+ "status": "success",
259
+ "schema_id": schema_id,
260
+ "schema_key": schema_key,
261
+ "file_path": local_file_path,
262
+ "message": f"Workspace exported and downloaded successfully"
263
+ }
@@ -47,6 +47,11 @@ class BioOsService(Service):
47
47
  'Action': 'ListWorkspaces',
48
48
  'Version': '2021-03-04'
49
49
  }, {}, {}),
50
+ 'CreateWorkspace':
51
+ ApiInfo('POST', '/', {
52
+ 'Action': 'CreateWorkspace',
53
+ 'Version': '2021-03-04'
54
+ }, {}, {}),
50
55
  'CreateDataModel':
51
56
  ApiInfo('POST', '/', {
52
57
  'Action': 'CreateDataModel',
@@ -127,12 +132,74 @@ class BioOsService(Service):
127
132
  'Action': 'DeleteWorkflow',
128
133
  'Version': '2021-03-04'
129
134
  }, {}, {}),
135
+ 'BindClusterToWorkspace':
136
+ ApiInfo('POST', '/', {
137
+ 'Action': 'BindClusterToWorkspace',
138
+ 'Version': '2021-03-04'
139
+ }, {}, {}),
140
+ 'ListWebappInstances':
141
+ ApiInfo('POST', '/', {
142
+ 'Action': 'ListWebappInstances',
143
+ 'Version': '2021-03-04'
144
+ }, {}, {}),
145
+ 'CreateWebappInstance':
146
+ ApiInfo('POST', '/', {
147
+ 'Action': 'CreateWebappInstance',
148
+ 'Version': '2021-03-04'
149
+ }, {}, {}),
150
+ 'CheckCreateWebappInstance':
151
+ ApiInfo('POST', '/', {
152
+ 'Action': 'CheckCreateWebappInstance',
153
+ 'Version': '2021-03-04'
154
+ }, {}, {}),
155
+ 'DeleteWebappInstance':
156
+ ApiInfo('POST', '/', {
157
+ 'Action': 'DeleteWebappInstance',
158
+ 'Version': '2021-03-04'
159
+ }, {}, {}),
160
+ 'StartWebappInstance':
161
+ ApiInfo('POST', '/', {
162
+ 'Action': 'StartWebappInstance',
163
+ 'Version': '2021-03-04'
164
+ }, {}, {}),
165
+ 'StopWebappInstance':
166
+ ApiInfo('POST', '/', {
167
+ 'Action': 'StopWebappInstance',
168
+ 'Version': '2021-03-04'
169
+ }, {}, {}),
170
+ 'ListWebappInstanceEvents':
171
+ ApiInfo('POST', '/', {
172
+ 'Action': 'ListWebappInstanceEvents',
173
+ 'Version': '2021-03-04'
174
+ }, {}, {}),
175
+ 'CommitIESImage':
176
+ ApiInfo('POST', '/', {
177
+ 'Action': 'CommitIESImage',
178
+ 'Version': '2021-03-04'
179
+ }, {}, {}),
180
+ 'ExportWorkspaceV2':
181
+ ApiInfo('POST', '/', {
182
+ 'Action': 'ExportWorkspaceV2',
183
+ 'Version': '2021-03-04'
184
+ }, {}, {}),
185
+ 'ListSchemas':
186
+ ApiInfo('POST', '/', {
187
+ 'Action': 'ListSchemas',
188
+ 'Version': '2021-03-04'
189
+ }, {}, {}),
190
+ 'GetExportWorkspacePreSignedURL':
191
+ ApiInfo('POST', '/', {
192
+ 'Action': 'GetExportWorkspacePreSignedURL',
193
+ 'Version': '2021-03-04'
194
+ }, {}, {}),
130
195
  }
131
196
  return api_info
132
197
 
133
198
  def list_workspaces(self, params): # 以下各方法的params需要在外部使用时构建
134
199
  return self.__request('ListWorkspaces', params)
135
200
 
201
+ def create_workspace(self, params):
202
+ return self.__request('CreateWorkspace', params)
136
203
  def create_data_model(self, params):
137
204
  return self.__request('CreateDataModel', params)
138
205
 
@@ -181,6 +248,42 @@ class BioOsService(Service):
181
248
  def delete_workflow(self, params):
182
249
  return self.__request("DeleteWorkflow", params)
183
250
 
251
+ def bind_cluster_to_workspace(self, params):
252
+ return self.__request("BindClusterToWorkspace", params)
253
+
254
+ def list_webinstance_apps(self, params):
255
+ return self.__request('ListWebappInstances', params)
256
+
257
+ def create_webinstance_app(self, params):
258
+ return self.__request('CreateWebappInstance', params)
259
+
260
+ def check_webinstance_app(self, params):
261
+ return self.__request("CheckCreateWebappInstance", params)
262
+
263
+ def delete_webinstance_app(self, params):
264
+ return self.__request("DeleteWebappInstance", params)
265
+
266
+ def start_webinstance_app(self, params):
267
+ return self.__request("StartWebappInstance", params)
268
+
269
+ def stop_webinstance_app(self, params):
270
+ return self.__request("StopWebappInstance", params)
271
+
272
+ def list_webinstance_events(self, params):
273
+ return self.__request("ListWebappInstanceEvents", params)
274
+
275
+ def commit_ies_image(self, params):
276
+ return self.__request("CommitIESImage", params)
277
+
278
+ def export_workspace_v2(self, params):
279
+ return self.__request("ExportWorkspaceV2", params)
280
+
281
+ def list_schemas(self, params):
282
+ return self.__request("ListSchemas", params)
283
+
284
+ def get_export_workspace_presigned_url(self, params):
285
+ return self.__request("GetExportWorkspacePreSignedURL", params)
286
+
184
287
  def __request(self, action, params):
185
288
  res = self.json(
186
289
  action, dict(),