digitalhub 0.9.2__py3-none-any.whl → 0.10.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.

Potentially problematic release.


This version of digitalhub might be problematic. Click here for more details.

Files changed (121) hide show
  1. digitalhub/__init__.py +2 -3
  2. digitalhub/client/_base/api_builder.py +1 -1
  3. digitalhub/client/_base/client.py +25 -2
  4. digitalhub/client/_base/params_builder.py +16 -0
  5. digitalhub/client/dhcore/api_builder.py +9 -3
  6. digitalhub/client/dhcore/client.py +30 -398
  7. digitalhub/client/dhcore/configurator.py +361 -0
  8. digitalhub/client/dhcore/error_parser.py +107 -0
  9. digitalhub/client/dhcore/models.py +13 -23
  10. digitalhub/client/dhcore/params_builder.py +178 -0
  11. digitalhub/client/dhcore/utils.py +4 -44
  12. digitalhub/client/local/api_builder.py +13 -18
  13. digitalhub/client/local/client.py +18 -2
  14. digitalhub/client/local/enums.py +11 -0
  15. digitalhub/client/local/params_builder.py +116 -0
  16. digitalhub/configurator/api.py +31 -0
  17. digitalhub/configurator/configurator.py +195 -0
  18. digitalhub/configurator/credentials_store.py +65 -0
  19. digitalhub/configurator/ini_module.py +74 -0
  20. digitalhub/entities/_base/_base/entity.py +2 -2
  21. digitalhub/entities/_base/context/entity.py +4 -4
  22. digitalhub/entities/_base/entity/builder.py +5 -5
  23. digitalhub/entities/_base/executable/entity.py +2 -2
  24. digitalhub/entities/_base/material/entity.py +12 -12
  25. digitalhub/entities/_base/material/status.py +1 -1
  26. digitalhub/entities/_base/material/utils.py +2 -2
  27. digitalhub/entities/_base/unversioned/entity.py +2 -2
  28. digitalhub/entities/_base/versioned/entity.py +2 -2
  29. digitalhub/entities/_commons/enums.py +2 -0
  30. digitalhub/entities/_commons/metrics.py +164 -0
  31. digitalhub/entities/_commons/types.py +5 -0
  32. digitalhub/entities/_commons/utils.py +2 -2
  33. digitalhub/entities/_processors/base.py +527 -0
  34. digitalhub/entities/{_operations/processor.py → _processors/context.py} +212 -837
  35. digitalhub/entities/_processors/utils.py +158 -0
  36. digitalhub/entities/artifact/artifact/spec.py +3 -1
  37. digitalhub/entities/artifact/crud.py +13 -12
  38. digitalhub/entities/artifact/utils.py +1 -1
  39. digitalhub/entities/builders.py +6 -18
  40. digitalhub/entities/dataitem/_base/entity.py +0 -41
  41. digitalhub/entities/dataitem/crud.py +27 -15
  42. digitalhub/entities/dataitem/table/entity.py +49 -35
  43. digitalhub/entities/dataitem/table/models.py +4 -3
  44. digitalhub/{utils/data_utils.py → entities/dataitem/table/utils.py} +46 -54
  45. digitalhub/entities/dataitem/utils.py +58 -10
  46. digitalhub/entities/function/crud.py +9 -9
  47. digitalhub/entities/model/_base/entity.py +120 -0
  48. digitalhub/entities/model/_base/spec.py +6 -17
  49. digitalhub/entities/model/_base/status.py +10 -0
  50. digitalhub/entities/model/crud.py +13 -12
  51. digitalhub/entities/model/huggingface/spec.py +9 -4
  52. digitalhub/entities/model/mlflow/models.py +2 -2
  53. digitalhub/entities/model/mlflow/spec.py +7 -7
  54. digitalhub/entities/model/mlflow/utils.py +44 -5
  55. digitalhub/entities/project/_base/entity.py +317 -9
  56. digitalhub/entities/project/_base/spec.py +8 -6
  57. digitalhub/entities/project/crud.py +12 -11
  58. digitalhub/entities/run/_base/entity.py +103 -6
  59. digitalhub/entities/run/_base/spec.py +4 -2
  60. digitalhub/entities/run/_base/status.py +12 -0
  61. digitalhub/entities/run/crud.py +8 -8
  62. digitalhub/entities/secret/_base/entity.py +3 -3
  63. digitalhub/entities/secret/_base/spec.py +4 -2
  64. digitalhub/entities/secret/crud.py +11 -9
  65. digitalhub/entities/task/_base/entity.py +4 -4
  66. digitalhub/entities/task/_base/models.py +51 -40
  67. digitalhub/entities/task/_base/spec.py +2 -0
  68. digitalhub/entities/task/_base/utils.py +2 -2
  69. digitalhub/entities/task/crud.py +12 -8
  70. digitalhub/entities/workflow/crud.py +9 -9
  71. digitalhub/factory/utils.py +9 -9
  72. digitalhub/readers/{_base → data/_base}/builder.py +1 -1
  73. digitalhub/readers/{_base → data/_base}/reader.py +16 -4
  74. digitalhub/readers/{api.py → data/api.py} +2 -2
  75. digitalhub/readers/{factory.py → data/factory.py} +3 -3
  76. digitalhub/readers/{pandas → data/pandas}/builder.py +2 -2
  77. digitalhub/readers/{pandas → data/pandas}/reader.py +110 -30
  78. digitalhub/readers/query/__init__.py +0 -0
  79. digitalhub/stores/_base/store.py +59 -69
  80. digitalhub/stores/api.py +8 -33
  81. digitalhub/stores/builder.py +44 -161
  82. digitalhub/stores/local/store.py +106 -89
  83. digitalhub/stores/remote/store.py +86 -11
  84. digitalhub/stores/s3/configurator.py +108 -0
  85. digitalhub/stores/s3/enums.py +17 -0
  86. digitalhub/stores/s3/models.py +21 -0
  87. digitalhub/stores/s3/store.py +154 -70
  88. digitalhub/{utils/s3_utils.py → stores/s3/utils.py} +7 -3
  89. digitalhub/stores/sql/configurator.py +88 -0
  90. digitalhub/stores/sql/enums.py +16 -0
  91. digitalhub/stores/sql/models.py +24 -0
  92. digitalhub/stores/sql/store.py +106 -85
  93. digitalhub/{readers/_commons → utils}/enums.py +5 -1
  94. digitalhub/utils/exceptions.py +6 -0
  95. digitalhub/utils/file_utils.py +8 -7
  96. digitalhub/utils/generic_utils.py +28 -15
  97. digitalhub/utils/git_utils.py +16 -9
  98. digitalhub/utils/types.py +5 -0
  99. digitalhub/utils/uri_utils.py +2 -2
  100. {digitalhub-0.9.2.dist-info → digitalhub-0.10.0.dist-info}/METADATA +25 -31
  101. {digitalhub-0.9.2.dist-info → digitalhub-0.10.0.dist-info}/RECORD +108 -99
  102. {digitalhub-0.9.2.dist-info → digitalhub-0.10.0.dist-info}/WHEEL +1 -2
  103. digitalhub/client/dhcore/env.py +0 -23
  104. digitalhub/entities/_base/project/entity.py +0 -341
  105. digitalhub-0.9.2.dist-info/top_level.txt +0 -2
  106. test/local/CRUD/test_artifacts.py +0 -96
  107. test/local/CRUD/test_dataitems.py +0 -96
  108. test/local/CRUD/test_models.py +0 -95
  109. test/local/imports/test_imports.py +0 -66
  110. test/local/instances/test_validate.py +0 -55
  111. test/test_crud_functions.py +0 -109
  112. test/test_crud_runs.py +0 -86
  113. test/test_crud_tasks.py +0 -81
  114. test/testkfp.py +0 -37
  115. test/testkfp_pipeline.py +0 -22
  116. /digitalhub/{entities/_base/project → configurator}/__init__.py +0 -0
  117. /digitalhub/entities/{_operations → _processors}/__init__.py +0 -0
  118. /digitalhub/readers/{_base → data}/__init__.py +0 -0
  119. /digitalhub/readers/{_commons → data/_base}/__init__.py +0 -0
  120. /digitalhub/readers/{pandas → data/pandas}/__init__.py +0 -0
  121. {digitalhub-0.9.2.dist-info → digitalhub-0.10.0.dist-info/licenses}/LICENSE.txt +0 -0
