digitalhub 0.13.0b2__py3-none-any.whl → 0.13.0b3__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.
Files changed (53) hide show
  1. digitalhub/__init__.py +1 -1
  2. digitalhub/context/api.py +5 -5
  3. digitalhub/context/builder.py +3 -5
  4. digitalhub/context/context.py +9 -1
  5. digitalhub/entities/_base/material/entity.py +3 -3
  6. digitalhub/entities/dataitem/crud.py +10 -2
  7. digitalhub/entities/dataitem/table/entity.py +3 -3
  8. digitalhub/entities/dataitem/utils.py +1 -2
  9. digitalhub/entities/task/_base/models.py +12 -3
  10. digitalhub/factory/factory.py +25 -3
  11. digitalhub/factory/utils.py +11 -3
  12. digitalhub/runtimes/_base.py +1 -1
  13. digitalhub/runtimes/builder.py +18 -1
  14. digitalhub/stores/client/__init__.py +12 -0
  15. digitalhub/stores/client/_base/api_builder.py +14 -0
  16. digitalhub/stores/client/_base/client.py +93 -0
  17. digitalhub/stores/client/_base/key_builder.py +28 -0
  18. digitalhub/stores/client/_base/params_builder.py +14 -0
  19. digitalhub/stores/client/api.py +10 -5
  20. digitalhub/stores/client/builder.py +3 -1
  21. digitalhub/stores/client/dhcore/api_builder.py +17 -0
  22. digitalhub/stores/client/dhcore/client.py +276 -58
  23. digitalhub/stores/client/dhcore/configurator.py +336 -141
  24. digitalhub/stores/client/dhcore/error_parser.py +35 -1
  25. digitalhub/stores/client/dhcore/params_builder.py +113 -17
  26. digitalhub/stores/client/dhcore/utils.py +32 -14
  27. digitalhub/stores/client/local/api_builder.py +17 -0
  28. digitalhub/stores/client/local/client.py +6 -8
  29. digitalhub/stores/credentials/api.py +8 -8
  30. digitalhub/stores/credentials/configurator.py +176 -3
  31. digitalhub/stores/credentials/enums.py +16 -3
  32. digitalhub/stores/credentials/handler.py +73 -45
  33. digitalhub/stores/credentials/ini_module.py +59 -27
  34. digitalhub/stores/credentials/store.py +33 -1
  35. digitalhub/stores/data/_base/store.py +8 -3
  36. digitalhub/stores/data/api.py +20 -16
  37. digitalhub/stores/data/builder.py +3 -9
  38. digitalhub/stores/data/s3/configurator.py +64 -23
  39. digitalhub/stores/data/s3/store.py +30 -27
  40. digitalhub/stores/data/s3/utils.py +9 -9
  41. digitalhub/stores/data/sql/configurator.py +23 -22
  42. digitalhub/stores/data/sql/store.py +14 -16
  43. digitalhub/utils/exceptions.py +6 -0
  44. digitalhub/utils/file_utils.py +53 -30
  45. digitalhub/utils/generic_utils.py +41 -33
  46. digitalhub/utils/git_utils.py +24 -14
  47. digitalhub/utils/io_utils.py +19 -18
  48. digitalhub/utils/uri_utils.py +31 -31
  49. {digitalhub-0.13.0b2.dist-info → digitalhub-0.13.0b3.dist-info}/METADATA +1 -1
  50. {digitalhub-0.13.0b2.dist-info → digitalhub-0.13.0b3.dist-info}/RECORD +53 -53
  51. {digitalhub-0.13.0b2.dist-info → digitalhub-0.13.0b3.dist-info}/WHEEL +0 -0
  52. {digitalhub-0.13.0b2.dist-info → digitalhub-0.13.0b3.dist-info}/licenses/AUTHORS +0 -0
  53. {digitalhub-0.13.0b2.dist-info → digitalhub-0.13.0b3.dist-info}/licenses/LICENSE +0 -0
@@ -15,11 +15,10 @@ import botocore.client # pylint: disable=unused-import
15
15
  from boto3.s3.transfer import TransferConfig
