seqslab-cli 3.3.4__tar.gz → 3.3.6__tar.gz

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.
Files changed (110) hide show
  1. {seqslab-cli-3.3.4/python/seqslab_cli.egg-info → seqslab-cli-3.3.6}/PKG-INFO +1 -1
  2. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/__init__.py +1 -1
  3. seqslab-cli-3.3.6/python/seqslab/color.py +8 -0
  4. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/drs/api/base.py +3 -3
  5. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/drs/commands.py +241 -110
  6. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/drs/internal/aiocopy.py +11 -6
  7. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/drs/storage/azure.py +7 -5
  8. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/drs/utils/atgxmetadata.py +1 -0
  9. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/trs/commands.py +16 -7
  10. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/trs/template/base.py +5 -3
  11. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/wes/commands.py +51 -61
  12. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/wes/internal/parameters.py +32 -1
  13. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/wes/resource/base.py +1 -2
  14. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/wes/resource/common.py +2 -2
  15. seqslab-cli-3.3.6/python/seqslab/workspace/resource/azure.py +26 -0
  16. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/workspace/resource/base.py +13 -1
  17. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6/python/seqslab_cli.egg-info}/PKG-INFO +1 -1
  18. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab_cli.egg-info/SOURCES.txt +1 -0
  19. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab_cli.egg-info/requires.txt +6 -6
  20. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/requirements.txt +6 -6
  21. seqslab-cli-3.3.4/python/seqslab/wes/resource/azure.py +0 -26
  22. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/LICENSE +0 -0
  23. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/MANIFEST.in +0 -0
  24. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/README.md +0 -0
  25. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/auth/__init__.py +0 -0
  26. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/auth/azuread.py +0 -0
  27. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/auth/commands.py +0 -0
  28. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/cli.py +0 -0
  29. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/context.py +0 -0
  30. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/drs/__init__.py +0 -0
  31. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/drs/api/__init__.py +0 -0
  32. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/drs/api/azure.py +0 -0
  33. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/drs/api/common.py +0 -0
  34. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/drs/api/template.py +0 -0
  35. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/drs/internal/__init__.py +0 -0
  36. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/drs/internal/common.py +0 -0
  37. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/drs/internal/utils.py +0 -0
  38. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/drs/storage/__init__.py +0 -0
  39. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/drs/storage/base.py +0 -0
  40. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/drs/utils/__init__.py +0 -0
  41. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/drs/utils/biomimetype.py +0 -0
  42. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/drs/utils/progressbar.py +0 -0
  43. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/exceptions.py +0 -0
  44. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/organization/__init__.py +0 -0
  45. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/organization/commands.py +0 -0
  46. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/organization/resource/__init__.py +0 -0
  47. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/organization/resource/base.py +0 -0
  48. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/plugin.py +0 -0
  49. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/role/__init__.py +0 -0
  50. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/role/commands.py +0 -0
  51. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/role/internal/__init__.py +0 -0
  52. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/role/internal/common.py +0 -0
  53. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/role/resource/__init__.py +0 -0
  54. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/role/resource/azure.py +0 -0
  55. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/role/resource/base.py +0 -0
  56. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/runsheet/__init__.py +0 -0
  57. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/runsheet/runsheet.py +0 -0
  58. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/sample_sheet/__init__.py +0 -0
  59. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/sample_sheet/_version.py +0 -0
  60. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/sample_sheet/util.py +0 -0
  61. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/scr/__init__.py +0 -0
  62. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/scr/commands.py +0 -0
  63. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/scr/internal/__init__.py +0 -0
  64. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/scr/internal/common.py +0 -0
  65. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/scr/resource/__init__.py +0 -0
  66. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/scr/resource/azure.py +0 -0
  67. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/scr/resource/base.py +0 -0
  68. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/session_logger.py +0 -0
  69. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/settings.py +0 -0
  70. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/statusbar.py +0 -0
  71. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/trs/__init__.py +0 -0
  72. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/trs/internal/__init__.py +0 -0
  73. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/trs/internal/utils.py +0 -0
  74. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/trs/register/__init__.py +0 -0
  75. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/trs/register/azure.py +0 -0
  76. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/trs/register/base.py +0 -0
  77. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/trs/register/common.py +0 -0
  78. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/trs/register/template.py +0 -0
  79. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/trs/resource/__init__.py +0 -0
  80. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/trs/resource/azure.py +0 -0
  81. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/trs/resource/base.py +0 -0
  82. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/trs/resource/common.py +0 -0
  83. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/trs/template/__init__.py +0 -0
  84. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/trs/template/template.py +0 -0
  85. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/usage_logger.py +0 -0
  86. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/user/__init__.py +0 -0
  87. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/user/commands.py +0 -0
  88. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/user/internal/__init__.py +0 -0
  89. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/user/internal/common.py +0 -0
  90. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/user/resource/__init__.py +0 -0
  91. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/user/resource/azure.py +0 -0
  92. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/user/resource/base.py +0 -0
  93. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/wes/__init__.py +0 -0
  94. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/wes/internal/__init__.py +0 -0
  95. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/wes/resource/__init__.py +0 -0
  96. {seqslab-cli-3.3.4/python/seqslab/workspace → seqslab-cli-3.3.6/python/seqslab/wes}/resource/azure.py +0 -0
  97. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/wes/template/__init__.py +0 -0
  98. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/wes/template/base.py +0 -0
  99. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/wes/template/template.py +0 -0
  100. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/workspace/__init__.py +0 -0
  101. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/workspace/commands.py +0 -0
  102. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/workspace/internal/__init__.py +0 -0
  103. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/workspace/internal/common.py +0 -0
  104. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab/workspace/resource/__init__.py +0 -0
  105. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab_cli.egg-info/dependency_links.txt +0 -0
  106. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab_cli.egg-info/entry_points.txt +0 -0
  107. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab_cli.egg-info/top_level.txt +0 -0
  108. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/python/seqslab_cli.egg-info/zip-safe +0 -0
  109. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/setup.cfg +0 -0
  110. {seqslab-cli-3.3.4 → seqslab-cli-3.3.6}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: seqslab-cli