@@ -4,19 +4,11 @@ import shutil
4
4
  from pathlib import Path
5
5
  from typing import Any
6
6
 
7
- from digitalhub.readers.api import get_reader_by_object
8
- from digitalhub.stores._base.store import Store, StoreConfig
7
+ from digitalhub.readers.data.api import get_reader_by_object
8
+ from digitalhub.stores._base.store import Store
9
9
  from digitalhub.utils.exceptions import StoreError
10
10
  from digitalhub.utils.file_utils import get_file_info_from_local
11
-
12
-
13
- class LocalStoreConfig(StoreConfig):
14
- """
15
- Local store configuration class.
16
- """
17
-
18
- path: str
19
- """Local path."""
11
+ from digitalhub.utils.types import SourcesOrListOfSources
20
12
 
21
13
 
22
14
  class LocalStore(Store):
@@ -25,9 +17,8 @@ class LocalStore(Store):
25
17
  artifacts on local filesystem based storage.
26
18
  """
27
19
 
28
- def __init__(self, name: str, store_type: str, config: LocalStoreConfig) -> None:
29
- super().__init__(name, store_type)
30
- self.config = config
20
+ def __init__(self, config: dict | None = None) -> None:
21
+ super().__init__()
31
22
 
32
23
  ##############################
33
24
  # I/O methods
@@ -61,7 +52,7 @@ class LocalStore(Store):
61
52
  """