16
16
  from botocore.exceptions import ClientError, NoCredentialsError
17
17
 
18
- from digitalhub.stores.credentials.enums import CredsOrigin
19
18
  from digitalhub.stores.data._base.store import Store
20
19
  from digitalhub.stores.data.s3.utils import get_bucket_name
21
20
  from digitalhub.stores.readers.data.api import get_reader_by_object
22
- from digitalhub.utils.exceptions import StoreError
21
+ from digitalhub.utils.exceptions import ConfigError, StoreError
23
22
  from digitalhub.utils.file_utils import get_file_info_from_s3, get_file_mime_type
24
23
  from digitalhub.utils.types import SourcesOrListOfSources
25
24
 
@@ -614,55 +613,59 @@ class S3Store(Store):
614
613
  """
615
614
  return boto3.client("s3", **cfg)
616
615
 
617
- def _check_factory(self, root: str) -> tuple[S3Client, str]:
616
+ def _check_factory(self, s3_path: str, retry: bool = True) -> tuple[S3Client, str]:
618
617
  """
619
- Check if the S3 bucket is accessible by sending a head_bucket request.
618
+ Checks if the S3 bucket collected from the URI is accessible.
619
+
620
+ Parameters
621
+ ----------
622
+ s3_path : str
623
+ Path to the S3 bucket (e.g., 's3://bucket/path').
624
+ retry : bool, optional
625
+ Whether to retry the operation if a ConfigError is raised. Default is True.
620
626
 
621
627
  Returns
622
628
  -------
623
- tuple[S3Client, str]
624
- A tuple containing the S3 client object and the name of the S3 bucket.
625
- """
626
- bucket = self._get_bucket(root)
629
+ tuple of S3Client and str
630
+ Tuple containing the S3 client object and the name of the S3 bucket.
627
631
 
628
- # Try to get client from environment variables
632
+ Raises
633
+ ------
634
+ ConfigError
635
+ If access to the specified bucket is not available and retry is False.
636
+ """
637
+ bucket = self._get_bucket(s3_path)
629
638
  try:
630
- cfg = self._configurator.get_client_config(CredsOrigin.ENV.value)
639
+ cfg = self._configurator.get_client_config()
631
640
  client = self._get_client(cfg)
632
641
  self._check_access_to_storage(client, bucket)
633
-
634
- # Fallback to file
635
- except StoreError:
636
- cfg = self._configurator.get_client_config(CredsOrigin.FILE.value)
637
- client = self._get_client(cfg)
638
- self._check_access_to_storage(client, bucket)
639
-
640
- return client, bucket
642
+ return client, bucket
643
+ except ConfigError as e:
644
+ if retry:
645
+ self._configurator.eval_change_origin()
646
+ return self._check_factory(s3_path, False)
647
+ raise e
641
648
 
642
649
  def _check_access_to_storage(self, client: S3Client, bucket: str) -> None:
643
650
  """
644
- Check if the S3 bucket is accessible by sending a head_bucket request.
651
+ Checks if the S3 bucket is accessible by sending a head_bucket request.
645
652
 
646
653
  Parameters
647
654
  ----------
648
655
  client : S3Client
649
- The S3 client object.
656
+ S3 client object.
650
657
  bucket : str
651
- The name of the S3 bucket.
652
-
653
- Returns
654
- -------
655
- None
658
+ Name of the S3 bucket.
656
659
 
657
660
  Raises
658
661
  ------
659
- ClientError:
662
+ ConfigError
660
663
  If access to the specified bucket is not available.
661
664
  """
662
665
  try:
663
666
  client.head_bucket(Bucket=bucket)
664
667
  except (ClientError, NoCredentialsError) as err:
665
- raise StoreError(f"No access to s3 bucket! Error: {err}")
668
+ raise ConfigError(f"No access to s3 bucket! Error: {err}")
666
669
 
667
670
  @staticmethod
668
671
  def _get_key(path: str) -> str:
@@ -16,34 +16,34 @@ from digitalhub.utils.exceptions import StoreError
16
16
 
17
17
  def get_bucket_name(path: str) -> str:
18
18
  """
19
- Get bucket name from path.
19
+ Extract the bucket name from an S3 path.
20
20
 
21
21
  Parameters
22
22
  ----------
23
23
  path : str
24
- The source path to get the key from.
24
+ S3 URI (e.g., 's3://bucket/key').
25
25
 
26
26
  Returns
27
27
  -------
28
28
  str
29
- The bucket name.
29
+ The bucket name extracted from the URI.
30
30
  """
31
31
  return urlparse(path).netloc
32
32
 
33
33
 
34
34
  def get_bucket_and_key(path: str) -> tuple[str, str]:
35
35
  """
36
- Get bucket and key from path.
36
+ Extract the bucket name and key from an S3 path.
37
37
 
38
38
  Parameters
39
39
  ----------
40
40
  path : str
41
- The source path to get the key from.
41
+ S3 URI (e.g., 's3://bucket/key').
42
42
 
43
43
  Returns
44
44
  -------
45
- tuple[str, str]
46
- The bucket and key.
45
+ tuple of str
46
+ Tuple containing (bucket, key) extracted from the URI.
47
47
  """
48
48
  parsed = urlparse(path)
49
49
  return parsed.netloc, parsed.path
@@ -51,7 +51,7 @@ def get_bucket_and_key(path: str) -> tuple[str, str]:
51
51
 
52
52
  def get_s3_source(bucket: str, key: str, filename: Path) -> None:
53
53
  """
54
- Get S3 source.
54
+ Download an object from S3 and save it to a local file.
55
55
 
56
56
  Parameters
57
57
  ----------
@@ -60,7 +60,7 @@ def get_s3_source(bucket: str, key: str, filename: Path) -> None:
60
60
  key : str
61
61
  S3 object key.
62
62
  filename : Path
63
- Path where to save the function source.
63
+ Local path where the downloaded object will be saved.
64
64
 
65
65
  Returns
66
66
  -------
@@ -15,18 +15,19 @@ class SqlStoreConfigurator(Configurator):
15
15
  """
16
16
 
17
17
  keys = [
18
- CredsEnvVar.DB_USERNAME,
19
- CredsEnvVar.DB_PASSWORD,
20
- CredsEnvVar.DB_HOST,
21
- CredsEnvVar.DB_PORT,
22
- CredsEnvVar.DB_DATABASE,
18
+ CredsEnvVar.DB_USERNAME.value,
19
+ CredsEnvVar.DB_PASSWORD.value,
20
+ CredsEnvVar.DB_HOST.value,
21
+ CredsEnvVar.DB_PORT.value,
22
+ CredsEnvVar.DB_DATABASE.value,
23
+ CredsEnvVar.DB_PLATFORM.value,
23
24
  ]
24
25
  required_keys = [
25
- CredsEnvVar.DB_USERNAME,
26
- CredsEnvVar.DB_PASSWORD,
27
- CredsEnvVar.DB_HOST,
28
- CredsEnvVar.DB_PORT,
29
- CredsEnvVar.DB_DATABASE,
26
+ CredsEnvVar.DB_USERNAME.value,
27
+ CredsEnvVar.DB_PASSWORD.value,
28
+ CredsEnvVar.DB_HOST.value,
29
+ CredsEnvVar.DB_PORT.value,
30
+ CredsEnvVar.DB_DATABASE.value,
30
31
  ]
31
32
 
32
33
  def __init__(self):
@@ -37,30 +38,30 @@ class SqlStoreConfigurator(Configurator):
37
38
  # Configuration methods
38
39
  ##############################
39
40
 
40
- def load_configs(self) -> None:
41
- # Load from env
42
- env_creds = {var.value: self._creds_handler.load_from_env(var.value) for var in self.keys}
41
+ def load_env_vars(self) -> None:
42
+ """
43
+ Load the credentials from the environment.
44
+ """
45
+ env_creds = self._creds_handler.load_from_env(self.keys)
43
46
  self._creds_handler.set_credentials(self._env, env_creds)
44
47
 