3
- Version: 3.3.4
3
+ Version: 3.3.6
4
4
  Summary: Atgenomix SeqsLab Command Line Tool
5
5
  Home-page: https://github.com/AnomeGAP/seqslab-cli
6
6
  Author: Allen Chang
@@ -21,6 +21,6 @@ name = "seqslab"
21
21
  __all__ = []
22
22
 
23
23
 
24
- __version__ = "3.3.4"
24
+ __version__ = "3.3.6"
25
25
 
26
26
  LOGGING = {"DIR_PATH": "/var/log/seqslab"}
@@ -0,0 +1,8 @@
1
+ def color_handler(func):
2
+ ColorDict = {"yellow": "\033[01;33m", "green": "\033[01;32m", "red": "\033[01;31m"}
3
+
4
+ def inner_function(*args, **kwargs):
5
+ kwargs.update(ColorDict)
6
+ return func(*args, **kwargs)
7
+
8
+ return inner_function
@@ -333,9 +333,9 @@ class DRSregister:
333
333
  folder_stdin["access_methods"].clear()
334
334
  for i, dsts in enumerate(dsts_list):
335
335
  folder_stdin["access_methods"].append(access_method_infos[i])
336
- folder_stdin["access_methods"][i]["access_url"][
337
- "url"
338
- ] = self.common_root(dsts=dsts, workspace=kwargs.get("workspace"))
336
+ folder_stdin["access_methods"][i]["access_url"]["url"] = (
337
+ self.common_root(dsts=dsts, workspace=kwargs.get("workspace"))
338
+ )
339
339
 