62
53
  raise StoreError("Local store does not support download.")
63
54
 
64
- def upload(self, src: str | list[str], dst: str) -> list[tuple[str, str]]:
55
+ def upload(self, src: SourcesOrListOfSources, dst: str) -> list[tuple[str, str]]:
65
56
  """
66
57
  Upload an artifact to storage.
67
58
 
@@ -92,6 +83,106 @@ class LocalStore(Store):
92
83
  """
93
84
  return [get_file_info_from_local(p) for p in paths]
94
85
 
86
+ ##############################
87
+ # Datastore methods
88
+ ##############################
89
+
90
+ def read_df(
91
+ self,
92
+ path: SourcesOrListOfSources,
93
+ file_format: str | None = None,
94
+ engine: str | None = None,
95
+ **kwargs,
96
+ ) -> Any:
97
+ """
98
+ Read DataFrame from path.
99
+
100
+ Parameters
101
+ ----------
102
+ path : SourcesOrListOfSources
103
+ Path(s) to read DataFrame from.
104
+ file_format : str
105
+ Extension of the file.
106
+ engine : str
107
+ Dataframe engine (pandas, polars, etc.).
108
+ **kwargs : dict
109
+ Keyword arguments.
110
+
111
+ Returns
112
+ -------
113
+ Any
114
+ DataFrame.
115
+ """
116
+ reader = self._get_reader(engine)
117
+
118
+ dfs = []
119
+ if isinstance(path, list):
120
+ for p in path:
121
+ file_format = self._get_extension(file_format, p)
122
+ dfs.append(reader.read_df(p, file_format, **kwargs))
123
+ elif Path(path).is_dir():
124
+ import glob
125
+
126
+ paths = glob.glob(f"{path}/*")
127
+ for p in paths:
128
+ file_format = self._get_extension(file_format, p)
129
+ dfs.append(reader.read_df(p, file_format, **kwargs))
130
+ else:
131
+ file_format = self._get_extension(file_format, path)
132
+ dfs.append(reader.read_df(path, file_format, **kwargs))
133
+
134
+ if len(dfs) == 1:
135
+ return dfs[0]
136
+
137
+ return reader.concat_dfs(dfs)
138
+
139
+ def query(
140
+ self,
141
+ query: str,
142
+ engine: str | None = None,
143
+ ) -> Any:
144
+ """
145
+ Query data from database.
146
+
147
+ Parameters
148
+ ----------
149
+ query : str
150
+ The query to execute.
151
+ engine : str
152
+ Dataframe engine (pandas, polars, etc.).
153
+
154
+ Returns
155
+ -------
156
+ Any
157
+ DataFrame.
158
+ """
159
+ raise StoreError("Local store does not support query.")
160
+
161
+ def write_df(self, df: Any, dst: str, extension: str | None = None, **kwargs) -> str:
162
+ """
163
+ Method to write a dataframe to a file. Kwargs are passed to df.to_parquet().
164
+ If destination is not provided, the dataframe is written to the default
165
+ store path with generated name.
166
+
167
+ Parameters
168
+ ----------
169
+ df : Any
170
+ The dataframe to write.
171
+ dst : str
172
+ The destination of the dataframe.
173
+ **kwargs : dict
174
+ Keyword arguments.
175
+
176
+ Returns
177
+ -------
178
+ str
179
+ Path of written dataframe.
180
+ """
181
+ self._check_local_dst(dst)
182
+ reader = get_reader_by_object(df)
183
+ reader.write_df(df, dst, extension=extension, **kwargs)
184
+ return dst
185
+
95
186
  ##############################