45
- # Load from file
46
- file_creds = {var.value: self._creds_handler.load_from_file(var.value) for var in self.keys}
48
+ def load_file_vars(self) -> None:
49
+ """
50
+ Load the credentials from the file.
51
+ """
52
+ file_creds = self._creds_handler.load_from_file(self.keys)
47
53
  self._creds_handler.set_credentials(self._file, file_creds)
48
54
 
49
- def get_sql_conn_string(self, origin: str) -> str:
55
+ def get_sql_conn_string(self) -> str:
50
56
  """
51
57
  Get the connection string from environment variables.
52
58
 
53
- Parameters
54
- ----------
55
- origin : str
56
- The origin of the credentials.
57
-
58
59
  Returns
59
60
  -------
60
61
  str
61
62
  The connection string.
62
63
  """
63
- creds = self.get_credentials(origin)
64
+ creds = self.get_credentials(self._origin)
64
65
  user = creds[CredsEnvVar.DB_USERNAME.value]
65
66
  password = creds[CredsEnvVar.DB_PASSWORD.value]
66
67
  host = creds[CredsEnvVar.DB_HOST.value]
@@ -14,10 +14,9 @@ from sqlalchemy import MetaData, Table, create_engine, select
14
14
  from sqlalchemy.engine import Engine
15
15
  from sqlalchemy.exc import SQLAlchemyError
16
16
 
17
- from digitalhub.stores.credentials.enums import CredsOrigin
18
17
  from digitalhub.stores.data._base.store import Store
19
18
  from digitalhub.stores.readers.data.api import get_reader_by_object
20
- from digitalhub.utils.exceptions import StoreError
19
+ from digitalhub.utils.exceptions import ConfigError, StoreError
21
20
  from digitalhub.utils.types import SourcesOrListOfSources
22
21
 
23
22
  if typing.TYPE_CHECKING:
@@ -287,21 +286,16 @@ class SqlStore(Store):
287
286
  # Helper methods
288
287
  ##############################
289
288
 
290
- def _get_connection_string(self, origin: str) -> str:
289
+ def _get_connection_string(self) -> str:
291
290
  """
292
291
  Get the connection string.
293
292
 
294
- Parameters
295
- ----------
296
- origin : str
297
- The origin of the credentials.
298
-
299
293
  Returns
300
294
  -------
301
295
  str
302
296
  The connection string.
303
297
  """
304
- return self._configurator.get_sql_conn_string(origin)
298
+ return self._configurator.get_sql_conn_string()
305
299
 
306
300
  def _get_engine(self, origin: str, schema: str | None = None) -> Engine:
307
301
  """
@@ -330,12 +324,14 @@ class SqlStore(Store):
330
324
  except Exception as ex:
331
325
  raise StoreError(f"Something wrong with connection string. Arguments: {str(ex.args)}")
332
326
 
333
- def _check_factory(self, schema: str | None = None) -> Engine:
327
+ def _check_factory(self, retry: bool = True, schema: str | None = None) -> Engine:
334
328
  """
335
329
  Check if the database is accessible and return the engine.
336
330
 
337
331
  Parameters
338
332
  ----------
333
+ retry : bool
334
+ Whether to retry if the database is not accessible.
339
335
  schema : str
340
336
  The schema.
341
337
 
@@ -345,12 +341,14 @@ class SqlStore(Store):
345
341
  The database engine.
346
342
  """
347
343
  try:
348
- engine = self._get_engine(CredsOrigin.ENV.value, schema)
349
- self._check_access_to_storage(engine)
350
- except StoreError:
351
- engine = self._get_engine(CredsOrigin.FILE.value, schema)
344
+ engine = self._get_engine(schema)
352
345
  self._check_access_to_storage(engine)
353
- return engine
346
+ return engine
347
+ except ConfigError as e:
348
+ if retry:
349
+ self._configurator.eval_change_origin()
350
+ return self._check_factory(retry=False, schema=schema)
351
+ raise e
354
352
 
355
353
  @staticmethod
356
354
  def _parse_path(path: str) -> dict:
@@ -435,4 +433,4 @@ class SqlStore(Store):
435
433
  engine.connect()
436
434
  except SQLAlchemyError:
437
435
  engine.dispose()
438
- raise StoreError("No access to db!")
436
+ raise ConfigError("No access to db!")
@@ -81,3 +81,9 @@ class ClientError(Exception):
81
81
  """