340
340
  folder_stdin["name"] = kwargs.get(
341
341
  "name",
@@ -15,10 +15,12 @@ from urllib.parse import unquote
15
15
 
16
16
  from jsonpath_ng.ext import parse
17
17
  from nubia import argument, command, context
18
+ from seqslab.color import color_handler
18
19
  from seqslab.drs.utils.atgxmetadata import AtgxMetaData
19
20
  from seqslab.drs.utils.progressbar import ProgressBarObject
20
21
  from seqslab.exceptions import async_exception_handler, exception_handler
21
22
  from seqslab.runsheet.runsheet import RunSheet
23
+ from seqslab.workspace.internal.common import get_factory as get_workspace_factory
22
24
  from tabulate import tabulate
23
25
  from termcolor import cprint
24
26
  from yarl import URL
@@ -57,6 +59,7 @@ class BaseDatahub:
57
59
  return context.get_context().args.proxy
58
60
 
59
61
  @staticmethod
62
+ @color_handler
60
63
  @exception_handler
61
64
  def _upload(src: URL, dst: URL, recursive: bool, **kwargs) -> list:
62
65
  # copy from local to cloud
@@ -67,33 +70,40 @@ class BaseDatahub:
67
70
  if os.path.isdir(paths[0]):
68
71
  if not recursive:
69
72
  raise OSError("--recursive (-r) is Required.")
70
- if not str(dst).endswith("/") and not kwargs.get("non_interactive"):
71
- sys.stderr.write(
72
- f'f"Do you mean to rename the `{os.path.basename(str(src))}` to '
73
- f'`{os.path.basename(str(dst))}`?(Y/N): "'
74
- )
75
- user_input = input()
76
- if user_input.lower() != "y":
77
- raise InterruptedError("Interrupt all the process.")
73
+ sys.stderr.write(
74
+ f"{kwargs['green']}"
75
+ + "Destination Path: "
76
+ + f"{kwargs['red']}"
77
+ + f"{str(dst)}\n"
78
+ + f"{kwargs['yellow']}\0"
79
+ )
78
80
  coro = aiocopy.dir_to_blob(URL(*paths), dst, **kwargs)
79
81
  else:
80
- if not str(dst).endswith("/") and not kwargs.get("non_interactive"):
82
+ if str(dst).endswith("/"):
83
+ sys.stderr.write(
84
+ f"{kwargs['green']}"
85
+ + "Destination Path: "
86
+ + f"{kwargs['red']}"
87
+ + f"{os.path.join(str(dst), os.path.basename(str(src)))} \n"
88
+ + f"{kwargs['yellow']}\0"
89
+ )
90
+ else:
81
91
  sys.stderr.write(
82
- f'f"Do you mean to rename the `{os.path.basename(str(src))}` to '
83
- f'`{os.path.basename(str(dst))}`?(Y/N): "'
92
+ f"{kwargs['green']}"
93
+ + "Destination Path: "
94
+ + f"{kwargs['red']}"
95
+ + f"{str(dst)} \n"
96
+ + f"{kwargs['yellow']}\0"
84
97
  )
85
- user_input = input()
86
- if user_input.lower() != "y":
87
- raise InterruptedError("Interrupt all the process.")
88
98
  coro = aiocopy.file_to_blob([URL(*paths)], dst, **kwargs)
89
99
  else:
90
- if not str(dst).endswith("/") and not kwargs.get("non_interactive"):
91
- sys.stderr.write(
92
- f"Do you mean to upload all the files into {str(dst)}/?(Y/N): "
93
- )
94
- user_input = input()
95
- if user_input.lower() != "y":
96
- raise InterruptedError("Interrupt all the process.")
100
+ sys.stderr.write(
101
+ f"{kwargs['green']}"
102
+ + "Destination Path: "
103
+ + f"{kwargs['red']}"
104
+ + f"cloud://{str(dst)}. \n"
105
+ + f"{kwargs['yellow']}\0"
106
+ )
97
107
  files = []
98
108
  dirs = []
99
109
  for p in paths:
@@ -156,6 +166,13 @@ class BaseDatahub:
156
166
  "Stdin format only supports json. Please read the document and make sure the format is valid."
157
167
  )
158
168
 
169
+ @staticmethod
170
+ def _valide_workspace(workspace: str) -> bool:
171
+ ctx = context.get_context()
172
+ backend = ctx.args.backend
173
+ resource = get_workspace_factory().load_resource()
174
+ return resource.validate_workspace(workspace, backend)
175
+
159
176
  @command(aliases=["copy"])
160
177
  @argument(
161
178
  "workspace",
@@ -173,7 +190,9 @@ class BaseDatahub:
173
190
  "dst",
174
191
  type=str,
175
192
  positional=False,
176
- description="Specify the destination directory, file path, or DRS URL (optional, default ='MyDir/'). ",
193
+ description="Specify the destination directory, file path, or DRS URL. Paths with leading slashes, leading "
194
+ "backslashes, or dot-only segments are invalid. Additionally, paths should not contain characters other than "
195
+ "[0-9a-zA-Z-._:/]. If not provided, the default is the current Linux epoch timestamp (optional).",
177
196
  )
178
197
  @argument(
179
198
  "recursive",
@@ -194,15 +213,13 @@ class BaseDatahub:
194
213
  "multiprocessing",
195
214
  type=int,
196
215
  positional=False,
197
- description="Specify the number of files that you want to upload at any given time. "
198
- "Changing the default value is not recommended (default = 1).",
216
+ description="Specify the number of files that you want to upload at any given time. (default = 1).",
199
217
  )