96
187
  # Private I/O methods
97
188
  ##############################
@@ -193,77 +284,3 @@ class LocalStore(Store):
193
284
  dst = dst / src
194
285
  self._build_path(dst)
195
286
  return dst
196
-
197
- ##############################
198
- # Datastore methods
199
- ##############################
200
-
201
- def write_df(self, df: Any, dst: str, extension: str | None = None, **kwargs) -> str:
202
- """
203
- Method to write a dataframe to a file. Kwargs are passed to df.to_parquet().
204
- If destination is not provided, the dataframe is written to the default
205
- store path with generated name.
206
-
207
- Parameters
208
- ----------
209
- df : Any
210
- The dataframe to write.
211
- dst : str
212
- The destination of the dataframe.
213
- **kwargs : dict
214
- Keyword arguments.
215
-
216
- Returns
217
- -------
218
- str
219
- Path of written dataframe.
220
- """
221
- self.store._check_local_dst(dst)
222
- self._validate_extension(Path(dst).suffix.removeprefix("."))
223
-
224
- # Write dataframe
225
- reader = get_reader_by_object(df)
226
- reader.write_df(df, dst, extension=extension, **kwargs)
227
-
228
- return dst
229
-
230
- ##############################
231
- # Helper methods
232
- ##############################
233
-
234
- @staticmethod
235
- def is_partition_or_dir(path: str) -> bool:
236
- """
237
- Check if path is a directory or a partition.
238
-
239
- Parameters
240
- ----------
241
- path : str
242
- The path to check.
243
-
244
- Returns
245
- -------
246
- bool
247
- """
248
- return Path(path).is_dir()
249
-
250
- @staticmethod
251
- def build_object_path(root: str, paths: str | list[str]) -> list[str]:
252
- """
253
- Method to build object path.
254
-
255
- Parameters
256
- ----------
257
- root : str
258
- The root of the object path.
259
- paths : str | list[str]
260
- The path to build.
261
-
262
- Returns
263
- -------
264
- list[str]
265
- Returns the path of the object.
266
- """
267
- if isinstance(paths, str):
268
- paths = [paths]
269
- return [str(Path(root) / path) for path in paths]
@@ -5,14 +5,9 @@ from typing import Any
5
5
 
6
6
  import requests
7
7
 
8
- from digitalhub.stores._base.store import Store, StoreConfig
8
+ from digitalhub.stores._base.store import Store
9
9
  from digitalhub.utils.exceptions import StoreError
10
-
11
-
12
- class RemoteStoreConfig(StoreConfig):
13
- """
14
- Remote store configuration class.
15
- """
10
+ from digitalhub.utils.types import SourcesOrListOfSources
16
11
 
17
12
 
18
13
  class RemoteStore(Store):
@@ -21,9 +16,8 @@ class RemoteStore(Store):
21
16
  artifacts from remote HTTP based storage.
22
17
  """
23
18
 
24
- def __init__(self, name: str, store_type: str, config: RemoteStoreConfig) -> None:
25
- super().__init__(name, store_type)
26
- self.config = config
19
+ def __init__(self, config: dict | None = None) -> None:
20
+ super().__init__()
27
21
 
28
22
  ##############################
29
23
  # I/O methods
@@ -69,7 +63,7 @@ class RemoteStore(Store):
69
63
 
70
64
  return self._download_file(root, dst, overwrite)
71
65
 
72
- def upload(self, src: str | list[str], dst: str) -> list[tuple[str, str]]:
66
+ def upload(self, src: SourcesOrListOfSources, dst: str) -> list[tuple[str, str]]:
73
67
  """
74
68
  Upload an artifact to storage.