82
82
  Raised when incontered errors on clients.
83
83
  """
84
+
85
+
86
+ class ConfigError(Exception):
87
+ """
88
+ Raised when incontered errors on configs.
89
+ """
@@ -16,6 +16,21 @@ from pydantic import BaseModel
16
16
  class FileInfo(BaseModel):
17
17
  """
18
18
  File info class.
19
+
20
+ Attributes
21
+ ----------
22
+ path : str or None
23
+ Path to the file.
24
+ name : str or None
25
+ Name of the file.
26
+ content_type : str or None
27
+ MIME type of the file.
28
+ size : int or None
29
+ Size of the file in bytes.
30
+ hash : str or None
31
+ Hash of the file contents.
32
+ last_modified : str or None
33
+ Last modified date/time in ISO format.
19
34
  """
20
35
 
21
36
  path: Optional[str] = None
@@ -25,13 +40,21 @@ class FileInfo(BaseModel):
25
40
  hash: Optional[str] = None
26
41
  last_modified: Optional[str] = None
27
42
 
28
- def to_dict(self):
43
+ def to_dict(self) -> dict:
44
+ """
45
+ Convert FileInfo to dictionary.
46
+
47
+ Returns
48
+ -------
49
+ dict
50
+ Dictionary representation of the FileInfo object.
51
+ """
29
52
  return self.model_dump()
30
53
 
31
54
 
32
55
  def calculate_blob_hash(data_path: str) -> str:
33
56
  """
34
- Calculate the hash of a file.
57
+ Calculate the SHA-256 hash of a file.
35
58
 
36
59
  Parameters
37
60
  ----------
@@ -41,7 +64,7 @@ def calculate_blob_hash(data_path: str) -> str:
41
64
  Returns
42
65
  -------
43
66
  str
44
- The hash of the file.
67
+ The SHA-256 hash of the file, prefixed with 'sha256:'.
45
68
  """
46
69
  with open(data_path, "rb") as f:
47
70
  data = f.read()
@@ -50,7 +73,7 @@ def calculate_blob_hash(data_path: str) -> str:
50
73
 
51
74
  def get_file_size(data_path: str) -> int:
52
75
  """
53
- Get the size of a file.
76
+ Get the size of a file in bytes.
54
77
 
55
78
  Parameters
56
79
  ----------
@@ -60,14 +83,14 @@ def get_file_size(data_path: str) -> int:
60
83
  Returns
61
84
  -------
62
85
  int
63
- The size of the file.
86
+ Size of the file in bytes.
64
87
  """
65
88
  return Path(data_path).stat().st_size
66
89
 
67
90
 
68
91
  def get_file_mime_type(data_path: str) -> str | None:
69
92
  """
70
- Get the mime type of a file.
93
+ Get the MIME type of a file.
71
94
 
72
95
  Parameters
73
96
  ----------
@@ -76,15 +99,15 @@ def get_file_mime_type(data_path: str) -> str | None:
76
99
 
77
100
  Returns
78
101
  -------
79
- str
80
- The mime type of the file.
102
+ str or None
103
+ The MIME type of the file, or None if unknown.
81
104
  """
82
105
  return guess_type(data_path)[0]
83
106
 
84
107
 
85
108
  def get_path_name(data_path: str) -> str:
86
109
  """
87
- Get the name of a file.
110
+ Get the name of a file from its path.
88
111
 
89
112
  Parameters
90
113
  ----------
@@ -101,7 +124,7 @@ def get_path_name(data_path: str) -> str:
101
124
 
102
125
  def get_last_modified(data_path: str) -> str:
103
126
  """
104
- Get the last modified date of a file.
127
+ Get the last modified date/time of a file in ISO format.
105
128
 
106
129
  Parameters
107
130
  ----------
@@ -111,7 +134,7 @@ def get_last_modified(data_path: str) -> str:
111
134
  Returns
112
135
  -------
113
136
  str
114
- The last modified date of the file.
137
+ The last modified date/time in ISO format.
115
138
  """