200
218
  @argument(
201
219
  "chunk_size",
202
220
  type=str,
203
221
  positional=False,
204
- description="Specify the size of each file that you want to upload in megabytes (MB). "
205
- "Changing the default value is not recommended (default = 8MB).",
222
+ description="Specify the size of each file that you want to upload in mebibyte (MiB). (default = 8MiB).",
206
223
  choices=["8", "16"],
207
224
  )
208
225
  @argument(
@@ -212,28 +229,19 @@ class BaseDatahub:
212
229
  description="Specify the stdout format (default = json).",
213
230
  choices=["json", "table", "tsv"],
214
231
  )
215
- @argument(
216
- "non_interactive",
217
- type=bool,
218
- positional=False,
219
- description="Whether to run in non-interactive mode, i.e. without prompt (default = False).",
220
- aliases=["ni"],
221
- )
222
232
  def upload(
223
233
  self,
224
234
  workspace: str,
225
235
  src: str,
226
- dst: str = "MyDir/",
236
+ dst: str = f"{str(int(datetime.datetime.now().timestamp()))}/",
227
237
  recursive: bool = False,
228
238
  output: str = "json",
229
239
  concurrency: int = 0,
230
240
  multiprocessing: int = 1,
231
241
  chunk_size: str = "8",
232
- expiry_time: str = None,
233
- non_interactive: bool = False,
234
242
  ) -> int:
235
243
  """
236
- Upload data from the local file system to a SeqsLab Data Hub cloud service.
244
+ Upload data from the local file system to a SeqsLab Data Hub cloud service. This command behaves similarly to gsutil cp command.
237
245
  """
238
246
 
239
247
  def __log(results: List[dict], output: str):
@@ -249,12 +257,26 @@ class BaseDatahub:
249
257
  else:
250
258
  logging.info(msg)
251
259
 
252
- if not workspace:
253
- logging.error("Invalid workspace name")
254
- cprint("Enter a valid workspace name.", "red")
260
+ if not self._valide_workspace(workspace):
261
+ logging.error("Workspace not found")
262
+ cprint(f"Workspace {workspace} not found.", "red")
255
263
  return errno.EINVAL
256
264
 
257
- if dst.isspace() or len(dst) == 0:
265
+ if dst:
266
+ # Standard Library
267
+ import re
268
+
269
+ invalid_patterns = [
270
+ r"(^|/|\\)\.{1,}($|/|\\|$)|^\s*$|^\/$|^\.$|^/|//", # ex: ., .., ..., ./, ../, .../, ././. etc
271
+ r"[^0-9a-zA-Z\-\._:/]+",
272
+ # ex: Only alphanumeric characters, hyphen, period, colon, and underscore are allowed.
273
+ ]
274
+ for pattern in invalid_patterns:
275
+ if re.search(pattern, dst):
276
+ logging.error(f"Invalid dst path {dst}")
277
+ cprint(f"Invalid dst path {dst}.", "red")
278
+ return errno.EINVAL
279
+ else:
258
280
  logging.error("Invalid dst path")
259
281
  cprint("Enter a valid dst path.", "red")
260
282
  return errno.EINVAL
@@ -262,19 +284,7 @@ class BaseDatahub:
262
284
  if os.path.isdir(src):
263
285
  if src.endswith("/"):
264
286
  src = URL(f'{str(src).rstrip("/")}', encoded=True)
265
- if expiry_time:
266
- # Standard Library
267
- import re
268
287
 
269
- pattern = r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}"
270
- if not re.fullmatch(pattern, expiry_time):
271
- cprint(
272
- "Make sure expiry_time format is correct. e.g. 2023-12-31 00:00:00",
273
- "red",
274
- )
275
- return errno.EINVAL
276
-
277
- dst = "/" if dst == "." else dst
278
288
  result = self._upload(
279
289
  src=URL(src, encoded=True),
280
290
  dst=URL(dst, encoded=True),
@@ -284,8 +294,6 @@ class BaseDatahub:
284
294
  concurrency=concurrency,
285
295
  chunk_size=self.size[chunk_size],
286
296
  proxy=self.proxy,
287
- non_interactive=non_interactive,
288
- expiry_time=expiry_time,
289
297
  )
290
298
  if isinstance(result, int):
291
299
  return result
@@ -322,23 +330,20 @@ class BaseDatahub:
322
330
  type=int,
323
331
  positional=False,
324
332
  description="Specify the numbers of chunks that can be transferred concurrently. The download bandwidth "
325
- "can be estimated using the formula concurrency * chunk_size. "
326
- "Changing the default value is not recommended (optional, default = 120).",
333
+ "can be estimated using the formula concurrency * chunk_size. (optional, default = 120).",
327
334
  )