75
69
 
@@ -104,6 +98,58 @@ class RemoteStore(Store):
104
98
  # Datastore methods
105
99
  ##############################
106
100
 
101
+ def read_df(
102
+ self,
103
+ path: SourcesOrListOfSources,
104
+ file_format: str | None = None,
105
+ engine: str | None = None,
106
+ **kwargs,
107
+ ) -> Any:
108
+ """
109
+ Read DataFrame from path.
110
+
111
+ Parameters
112
+ ----------
113
+ path : SourcesOrListOfSources
114
+ Path(s) to read DataFrame from.
115
+ file_format : str
116
+ Extension of the file.
117
+ engine : str
118
+ Dataframe engine (pandas, polars, etc.).
119
+ **kwargs : dict
120
+ Keyword arguments.
121
+
122
+ Returns
123
+ -------
124
+ Any
125
+ DataFrame.
126
+ """
127
+ reader = self._get_reader(engine)
128
+ extension = self._head_extension(path, file_format)
129
+ return reader.read_df(path, extension, **kwargs)
130
+
131
+ def query(
132
+ self,
133
+ query: str,
134
+ engine: str | None = None,
135
+ ) -> Any:
136
+ """
137
+ Query data from database.
138
+
139
+ Parameters
140
+ ----------
141
+ query : str
142
+ The query to execute.
143
+ engine : str
144
+ Dataframe engine (pandas, polars, etc.).
145
+
146
+ Returns
147
+ -------
148
+ Any
149
+ DataFrame.
150
+ """
151
+ raise StoreError("Remote store does not support query.")
152
+
107
153
  def write_df(self, df: Any, dst: str, extension: str | None = None, **kwargs) -> str:
108
154
  """
109
155
  Method to write a dataframe to a file. Note that this method is not implemented
@@ -167,3 +213,32 @@ class RemoteStore(Store):
167
213
  for chunk in r.iter_content(chunk_size=8192):
168
214
  f.write(chunk)
169
215
  return str(dst)
216
+
217
+ def _head_extension(self, url: str, file_format: str | None = None) -> str:
218
+ """
219
+ Method to get the extension of a file from a given url.
220
+
221
+ Parameters
222
+ ----------
223
+ url : str
224
+ The url of the file to get the extension.
225
+ file_format : str
226
+ The file format to check.
227
+
228
+ Returns
229
+ -------
230
+ str
231
+ File extension.
232
+ """
233
+ if file_format is not None:
234
+ return file_format
235
+ try:
236
+ r = requests.head(url, timeout=60)
237
+ r.raise_for_status()
238
+ content_type = r.headers["content-type"]
239
+ if "text" in content_type:
240
+ return "csv"
241
+ else:
242
+ raise ValueError("Content type not supported.")
243
+ except Exception as e:
244
+ raise e
@@ -0,0 +1,108 @@
1
+ from __future__ import annotations
2
+
3
+ from botocore.config import Config
4
+
5
+ from digitalhub.configurator.configurator import configurator
6
+ from digitalhub.stores.s3.enums import S3StoreEnv
7
+ from digitalhub.stores.s3.models import S3StoreConfig
8
+ from digitalhub.utils.exceptions import StoreError
9
+
10
+
11
+ class S3StoreConfigurator:
12
+ """
13
+ Configure the store by getting the credentials from user
14
+ provided config or from environment.
15
+ """
16
+
17
+ def __init__(self, config: dict | None = None) -> None:
18
+ self.configure(config)
19
+
20
+ ##############################
21
+ # Configuration methods
22
+ ##############################
23
+
24
+ def configure(self, config: dict | None = None) -> None:
25
+ """
26
+ Configure the store by getting the credentials from user
27
+ provided config or from environment.
28
+
29
+ Parameters
30
+ ----------
31
+ config : dict
32
+ Configuration dictionary.
33
+
34
+ Returns
35
+ -------
36
+ None
37
+ """
38
+ # Validate config
39
+ if config is None:
40
+ self._get_config()
41
+ else:
42
+ config = S3StoreConfig(**config)
43
+ for pair in [
44
+ (S3StoreEnv.ENDPOINT_URL.value, config.endpoint),
45
+ (S3StoreEnv.ACCESS_KEY_ID.value, config.access_key),
46
+ (S3StoreEnv.SECRET_ACCESS_KEY.value, config.secret_key),
47
+ (S3StoreEnv.SESSION_TOKEN.value, config.session_token),
48
+ (S3StoreEnv.BUCKET_NAME.value, config.bucket_name),
49
+ (S3StoreEnv.REGION.value, config.region),
50
+ (S3StoreEnv.SIGNATURE_VERSION.value, config.signature_version),
51
+ ]:
52
+ configurator.set_credential(*pair)
53
+
54
+ def get_boto3_client_config(self) -> dict:
55
+ """
56
+ Get S3 credentials (access key, secret key,
57
+ session token and other config).
58
+
59
+ Returns
60
+ -------
61
+ dict
62
+ The credentials.
63
+ """
64
+ creds = configurator.get_all_credentials()
65
+ try:
66
+ return {
67
+ "endpoint_url": creds[S3StoreEnv.ENDPOINT_URL.value],
68
+ "aws_access_key_id": creds[S3StoreEnv.ACCESS_KEY_ID.value],
69
+ "aws_secret_access_key": creds[S3StoreEnv.SECRET_ACCESS_KEY.value],
70
+ "aws_session_token": creds[S3StoreEnv.SESSION_TOKEN.value],
71
+ "config": Config(
72
+ region_name=creds[S3StoreEnv.REGION.value],
73
+ signature_version=creds[S3StoreEnv.SIGNATURE_VERSION.value],
74
+ ),
75
+ }
76
+ except KeyError as e:
77
+ raise StoreError(f"Missing credentials for S3 store. {str(e)}")
78
+
79
+ def _get_config(self) -> None:
80
+ """
81
+ Get the store configuration.
82
+
83
+ Returns
84
+ -------
85
+ None
86
+ """
87
+ required_vars = [
88
+ S3StoreEnv.ENDPOINT_URL,
89
+ S3StoreEnv.ACCESS_KEY_ID,
90
+ S3StoreEnv.SECRET_ACCESS_KEY,
91
+ S3StoreEnv.BUCKET_NAME,
92
+ ]
93
+ optional_vars = [S3StoreEnv.REGION, S3StoreEnv.SIGNATURE_VERSION, S3StoreEnv.SESSION_TOKEN]
94
+
95
+ # Load required environment variables
96
+ credentials = {var.value: configurator.load_var(var.value) for var in required_vars}
97
+
98
+ # Check for missing required credentials
99
+ missing_vars = [key for key, value in credentials.items() if value is None]
100
+ if missing_vars:
101
+ raise StoreError(f"Missing credentials for S3 store: {', '.join(missing_vars)}")
102
+
103
+ # Load optional environment variables
104
+ credentials.update({var.value: configurator.load_var(var.value) for var in optional_vars})
105
+
106
+ # Set credentials
107
+ for key, value in credentials.items():
108
+ configurator.set_credential(key, value)
@@ -0,0 +1,17 @@
1
+ from __future__ import annotations
2
+
3
+ from enum import Enum
4
+
5
+
6
+ class S3StoreEnv(Enum):
7
+ """
8
+ S3Store environment
9
+ """
10
+
11
+ ENDPOINT_URL = "S3_ENDPOINT_URL"
12
+ ACCESS_KEY_ID = "AWS_ACCESS_KEY_ID"
13
+ SECRET_ACCESS_KEY = "AWS_SECRET_ACCESS_KEY"
14
+ SESSION_TOKEN = "AWS_SESSION_TOKEN"
15
+ BUCKET_NAME = "S3_BUCKET"
16
+ REGION = "S3_REGION"
17
+ SIGNATURE_VERSION = "S3_SIGNATURE_VERSION"
@@ -0,0 +1,21 @@
1
+ from __future__ import annotations
2
+
3
+ from pydantic import BaseModel
4
+
5
+
6
+ class S3StoreConfig(BaseModel):
7
+ """
8
+ S3 store configuration class.
9
+ """
10
+
11
+ endpoint_url: str
12
+ """S3 endpoint URL."""
13
+
14
+ aws_access_key_id: str
15
+ """AWS access key ID."""
16
+
17
+ aws_secret_access_key: str
18
+ """AWS secret access key."""
19
+
20
+ bucket_name: str
21
+ """S3 bucket name."""