116
139
  path = Path(data_path)
117
140
  timestamp = path.stat().st_mtime
@@ -120,7 +143,7 @@ def get_last_modified(data_path: str) -> str:
120
143
 
121
144
  def get_s3_path(src_path: str) -> str:
122
145
  """
123
- Get the S3 path of a file.
146
+ Get the S3 URI of a file path.
124
147
 
125
148
  Parameters
126
149
  ----------
@@ -130,26 +153,26 @@ def get_s3_path(src_path: str) -> str:
130
153
  Returns
131
154
  -------
132
155
  str
133
- The S3 path of the file.
156
+ The S3 URI of the file.
134
157
  """
135
158
  return Path(src_path).as_uri()
136
159
 
137
160
 
138
161
  def get_file_info_from_local(path: str, src_path: str) -> None | dict:
139
162
  """
140
- Get file info from path.
163
+ Get file info from a local path.
141
164
 
142
165
  Parameters
143
166
  ----------
144
167
  path : str
145
168
  Target path of the object.
146
169
  src_path : str
147
- Local path of some source.
170
+ Local path of the source file.
148
171
 
149
172
  Returns
150
173
  -------
151
- dict
152
- File info.
174
+ dict or None
175
+ File info dictionary, or None if an error occurs.
153
176
  """
154
177
  try:
155
178
  name = get_path_name(path)
@@ -172,19 +195,19 @@ def get_file_info_from_local(path: str, src_path: str) -> None | dict:
172
195
 
173
196
  def get_file_info_from_s3(path: str, metadata: dict) -> None | dict:
174
197
  """
175
- Get file info from path.
198
+ Get file info from S3 metadata.
176
199
 
177
200
  Parameters
178
201
  ----------
179
202
  path : str
180
203
  Object source path.
181
204
  metadata : dict
182
- Metadata of the object from S3.
205
+ Metadata dictionary of the object from S3.
183
206
 
184
207
  Returns
185
208
  -------
186
- dict
187
- File info.
209
+ dict or None
210
+ File info dictionary, or None if an error occurs.
188
211
  """
189
212
  try:
190
213
  size = metadata["ContentLength"]
@@ -214,17 +237,17 @@ def get_file_info_from_s3(path: str, metadata: dict) -> None | dict:
214
237
 
215
238
  def eval_zip_type(source: str) -> bool:
216
239
  """
217
- Evaluate zip type.
240
+ Evaluate whether the source is a zip file.
218
241
 
219
242
  Parameters
220
243
  ----------
221
244
  source : str
222
- Source.
245
+ Source file path.
223
246
 
224
247
  Returns
225
248
  -------
226
249
  bool
227
- True if path is zip.
250
+ True if the path is a zip file, False otherwise.
228
251
  """
229
252
  extension = source.endswith(".zip")
230
253
  mime_zip = get_file_mime_type(source) == "application/zip"
@@ -233,34 +256,34 @@ def eval_zip_type(source: str) -> bool:
233
256
 
234
257
  def eval_text_type(source: str) -> bool:
235
258
  """
236
- Evaluate text type.
259
+ Evaluate whether the source is a plain text file.
237
260
 
238
261
  Parameters
239
262
  ----------
240
263
  source : str
241
- Source.
264
+ Source file path.
242
265
 
243
266
  Returns
244
267
  -------
245
268
  bool
246
- True if path is text.
269
+ True if the path is a plain text file, False otherwise.
247
270
  """
248
271
  return get_file_mime_type(source) == "text/plain"
249
272
 
250
273
 
251
274
  def eval_py_type(source: str) -> bool:
252
275
  """
253
- Evaluate python type.
276
+ Evaluate whether the source is a Python file.
254
277
 
255
278
  Parameters
256
279
  ----------
257
280
  source : str
258
- Source.
281
+ Source file path.
259
282
 
260
283
  Returns
261
284
  -------
262
285
  bool
263
- True if path is python.
286
+ True if the path is a Python file, False otherwise.
264
287
  """
265
288
  extension = source.endswith(".py")
266
289
  mime_py = get_file_mime_type(source) == "text/x-python"