328
335
  @argument(
329
336
  "multiprocessing",
330
337
  type=int,
331
338
  positional=False,
332
- description="Specify the number of files that you want to upload at any given time. "
333
- "Changing the default value is not recommended (default = 1).",
339
+ description="Specify the number of files that you want to upload at any given time. (default = 1).",
334
340
  )
335
341
  @argument(
336
342
  "chunk_size",
337
343
  type=str,
338
344
  positional=False,
339
345
  description="Specify the size of chunk in the chunked downloading process. "
340
- "If the specified size is greater than 4MB, the md5 hashes are not checked. "
341
- "Changing the default value is not recommended (optional, default = 4MB).",
346
+ "If the specified size is greater than 4MiB, the md5 hashes are not checked. (optional, default = 4MiB).",
342
347
  choices=["4", "8", "16", "32"],
343
348
  )
344
349
  @argument(
@@ -384,9 +389,9 @@ class BaseDatahub:
384
389
  else:
385
390
  logging.info(msg)
386
391
 
387
- if not workspace:
388
- logging.error("Invalid workspace name")
389
- cprint("Invalid workspace name", "red")
392
+ if not self._valide_workspace(workspace):
393
+ logging.error("Workspace not found")
394
+ cprint(f"Workspace {workspace} not found.", "red")
390
395
  return errno.EINVAL
391
396
 
392
397
  tasks = []
@@ -461,9 +466,9 @@ class BaseDatahub:
461
466
  browsers. The share link expires either after the first successful download or after a period of three months.
462
467
  """
463
468
 
464
- if not workspace:
465
- logging.error("Invalid workspace name")
466
- cprint("Invalid workspace name", "red")
469
+ if not self._valide_workspace(workspace):
470
+ logging.error("Workspace not found")
471
+ cprint("Workspace not found.", "red")
467
472
  return errno.EINVAL
468
473
 
469
474
  api_backend = drs_register().load_register(workspace)
@@ -663,6 +668,10 @@ class BaseDatahub:
663
668
  """
664
669
  Register a DRS object as a blob.
665
670
  """
671
+ if not self._valide_workspace(workspace):
672
+ logging.error("Workspace not found")
673
+ cprint("Workspace not found.", "red")
674
+ return errno.EINVAL
666
675
 
667
676
  def __log(results: List[dict], output: str):
668
677
  self._stdout(results=results, output=output)
@@ -725,17 +734,21 @@ class BaseDatahub:
725
734
  "mime_type": kwargs.get("mime_type"),
726
735
  "file_type": kwargs.get("file_type"),
727
736
  "created_time": kwargs.get("created_time"),
728
- "updated_time": kwargs.get("updated_time")
729
- if kwargs.get("updated_time")
730
- else kwargs.get("created_time"),
737
+ "updated_time": (
738
+ kwargs.get("updated_time")
739
+ if kwargs.get("updated_time")
740
+ else kwargs.get("created_time")
741
+ ),
731
742
  "size": kwargs.get("size"),
732
743
  "access_methods": json.loads(kwargs.get("access_methods")),
733
744
  "checksums": checksums,
734
745
  "description": kwargs.get("description"),
735
746
  "aliases": kwargs.get("aliases"),
736
- "metadata": json.loads(kwargs.get("metadata"))
737
- if kwargs.get("metadata")
738
- else None,
747
+ "metadata": (
748
+ json.loads(kwargs.get("metadata"))
749
+ if kwargs.get("metadata")
750
+ else None
751
+ ),
739
752
  "tags": kwargs.get("tags"),
740
753
  "id": kwargs.get("id"),
741
754
  "deleted_time": kwargs.get("deleted_time"),
@@ -828,8 +841,9 @@ class BaseDatahub:
828
841
  msg = f"{r['id']} has been registered."
829
842
  logging.info(msg)
830
843
 
831
- if not workspace:
832
- cprint("Enter a valid workspace", "red")
844
+ if not self._valide_workspace(workspace):
845
+ logging.error("Workspace not found")
846
+ cprint("Workspace not found.", "red")
833
847
  return errno.EINVAL
834
848
 
835
849
  results = self._register_bundle(
@@ -856,9 +870,11 @@ class BaseDatahub:
856
870
  "file_type": "bundle",
857
871
  "description": kwargs.get("description"),
858
872
  "aliases": kwargs.get("aliases"),
859
- "metadata": json.loads(kwargs.get("metadata"))
860
- if kwargs.get("metadata")
861
- else None,
873
+ "metadata": (
874
+ json.loads(kwargs.get("metadata"))
875
+ if kwargs.get("metadata")
876
+ else None
877
+ ),
862
878
  "tags": kwargs.get("tags"),
863
879
  "id": kwargs.get("id"),
864
880
  "contents": drs_ids,
@@ -872,7 +888,7 @@ class BaseDatahub:
872
888
 
873
889
  @command(aliases=["deregister"])
874
890
  @argument(
875
- "ids",
891
+ "id",
876
892
  type=List[str],
877
893
  positional=False,
878
894
  description="Specify the IDs of the DRS objects that you want to delete "
@@ -894,14 +910,14 @@ class BaseDatahub:
894
910
  )
895
911
  def deregister(
896
912
  self,
897
- ids: List[str] = [],
913
+ id: List[str] = [],
898
914
  tags: List[str] = [],
899
915
  names: List[str] = [],
900
916
  ) -> int:
901
917
  """
902
918
  Deregister DRS objects by DRS ID, DRS name, or DRS tag.
903
919
  """
904
- if not ids and not names and not tags:
920
+ if not id and not names and not tags:
905
921
  cprint(
906
922
  "Must specify one of the IDs, names or tags to identify DRS objects for deletion",
907
923
  "red",
@@ -909,7 +925,7 @@ class BaseDatahub:
909
925
  return errno.ENOENT
910
926
 
911
927
  resps = asyncio.run(
912
- utils.drs_delete(ids, names, tags, query_opts="?backend_content=false")
928
+ utils.drs_delete(id, names, tags, query_opts="?backend_content=false")
913
929
  )
914
930
  for r in resps:
915
931
  cprint(r, "yellow")
@@ -918,7 +934,7 @@ class BaseDatahub:
918
934
 
919
935
  @command(aliases=["clean"])
920
936
  @argument(
921
- "ids",
937
+ "id",
922
938
  type=List[str],
923
939
  positional=False,
924
940
  description="Specify the IDs of the DRS objects that you want to delete "
@@ -940,14 +956,14 @@ class BaseDatahub:
940
956
  )
941
957
  def delete(
942
958
  self,
943
- ids: List[str] = [],
959
+ id: List[str] = [],
944
960
  tags: List[str] = [],
945
961
  names: List[str] = [],
946
962
  ) -> int:
947
963
  """
948
964
  Delete DRS objects and the associated files in cloud storage by DRS ID, DRS name, or DRS tag.
949
965
  """
950
- if not ids and not names and not tags:
966
+ if not id and not names and not tags:
951
967
  cprint(
952
968
  "Must specify one of the IDs, names or tags to identify "
953
969
  "the associated files in cloud storage of DRS objects for deletion",
@@ -956,8 +972,12 @@ class BaseDatahub:
956
972
  return errno.ENOENT
957
973
 
958
974
  resps = asyncio.run(
959
- utils.drs_delete(ids, names, tags, query_opts="?backend_content=true")
975
+ utils.drs_delete(id, names, tags, query_opts="?backend_content=true")
960
976
  )
977
+ if isinstance(resps, int):
978
+ cprint(f"Delete with return code {resps}", "red")
979
+ return resps
980
+
961
981
  for r in resps:
962
982
  cprint(r, "yellow")
963
983
 
@@ -1074,7 +1094,9 @@ class BaseDatahub:
1074
1094
  fq_paths = BaseDatahub.find_fastq_paths(fastq_path, fastq_signature)
1075
1095
 
1076
1096
  if run_sheet.SampleSheet.is_single_end:
1077
- assert len(fq_paths) == 1
1097
+ assert (
1098
+ len(fq_paths) == 1
1099
+ ), f"{str(fq_paths)} does not contain exactly one fastq path"
1078
1100
  sample_meta["Pair"] = "1"
1079
1101
  upload_info.append(
1080
1102
  {
@@ -1094,7 +1116,9 @@ class BaseDatahub:
1094
1116
  )
1095
1117
 
1096
1118
  if run_sheet.SampleSheet.is_paired_end:
1097
- assert len(fq_paths) == 2
1119
+ assert (
1120
+ len(fq_paths) == 2
1121
+ ), f"{str(fq_paths)} does not contain exactly two fastq paths"
1098
1122
  fq_paths.sort()
1099
1123
  sample_meta["Pair"] = "1"
1100
1124
  upload_info.append(
@@ -1202,24 +1226,15 @@ class BaseDatahub:
1202
1226
  "multiprocessing",
1203
1227
  type=int,
1204
1228
  positional=False,
1205
- description="Specify the number of files that you want to upload at any given time. "
1206
- "Changing the default value is not recommended (optional, default = 1).",
1229
+ description="Specify the number of files that you want to upload at any given time. (optional, default = 1).",
1207
1230
  )
1208
1231
  @argument(
1209
1232
  "chunk_size",
1210
1233
  type=str,
1211
1234
  positional=False,
1212
- description="Specify the size of each file that you want to upload in megabytes (MB). "
1213
- "Changing the default value is not recommended (default = 8MB).",
1235
+ description="Specify the size of each file that you want to upload in mebibyte (MiB). (default = 8MiB).",
1214
1236
  choices=["8", "16"],
1215
1237
  )
1216
- @argument(
1217
- "non_interactive",
1218
- type=bool,
1219
- positional=False,
1220
- description="Whether to run in non-interactive mode, i.e. without prompt (default = False).",
1221
- aliases=["ni"],
1222
- )
1223
1238
  @argument(
1224
1239
  "seq_run_id",
1225
1240
  type=str,
@@ -1237,17 +1252,16 @@ class BaseDatahub:
1237
1252
  concurrency: int = 0,
1238
1253
  multiprocessing: int = 1,
1239
1254
  chunk_size: str = "8",
1240
- expiry_time: str = None,
1241
- non_interactive: bool = False,
1242
1255
  seq_run_id: str = "",
1243
1256
  ) -> int:
1244
1257
  """
1245
1258
  Upload samples based on the Run Sheet and FASTQ path information.
1246
1259
  """
1247
- if not workspace:
1248
- logging.error("Invalid workspace name")
1249
- cprint("Invalid workspace name", "red")
1260
+ if not self._valide_workspace(workspace):
1261
+ logging.error("Workspace not found")
1262
+ cprint("Workspace not found.", "red")
1250
1263
  return errno.EINVAL
1264
+
1251
1265
  failed = False
1252
1266
  if not upload_dst:
1253
1267
  upload_dst = int(datetime.datetime.timestamp(datetime.datetime.now()))
@@ -1282,8 +1296,7 @@ class BaseDatahub:
1282
1296
  concurrency=concurrency,
1283
1297
  chunk_size=self.size[chunk_size],
1284
1298
  proxy=self.proxy,
1285
- non_interactive=non_interactive,
1286
- expiry_time=expiry_time,
1299
+ non_interactive=False,
1287
1300
  )
1288
1301
  if isinstance(result, list):
1289
1302
  r = result[0]
@@ -1375,8 +1388,9 @@ class BaseDatahub:
1375
1388
  """
1376
1389
  Update a DRS object.
1377
1390
  """
1378
- if not workspace:
1379
- cprint("Enter a valid workspace", "red")
1391
+ if not self._valide_workspace(workspace):
1392
+ logging.error("Workspace not found")
1393
+ cprint("Workspace not found.", "red")
1380
1394
  return errno.EINVAL
1381
1395
 
1382
1396
  if stdin:
@@ -1427,8 +1441,9 @@ class BaseDatahub:
1427
1441
  """
1428
1442
  Get a DRS object.
1429
1443
  """
1430
- if not workspace:
1431
- cprint("Enter a valid workspace", "red")
1444
+ if not self._valide_workspace(workspace):
1445
+ logging.error("Workspace not found")
1446
+ cprint("Workspace not found.", "red")
1432
1447
  return errno.EINVAL
1433
1448
 
1434
1449
  def __log(results: dict):
@@ -1448,6 +1463,122 @@ class BaseDatahub:
1448
1463
  api_backend = drs_register().load_register(workspace)
1449
1464
  return api_backend.get_drs(drs_id=id)
1450
1465
 
1466
+ @exception_handler
1467
+ def _patch_add_read_drs(self, add_reads_id: str, workspace: str, tag: str):
1468
+ results = self._get(add_reads_id, workspace)
1469
+ if isinstance(results, int):
1470
+ return results
1471
+ cprint(f"Found Add Reads target DRS object {add_reads_id}", color="yellow")
1472
+ tags = [item["name"] for item in results["tags"]] + [tag]
1473
+ patch_payload = {"tags": tags}
1474
+ results = self._change(add_reads_id, **patch_payload)
1475
+ if isinstance(results, int):
1476
+ return results
1477
+ cprint(
1478
+ f"Add Reads target DRS object {add_reads_id} patched with tags {tags}",
1479
+ color="yellow",
1480
+ )
1481
+ return add_reads_id
1482
+
1483
+ @command(aliases=["add-reads"])
1484
+ @argument(
1485
+ "workspace",
1486
+ type=str,
1487
+ positional=False,
1488
+ description="Specify the workspace based on the signed in account (required).",
1489
+ )
1490
+ @argument(
1491
+ "run_sheet",
1492
+ type=str,
1493
+ positional=False,
1494
+ description="Specify the Run Sheet file path (required).",
1495
+ )
1496
+ @argument(
1497
+ "seq_run_id",
1498
+ type=str,
1499
+ positional=False,
1500
+ description="Specify a runsheet header field as a sequencer run identifier; the specified value will be "
1501
+ "used as a sequencer run specific label for future dataset management (optional).",
1502
+ )
1503
+ def add_reads_runsheet(
1504
+ self,
1505
+ workspace,
1506
+ run_sheet: str,
1507
+ seq_run_id: str = "",
1508
+ ) -> int:
1509
+ """
1510
+ Identify and Patch existing DRS object with add-reads runsheet label for runsheet-based run triggering for
1511
+ DRS auto-matching
1512
+ """
1513
+ if not self._valide_workspace(workspace):
1514
+ logging.error("Workspace not found")
1515
+ cprint("Workspace not found.", "red")
1516
+ return errno.EINVAL
1517
+
1518
+ # SampleSheetV2 based RunSheet SeqsLab fields
1519
+ seqslab_section = "SeqsLabRunSheet"
1520
+ seqslab_format = "SeqsLabColumnFormat"
1521
+ seqslab_sep = "#"
1522
+ rs = RunSheet(run_sheet, seqslab_section, seqslab_format, seqslab_sep)
1523
+
1524
+ # set base tags
1525
+ base_tgs = ""
1526
+ try:
1527
+ if seq_run_id:
1528
+ base_tgs = f"{rs.SampleSheet.Header[seq_run_id]}/"
1529
+ except KeyError:
1530
+ print(
1531
+ f"Given base_label_field not found in SampleSheet Header {seq_run_id}"
1532
+ )
1533
+ return errno.EINVAL
1534
+
1535
+ add_reads = False
1536
+ for sa in rs.SampleSheet.samples:
1537
+ if rs.SampleSheet.is_single_end:
1538
+ assert (
1539
+ sa.to_json().get("Add_Read2_ID") == ""
1540
+ and sa.to_json().get("Add_Read2_Label") == ""
1541
+ ), "columns Add_Read2_ID and Add_Read2_Label should be blank for single_end sequencer run"
1542
+ if (add_read1_id := sa.to_json().get("Add_Read1_ID")) and (
1543
+ add_read1_label := sa.to_json().get("Add_Read1_Label")
1544
+ ):
1545
+ add_reads = True
1546
+ ret = self._patch_add_read_drs(
1547
+ add_read1_id,
1548
+ workspace,
1549
+ f"{base_tgs}{sa.get('Run_Name', rs.SampleSheet.Header.Date)}/"
1550
+ f"{add_read1_label}",
1551
+ )
1552
+ if isinstance(ret, int):
1553
+ return ret
1554
+ if rs.SampleSheet.is_paired_end:
1555
+ if (add_read1_id := sa.to_json().get("Add_Read1_ID")) and (
1556
+ add_read1_label := sa.to_json().get("Add_Read1_Label")
1557
+ ):
1558
+ add_reads = True
1559
+ ret = self._patch_add_read_drs(
1560
+ add_read1_id,
1561
+ workspace,
1562
+ f"{base_tgs}{sa.get('Run_Name', rs.SampleSheet.Header.Date)}/{add_read1_label}",
1563
+ )
1564
+ if isinstance(ret, int):
1565
+ return ret
1566
+ if (add_read2_id := sa.to_json().get("Add_Read2_ID")) and (
1567
+ add_read2_label := sa.to_json().get("Add_Read2_Label")
1568
+ ):
1569
+ add_reads = True
1570
+ ret = self._patch_add_read_drs(
1571
+ add_read2_id,
1572
+ workspace,
1573
+ f"{base_tgs}{sa.get('Run_Name', rs.SampleSheet.Header.Date)}/{add_read2_label}",
1574
+ )
1575
+ if isinstance(ret, int):
1576
+ return ret
1577
+
1578
+ if not add_reads:
1579
+ cprint("No DRS object is patched in this operation", color="yellow")
1580
+ return 0
1581
+
1451
1582
 
1452
1583
  @command
1453
1584
  class Datahub(BaseDatahub):