digitalhub 0.7.0b2__py3-none-any.whl → 0.8.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.
- digitalhub/__init__.py +63 -93
- digitalhub/client/__init__.py +0 -0
- digitalhub/client/_base/__init__.py +0 -0
- digitalhub/client/_base/client.py +56 -0
- digitalhub/client/api.py +63 -0
- digitalhub/client/builder.py +50 -0
- digitalhub/client/dhcore/__init__.py +0 -0
- digitalhub/client/dhcore/client.py +669 -0
- digitalhub/client/dhcore/env.py +21 -0
- digitalhub/client/dhcore/models.py +46 -0
- digitalhub/client/dhcore/utils.py +111 -0
- digitalhub/client/local/__init__.py +0 -0
- digitalhub/client/local/client.py +533 -0
- digitalhub/context/__init__.py +0 -0
- digitalhub/context/api.py +93 -0
- digitalhub/context/builder.py +94 -0
- digitalhub/context/context.py +136 -0
- digitalhub/datastores/__init__.py +0 -0
- digitalhub/datastores/_base/__init__.py +0 -0
- digitalhub/datastores/_base/datastore.py +85 -0
- digitalhub/datastores/api.py +37 -0
- digitalhub/datastores/builder.py +110 -0
- digitalhub/datastores/local/__init__.py +0 -0
- digitalhub/datastores/local/datastore.py +50 -0
- digitalhub/datastores/remote/__init__.py +0 -0
- digitalhub/datastores/remote/datastore.py +31 -0
- digitalhub/datastores/s3/__init__.py +0 -0
- digitalhub/datastores/s3/datastore.py +46 -0
- digitalhub/datastores/sql/__init__.py +0 -0
- digitalhub/datastores/sql/datastore.py +68 -0
- digitalhub/entities/__init__.py +0 -0
- digitalhub/entities/_base/__init__.py +0 -0
- digitalhub/entities/_base/_base/__init__.py +0 -0
- digitalhub/entities/_base/_base/entity.py +82 -0
- digitalhub/entities/_base/api_utils.py +620 -0
- digitalhub/entities/_base/context/__init__.py +0 -0
- digitalhub/entities/_base/context/entity.py +118 -0
- digitalhub/entities/_base/crud.py +468 -0
- digitalhub/entities/_base/entity/__init__.py +0 -0
- digitalhub/entities/_base/entity/_constructors/__init__.py +0 -0
- digitalhub/entities/_base/entity/_constructors/metadata.py +44 -0
- digitalhub/entities/_base/entity/_constructors/name.py +31 -0
- digitalhub/entities/_base/entity/_constructors/spec.py +33 -0
- digitalhub/entities/_base/entity/_constructors/status.py +52 -0
- digitalhub/entities/_base/entity/_constructors/uuid.py +26 -0
- digitalhub/entities/_base/entity/builder.py +175 -0
- digitalhub/entities/_base/entity/entity.py +106 -0
- digitalhub/entities/_base/entity/metadata.py +59 -0
- digitalhub/entities/_base/entity/spec.py +58 -0
- digitalhub/entities/_base/entity/status.py +43 -0
- digitalhub/entities/_base/executable/__init__.py +0 -0
- digitalhub/entities/_base/executable/entity.py +405 -0
- digitalhub/entities/_base/material/__init__.py +0 -0
- digitalhub/entities/_base/material/entity.py +214 -0
- digitalhub/entities/_base/material/spec.py +22 -0
- digitalhub/entities/_base/material/status.py +49 -0
- digitalhub/entities/_base/runtime_entity/__init__.py +0 -0
- digitalhub/entities/_base/runtime_entity/builder.py +106 -0
- digitalhub/entities/_base/unversioned/__init__.py +0 -0
- digitalhub/entities/_base/unversioned/builder.py +66 -0
- digitalhub/entities/_base/unversioned/entity.py +49 -0
- digitalhub/entities/_base/versioned/__init__.py +0 -0
- digitalhub/entities/_base/versioned/builder.py +68 -0
- digitalhub/entities/_base/versioned/entity.py +53 -0
- digitalhub/entities/artifact/__init__.py +0 -0
- digitalhub/entities/artifact/_base/__init__.py +0 -0
- digitalhub/entities/artifact/_base/builder.py +86 -0
- digitalhub/entities/artifact/_base/entity.py +39 -0
- digitalhub/entities/artifact/_base/spec.py +15 -0
- digitalhub/entities/artifact/_base/status.py +9 -0
- digitalhub/entities/artifact/artifact/__init__.py +0 -0
- digitalhub/entities/artifact/artifact/builder.py +18 -0
- digitalhub/entities/artifact/artifact/entity.py +32 -0
- digitalhub/entities/artifact/artifact/spec.py +27 -0
- digitalhub/entities/artifact/artifact/status.py +15 -0
- digitalhub/entities/artifact/crud.py +332 -0
- digitalhub/entities/builders.py +63 -0
- digitalhub/entities/dataitem/__init__.py +0 -0
- digitalhub/entities/dataitem/_base/__init__.py +0 -0
- digitalhub/entities/dataitem/_base/builder.py +86 -0
- digitalhub/entities/dataitem/_base/entity.py +75 -0
- digitalhub/entities/dataitem/_base/spec.py +15 -0
- digitalhub/entities/dataitem/_base/status.py +20 -0
- digitalhub/entities/dataitem/crud.py +372 -0
- digitalhub/entities/dataitem/dataitem/__init__.py +0 -0
- digitalhub/entities/dataitem/dataitem/builder.py +18 -0
- digitalhub/entities/dataitem/dataitem/entity.py +32 -0
- digitalhub/entities/dataitem/dataitem/spec.py +15 -0
- digitalhub/entities/dataitem/dataitem/status.py +9 -0
- digitalhub/entities/dataitem/iceberg/__init__.py +0 -0
- digitalhub/entities/dataitem/iceberg/builder.py +18 -0
- digitalhub/entities/dataitem/iceberg/entity.py +32 -0
- digitalhub/entities/dataitem/iceberg/spec.py +15 -0
- digitalhub/entities/dataitem/iceberg/status.py +9 -0
- digitalhub/entities/dataitem/table/__init__.py +0 -0
- digitalhub/entities/dataitem/table/builder.py +18 -0
- digitalhub/entities/dataitem/table/entity.py +146 -0
- digitalhub/entities/dataitem/table/models.py +62 -0
- digitalhub/entities/dataitem/table/spec.py +25 -0
- digitalhub/entities/dataitem/table/status.py +9 -0
- digitalhub/entities/function/__init__.py +0 -0
- digitalhub/entities/function/_base/__init__.py +0 -0
- digitalhub/entities/function/_base/builder.py +79 -0
- digitalhub/entities/function/_base/entity.py +98 -0
- digitalhub/entities/function/_base/models.py +118 -0
- digitalhub/entities/function/_base/spec.py +15 -0
- digitalhub/entities/function/_base/status.py +9 -0
- digitalhub/entities/function/crud.py +279 -0
- digitalhub/entities/model/__init__.py +0 -0
- digitalhub/entities/model/_base/__init__.py +0 -0
- digitalhub/entities/model/_base/builder.py +86 -0
- digitalhub/entities/model/_base/entity.py +34 -0
- digitalhub/entities/model/_base/spec.py +49 -0
- digitalhub/entities/model/_base/status.py +9 -0
- digitalhub/entities/model/crud.py +331 -0
- digitalhub/entities/model/huggingface/__init__.py +0 -0
- digitalhub/entities/model/huggingface/builder.py +18 -0
- digitalhub/entities/model/huggingface/entity.py +32 -0
- digitalhub/entities/model/huggingface/spec.py +36 -0
- digitalhub/entities/model/huggingface/status.py +9 -0
- digitalhub/entities/model/mlflow/__init__.py +0 -0
- digitalhub/entities/model/mlflow/builder.py +18 -0
- digitalhub/entities/model/mlflow/entity.py +32 -0
- digitalhub/entities/model/mlflow/models.py +26 -0
- digitalhub/entities/model/mlflow/spec.py +44 -0
- digitalhub/entities/model/mlflow/status.py +9 -0
- digitalhub/entities/model/mlflow/utils.py +81 -0
- digitalhub/entities/model/model/__init__.py +0 -0
- digitalhub/entities/model/model/builder.py +18 -0
- digitalhub/entities/model/model/entity.py +32 -0
- digitalhub/entities/model/model/spec.py +15 -0
- digitalhub/entities/model/model/status.py +9 -0
- digitalhub/entities/model/sklearn/__init__.py +0 -0
- digitalhub/entities/model/sklearn/builder.py +18 -0
- digitalhub/entities/model/sklearn/entity.py +32 -0
- digitalhub/entities/model/sklearn/spec.py +15 -0
- digitalhub/entities/model/sklearn/status.py +9 -0
- digitalhub/entities/project/__init__.py +0 -0
- digitalhub/entities/project/_base/__init__.py +0 -0
- digitalhub/entities/project/_base/builder.py +128 -0
- digitalhub/entities/project/_base/entity.py +2078 -0
- digitalhub/entities/project/_base/spec.py +50 -0
- digitalhub/entities/project/_base/status.py +9 -0
- digitalhub/entities/project/crud.py +357 -0
- digitalhub/entities/run/__init__.py +0 -0
- digitalhub/entities/run/_base/__init__.py +0 -0
- digitalhub/entities/run/_base/builder.py +94 -0
- digitalhub/entities/run/_base/entity.py +307 -0
- digitalhub/entities/run/_base/spec.py +50 -0
- digitalhub/entities/run/_base/status.py +9 -0
- digitalhub/entities/run/crud.py +219 -0
- digitalhub/entities/secret/__init__.py +0 -0
- digitalhub/entities/secret/_base/__init__.py +0 -0
- digitalhub/entities/secret/_base/builder.py +81 -0
- digitalhub/entities/secret/_base/entity.py +74 -0
- digitalhub/entities/secret/_base/spec.py +35 -0
- digitalhub/entities/secret/_base/status.py +9 -0
- digitalhub/entities/secret/crud.py +290 -0
- digitalhub/entities/task/__init__.py +0 -0
- digitalhub/entities/task/_base/__init__.py +0 -0
- digitalhub/entities/task/_base/builder.py +91 -0
- digitalhub/entities/task/_base/entity.py +136 -0
- digitalhub/entities/task/_base/models.py +208 -0
- digitalhub/entities/task/_base/spec.py +53 -0
- digitalhub/entities/task/_base/status.py +9 -0
- digitalhub/entities/task/crud.py +228 -0
- digitalhub/entities/utils/__init__.py +0 -0
- digitalhub/entities/utils/api.py +346 -0
- digitalhub/entities/utils/entity_types.py +19 -0
- digitalhub/entities/utils/state.py +31 -0
- digitalhub/entities/utils/utils.py +202 -0
- digitalhub/entities/workflow/__init__.py +0 -0
- digitalhub/entities/workflow/_base/__init__.py +0 -0
- digitalhub/entities/workflow/_base/builder.py +79 -0
- digitalhub/entities/workflow/_base/entity.py +74 -0
- digitalhub/entities/workflow/_base/spec.py +15 -0
- digitalhub/entities/workflow/_base/status.py +9 -0
- digitalhub/entities/workflow/crud.py +278 -0
- digitalhub/factory/__init__.py +0 -0
- digitalhub/factory/api.py +277 -0
- digitalhub/factory/factory.py +268 -0
- digitalhub/factory/utils.py +90 -0
- digitalhub/readers/__init__.py +0 -0
- digitalhub/readers/_base/__init__.py +0 -0
- digitalhub/readers/_base/builder.py +26 -0
- digitalhub/readers/_base/reader.py +70 -0
- digitalhub/readers/api.py +80 -0
- digitalhub/readers/factory.py +133 -0
- digitalhub/readers/pandas/__init__.py +0 -0
- digitalhub/readers/pandas/builder.py +29 -0
- digitalhub/readers/pandas/reader.py +207 -0
- digitalhub/runtimes/__init__.py +0 -0
- digitalhub/runtimes/_base.py +102 -0
- digitalhub/runtimes/builder.py +32 -0
- digitalhub/stores/__init__.py +0 -0
- digitalhub/stores/_base/__init__.py +0 -0
- digitalhub/stores/_base/store.py +189 -0
- digitalhub/stores/api.py +54 -0
- digitalhub/stores/builder.py +211 -0
- digitalhub/stores/local/__init__.py +0 -0
- digitalhub/stores/local/store.py +230 -0
- digitalhub/stores/remote/__init__.py +0 -0
- digitalhub/stores/remote/store.py +143 -0
- digitalhub/stores/s3/__init__.py +0 -0
- digitalhub/stores/s3/store.py +563 -0
- digitalhub/stores/sql/__init__.py +0 -0
- digitalhub/stores/sql/store.py +328 -0
- digitalhub/utils/__init__.py +0 -0
- digitalhub/utils/data_utils.py +127 -0
- digitalhub/utils/exceptions.py +67 -0
- digitalhub/utils/file_utils.py +204 -0
- digitalhub/utils/generic_utils.py +183 -0
- digitalhub/utils/git_utils.py +148 -0
- digitalhub/utils/io_utils.py +116 -0
- digitalhub/utils/logger.py +17 -0
- digitalhub/utils/s3_utils.py +58 -0
- digitalhub/utils/uri_utils.py +56 -0
- {digitalhub-0.7.0b2.dist-info → digitalhub-0.8.0.dist-info}/METADATA +30 -13
- digitalhub-0.8.0.dist-info/RECORD +231 -0
- {digitalhub-0.7.0b2.dist-info → digitalhub-0.8.0.dist-info}/WHEEL +1 -1
- test/local/CRUD/test_artifacts.py +96 -0
- test/local/CRUD/test_dataitems.py +96 -0
- test/local/CRUD/test_models.py +95 -0
- test/test_crud_functions.py +1 -1
- test/test_crud_runs.py +1 -1
- test/test_crud_tasks.py +1 -1
- digitalhub-0.7.0b2.dist-info/RECORD +0 -14
- test/test_crud_artifacts.py +0 -96
- test/test_crud_dataitems.py +0 -96
- {digitalhub-0.7.0b2.dist-info → digitalhub-0.8.0.dist-info}/LICENSE.txt +0 -0
- {digitalhub-0.7.0b2.dist-info → digitalhub-0.8.0.dist-info}/top_level.txt +0 -0
- /test/{test_imports.py → local/imports/test_imports.py} +0 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
from digitalhub.client.dhcore.env import FALLBACK_USER
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AuthConfig(BaseModel):
|
|
9
|
+
"""Client configuration model."""
|
|
10
|
+
|
|
11
|
+
user: str = FALLBACK_USER
|
|
12
|
+
"""Username."""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BasicAuth(AuthConfig):
|
|
16
|
+
"""Basic authentication model."""
|
|
17
|
+
|
|
18
|
+
password: str
|
|
19
|
+
"""Basic authentication password."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ClientParams(AuthConfig):
|
|
23
|
+
"""Client id authentication model."""
|
|
24
|
+
|
|
25
|
+
client_id: str = None
|
|
26
|
+
"""OAuth2 client id."""
|
|
27
|
+
|
|
28
|
+
client_scecret: str = None
|
|
29
|
+
"""OAuth2 client secret."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class OAuth2TokenAuth(ClientParams):
|
|
33
|
+
"""OAuth2 token authentication model."""
|
|
34
|
+
|
|
35
|
+
access_token: str
|
|
36
|
+
"""OAuth2 token."""
|
|
37
|
+
|
|
38
|
+
refresh_token: str = None
|
|
39
|
+
"""OAuth2 refresh token."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class TokenExchangeAuth(ClientParams):
|
|
43
|
+
"""Token exchange authentication model."""
|
|
44
|
+
|
|
45
|
+
exchange_token: str
|
|
46
|
+
"""Exchange token."""
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import typing
|
|
5
|
+
|
|
6
|
+
from digitalhub.client.api import check_client_exists, get_client
|
|
7
|
+
|
|
8
|
+
if typing.TYPE_CHECKING:
|
|
9
|
+
from digitalhub.client.dhcore.client import ClientDHCore
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def set_dhcore_env(
|
|
13
|
+
endpoint: str | None = None,
|
|
14
|
+
user: str | None = None,
|
|
15
|
+
password: str | None = None,
|
|
16
|
+
access_token: str | None = None,
|
|
17
|
+
refresh_token: str | None = None,
|
|
18
|
+
client_id: str | None = None,
|
|
19
|
+
) -> None:
|
|
20
|
+
"""
|
|
21
|
+
Function to set environment variables for DHCore config.
|
|
22
|
+
Note that if the environment variable is already set, it
|
|
23
|
+
will be overwritten. It also ovverides the remote client
|
|
24
|
+
configuration.
|
|
25
|
+
|
|
26
|
+
Parameters
|
|
27
|
+
----------
|
|
28
|
+
endpoint : str
|
|
29
|
+
The endpoint of DHCore.
|
|
30
|
+
user : str
|
|
31
|
+
The user of DHCore.
|
|
32
|
+
password : str
|
|
33
|
+
The password of DHCore.
|
|
34
|
+
access_token : str
|
|
35
|
+
The access token of DHCore.
|
|
36
|
+
refresh_token : str
|
|
37
|
+
The refresh token of DHCore.
|
|
38
|
+
client_id : str
|
|
39
|
+
The client id of DHCore.
|
|
40
|
+
|
|
41
|
+
Returns
|
|
42
|
+
-------
|
|
43
|
+
None
|
|
44
|
+
"""
|
|
45
|
+
if endpoint is not None:
|
|
46
|
+
os.environ["DHCORE_ENDPOINT"] = endpoint
|
|
47
|
+
if user is not None:
|
|
48
|
+
os.environ["DHCORE_USER"] = user
|
|
49
|
+
if password is not None:
|
|
50
|
+
os.environ["DHCORE_PASSWORD"] = password
|
|
51
|
+
if access_token is not None:
|
|
52
|
+
os.environ["DHCORE_ACCESS_TOKEN"] = access_token
|
|
53
|
+
if refresh_token is not None:
|
|
54
|
+
os.environ["DHCORE_REFRESH_TOKEN"] = refresh_token
|
|
55
|
+
if client_id is not None:
|
|
56
|
+
os.environ["DHCORE_CLIENT_ID"] = client_id
|
|
57
|
+
|
|
58
|
+
if check_client_exists(local=False):
|
|
59
|
+
update_client_from_env()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def update_client_from_env() -> None:
|
|
63
|
+
"""
|
|
64
|
+
Function to update client from environment variables.
|
|
65
|
+
|
|
66
|
+
Returns
|
|
67
|
+
-------
|
|
68
|
+
None
|
|
69
|
+
"""
|
|
70
|
+
client: ClientDHCore = get_client(local=False)
|
|
71
|
+
|
|
72
|
+
# Update endpoint
|
|
73
|
+
endpoint = os.getenv("DHCORE_ENDPOINT")
|
|
74
|
+
if endpoint is not None:
|
|
75
|
+
client._endpoint_core = endpoint
|
|
76
|
+
|
|
77
|
+
# Update auth
|
|
78
|
+
|
|
79
|
+
# If token is set, it will override the other auth options
|
|
80
|
+
access_token = os.getenv("DHCORE_ACCESS_TOKEN")
|
|
81
|
+
refresh_token = os.getenv("DHCORE_REFRESH_TOKEN")
|
|
82
|
+
client_id = os.getenv("DHCORE_CLIENT_ID")
|
|
83
|
+
|
|
84
|
+
if access_token is not None:
|
|
85
|
+
if refresh_token is not None:
|
|
86
|
+
client._refresh_token = refresh_token
|
|
87
|
+
if client_id is not None:
|
|
88
|
+
client._client_id = client_id
|
|
89
|
+
client._access_token = access_token
|
|
90
|
+
client._auth_type = "oauth2"
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
# Otherwise, if user and password are set, basic auth will be used
|
|
94
|
+
username = os.getenv("DHCORE_USER")
|
|
95
|
+
password = os.getenv("DHCORE_PASSWORD")
|
|
96
|
+
if username is not None and password is not None:
|
|
97
|
+
client._user = username
|
|
98
|
+
client._password = password
|
|
99
|
+
client._auth_type = "basic"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def refresh_token() -> None:
|
|
103
|
+
"""
|
|
104
|
+
Function to refresh token.
|
|
105
|
+
|
|
106
|
+
Returns
|
|
107
|
+
-------
|
|
108
|
+
None
|
|
109
|
+
"""
|
|
110
|
+
client: ClientDHCore = get_client(local=False)
|
|
111
|
+
client._get_new_access_token()
|
|
File without changes
|
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from copy import deepcopy
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
|
|
6
|
+
from digitalhub.client._base.client import Client
|
|
7
|
+
from digitalhub.utils.exceptions import BackendError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ClientLocal(Client):
|
|
11
|
+
"""
|
|
12
|
+
Local client.
|
|
13
|
+
|
|
14
|
+
The Local client can be used when a remote Digitalhub backend is not available.
|
|
15
|
+
It handles the creation, reading, updating and deleting of objects in memory,
|
|
16
|
+
storing them in a local dictionary.
|
|
17
|
+
The functionality of the Local client is almost the same as the DHCore client.
|
|
18
|
+
Main differences are:
|
|
19
|
+
- Local client does delete objects on cascade.
|
|
20
|
+
- The run execution are forced to be local.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self) -> None:
|
|
24
|
+
super().__init__()
|
|
25
|
+
self._db: dict[str, dict[str, dict]] = {}
|
|
26
|
+
|
|
27
|
+
##############################
|
|
28
|
+
# CRUD
|
|
29
|
+
##############################
|
|
30
|
+
|
|
31
|
+
def create_object(self, api: str, obj: dict, **kwargs) -> dict:
|
|
32
|
+
"""
|
|
33
|
+
Create an object in local.
|
|
34
|
+
|
|
35
|
+
Parameters
|
|
36
|
+
----------
|
|
37
|
+
api : str
|
|
38
|
+
Create API.
|
|
39
|
+
obj : dict
|
|
40
|
+
Object to create.
|
|
41
|
+
|
|
42
|
+
Returns
|
|
43
|
+
-------
|
|
44
|
+
dict
|
|
45
|
+
The created object.
|
|
46
|
+
"""
|
|
47
|
+
entity_type, _, context_api = self._parse_api(api)
|
|
48
|
+
try:
|
|
49
|
+
# Check if entity_type is valid
|
|
50
|
+
if entity_type is None:
|
|
51
|
+
raise TypeError
|
|
52
|
+
|
|
53
|
+
# Check if entity_type exists, if not, create a mapping
|
|
54
|
+
self._db.setdefault(entity_type, {})
|
|
55
|
+
|
|
56
|
+
# Base API
|
|
57
|
+
#
|
|
58
|
+
# POST /api/v1/projects
|
|
59
|
+
#
|
|
60
|
+
# Project are not versioned, everything is stored on "entity_id" key
|
|
61
|
+
if not context_api:
|
|
62
|
+
if entity_type == "projects":
|
|
63
|
+
entity_id = obj["name"]
|
|
64
|
+
if entity_id in self._db[entity_type]:
|
|
65
|
+
raise ValueError
|
|
66
|
+
self._db[entity_type][entity_id] = obj
|
|
67
|
+
|
|
68
|
+
# Context API
|
|
69
|
+
#
|
|
70
|
+
# POST /api/v1/-/<project-name>/artifacts
|
|
71
|
+
# POST /api/v1/-/<project-name>/functions
|
|
72
|
+
# POST /api/v1/-/<project-name>/runs
|
|
73
|
+
#
|
|
74
|
+
# Runs and tasks are not versioned, so we keep name as entity_id.
|
|
75
|
+
# We have both "name" and "id" attributes for versioned objects so we use them as storage keys.
|
|
76
|
+
# The "latest" key is used to store the latest version of the object.
|
|
77
|
+
else:
|
|
78
|
+
entity_id = obj["id"]
|
|
79
|
+
name = obj.get("name", entity_id)
|
|
80
|
+
self._db[entity_type].setdefault(name, {})
|
|
81
|
+
if entity_id in self._db[entity_type][name]:
|
|
82
|
+
raise ValueError
|
|
83
|
+
self._db[entity_type][name][entity_id] = obj
|
|
84
|
+
self._db[entity_type][name]["latest"] = obj
|
|
85
|
+
|
|
86
|
+
# Return the created object
|
|
87
|
+
return obj
|
|
88
|
+
|
|
89
|
+
# Key error are possibly raised by accessing invalid objects
|
|
90
|
+
except (KeyError, TypeError):
|
|
91
|
+
msg = self._format_msg(1, entity_type=entity_type)
|
|
92
|
+
raise BackendError(msg)
|
|
93
|
+
|
|
94
|
+
# If try to create already existing object
|
|
95
|
+
except ValueError:
|
|
96
|
+
msg = self._format_msg(2, entity_type=entity_type, entity_id=entity_id)
|
|
97
|
+
raise BackendError(msg)
|
|
98
|
+
|
|
99
|
+
def read_object(self, api: str, **kwargs) -> dict:
|
|
100
|
+
"""
|
|
101
|
+
Get an object from local.
|
|
102
|
+
|
|
103
|
+
Parameters
|
|
104
|
+
----------
|
|
105
|
+
api : str
|
|
106
|
+
Read API.
|
|
107
|
+
|
|
108
|
+
Returns
|
|
109
|
+
-------
|
|
110
|
+
dict
|
|
111
|
+
The read object.
|
|
112
|
+
"""
|
|
113
|
+
entity_type, entity_id, context_api = self._parse_api(api)
|
|
114
|
+
if entity_id is None:
|
|
115
|
+
msg = self._format_msg(4)
|
|
116
|
+
raise BackendError(msg)
|
|
117
|
+
try:
|
|
118
|
+
# Base API
|
|
119
|
+
#
|
|
120
|
+
# GET /api/v1/projects/<entity_id>
|
|
121
|
+
#
|
|
122
|
+
# self._parse_api() should return only entity_type
|
|
123
|
+
|
|
124
|
+
if not context_api:
|
|
125
|
+
obj = self._db[entity_type][entity_id]
|
|
126
|
+
|
|
127
|
+
# If the object is a project, we need to add the project spec,
|
|
128
|
+
# for example artifacts, functions, workflows, etc.
|
|
129
|
+
# Technically we have only projects that access base apis,
|
|
130
|
+
# we check entity_type just in case we add something else.
|
|
131
|
+
if entity_type == "projects":
|
|
132
|
+
obj = self._get_project_spec(obj, entity_id)
|
|
133
|
+
return obj
|
|
134
|
+
|
|
135
|
+
# Context API
|
|
136
|
+
#
|
|
137
|
+
# GET /api/v1/-/<project-name>/runs/<entity_id>
|
|
138
|
+
# GET /api/v1/-/<project-name>/artifacts/<entity_id>
|
|
139
|
+
# GET /api/v1/-/<project-name>/functions/<entity_id>
|
|
140
|
+
#
|
|
141
|
+
# self._parse_api() should return entity_type and entity_id/version
|
|
142
|
+
|
|
143
|
+
else:
|
|
144
|
+
for _, v in self._db[entity_type].items():
|
|
145
|
+
if entity_id in v:
|
|
146
|
+
return v[entity_id]
|
|
147
|
+
else:
|
|
148
|
+
raise KeyError
|
|
149
|
+
|
|
150
|
+
except KeyError:
|
|
151
|
+
msg = self._format_msg(3, entity_type=entity_type, entity_id=entity_id)
|
|
152
|
+
raise BackendError(msg)
|
|
153
|
+
|
|
154
|
+
def update_object(self, api: str, obj: dict, **kwargs) -> dict:
|
|
155
|
+
"""
|
|
156
|
+
Update an object in local.
|
|
157
|
+
|
|
158
|
+
Parameters
|
|
159
|
+
----------
|
|
160
|
+
api : str
|
|
161
|
+
Update API.
|
|
162
|
+
obj : dict
|
|
163
|
+
Object to update.
|
|
164
|
+
|
|
165
|
+
Returns
|
|
166
|
+
-------
|
|
167
|
+
dict
|
|
168
|
+
The updated object.
|
|
169
|
+
"""
|
|
170
|
+
entity_type, entity_id, context_api = self._parse_api(api)
|
|
171
|
+
try:
|
|
172
|
+
# API example
|
|
173
|
+
#
|
|
174
|
+
# PUT /api/v1/projects/<entity_id>
|
|
175
|
+
|
|
176
|
+
if not context_api:
|
|
177
|
+
self._db[entity_type][entity_id] = obj
|
|
178
|
+
|
|
179
|
+
# Context API
|
|
180
|
+
#
|
|
181
|
+
# PUT /api/v1/-/<project-name>/runs/<entity_id>
|
|
182
|
+
# PUT /api/v1/-/<project-name>/artifacts/<entity_id>
|
|
183
|
+
|
|
184
|
+
else:
|
|
185
|
+
name = obj.get("name", entity_id)
|
|
186
|
+
self._db[entity_type][name][entity_id] = obj
|
|
187
|
+
|
|
188
|
+
except KeyError:
|
|
189
|
+
msg = self._format_msg(3, entity_type=entity_type, entity_id=entity_id)
|
|
190
|
+
raise BackendError(msg)
|
|
191
|
+
|
|
192
|
+
return obj
|
|
193
|
+
|
|
194
|
+
def delete_object(self, api: str, **kwargs) -> dict:
|
|
195
|
+
"""
|
|
196
|
+
Delete an object from local.
|
|
197
|
+
|
|
198
|
+
Parameters
|
|
199
|
+
----------
|
|
200
|
+
api : str
|
|
201
|
+
Delete API.
|
|
202
|
+
**kwargs : dict
|
|
203
|
+
Keyword arguments parsed from request.
|
|
204
|
+
|
|
205
|
+
Returns
|
|
206
|
+
-------
|
|
207
|
+
dict
|
|
208
|
+
Response object.
|
|
209
|
+
"""
|
|
210
|
+
entity_type, entity_id, context_api = self._parse_api(api)
|
|
211
|
+
try:
|
|
212
|
+
# Base API
|
|
213
|
+
#
|
|
214
|
+
# DELETE /api/v1/projects/<entity_id>
|
|
215
|
+
|
|
216
|
+
if not context_api:
|
|
217
|
+
self._db[entity_type].pop(entity_id)
|
|
218
|
+
|
|
219
|
+
# Context API
|
|
220
|
+
#
|
|
221
|
+
# DELETE /api/v1/-/<project-name>/artifacts/<entity_id>
|
|
222
|
+
#
|
|
223
|
+
# We do not handle cascade in local client and
|
|
224
|
+
# in the sdk we selectively delete objects by id,
|
|
225
|
+
# not by name nor entity_type.
|
|
226
|
+
|
|
227
|
+
else:
|
|
228
|
+
reset_latest = False
|
|
229
|
+
|
|
230
|
+
# Name is optional and extracted from kwargs
|
|
231
|
+
# "params": {"name": <name>}
|
|
232
|
+
name = kwargs.get("params", {}).get("name")
|
|
233
|
+
|
|
234
|
+
# Delete by name
|
|
235
|
+
if entity_id is None and name is not None:
|
|
236
|
+
self._db[entity_type].pop(name, None)
|
|
237
|
+
return {"deleted": True}
|
|
238
|
+
|
|
239
|
+
# Delete by id
|
|
240
|
+
for _, v in self._db[entity_type].items():
|
|
241
|
+
if entity_id in v:
|
|
242
|
+
v.pop(entity_id)
|
|
243
|
+
|
|
244
|
+
# Handle latest
|
|
245
|
+
if v["latest"]["id"] == entity_id:
|
|
246
|
+
name = v["latest"].get("name", entity_id)
|
|
247
|
+
v.pop("latest")
|
|
248
|
+
reset_latest = True
|
|
249
|
+
break
|
|
250
|
+
else:
|
|
251
|
+
raise KeyError
|
|
252
|
+
|
|
253
|
+
if name is not None:
|
|
254
|
+
# Pop name if empty
|
|
255
|
+
if not self._db[entity_type][name]:
|
|
256
|
+
self._db[entity_type].pop(name)
|
|
257
|
+
|
|
258
|
+
# Handle latest
|
|
259
|
+
elif reset_latest:
|
|
260
|
+
latest_uuid = None
|
|
261
|
+
latest_date = None
|
|
262
|
+
for k, v in self._db[entity_type][name].items():
|
|
263
|
+
# Get created from metadata. If tzinfo is None, set it to UTC
|
|
264
|
+
# If created is not in ISO format, use fallback
|
|
265
|
+
fallback = datetime.fromtimestamp(0, timezone.utc)
|
|
266
|
+
try:
|
|
267
|
+
current_created = datetime.fromisoformat(v.get("metadata", {}).get("created"))
|
|
268
|
+
if current_created.tzinfo is None:
|
|
269
|
+
current_created = current_created.replace(tzinfo=timezone.utc)
|
|
270
|
+
except ValueError:
|
|
271
|
+
current_created = fallback
|
|
272
|
+
|
|
273
|
+
# Update latest date and uuid
|
|
274
|
+
if latest_date is None or current_created > latest_date:
|
|
275
|
+
latest_uuid = k
|
|
276
|
+
latest_date = current_created
|
|
277
|
+
|
|
278
|
+
# Set new latest
|
|
279
|
+
if latest_uuid is not None:
|
|
280
|
+
self._db[entity_type][name]["latest"] = self._db[entity_type][name][latest_uuid]
|
|
281
|
+
|
|
282
|
+
except KeyError:
|
|
283
|
+
msg = self._format_msg(3, entity_type=entity_type, entity_id=entity_id)
|
|
284
|
+
raise BackendError(msg)
|
|
285
|
+
return {"deleted": True}
|
|
286
|
+
|
|
287
|
+
def list_objects(self, api: str, **kwargs) -> list:
|
|
288
|
+
"""
|
|
289
|
+
List objects.
|
|
290
|
+
|
|
291
|
+
Parameters
|
|
292
|
+
----------
|
|
293
|
+
api : str
|
|
294
|
+
List API.
|
|
295
|
+
**kwargs : dict
|
|
296
|
+
Keyword arguments parsed from request.
|
|
297
|
+
|
|
298
|
+
Returns
|
|
299
|
+
-------
|
|
300
|
+
list | None
|
|
301
|
+
The list of objects.
|
|
302
|
+
"""
|
|
303
|
+
entity_type, _, _ = self._parse_api(api)
|
|
304
|
+
|
|
305
|
+
# Name is optional and extracted from kwargs
|
|
306
|
+
# "params": {"name": <name>}
|
|
307
|
+
name = kwargs.get("params", {}).get("name")
|
|
308
|
+
if name is not None:
|
|
309
|
+
return [self._db[entity_type][name]["latest"]]
|
|
310
|
+
|
|
311
|
+
try:
|
|
312
|
+
# If no name is provided, get latest objects
|
|
313
|
+
listed_objects = [v["latest"] for _, v in self._db[entity_type].items()]
|
|
314
|
+
except KeyError:
|
|
315
|
+
listed_objects = []
|
|
316
|
+
|
|
317
|
+
# If kind is provided, return objects by kind
|
|
318
|
+
kind = kwargs.get("params", {}).get("kind")
|
|
319
|
+
if kind is not None:
|
|
320
|
+
listed_objects = [obj for obj in listed_objects if obj["kind"] == kind]
|
|
321
|
+
|
|
322
|
+
# If function is provided, return objects by function
|
|
323
|
+
spec_params = ["function", "task"]
|
|
324
|
+
for i in spec_params:
|
|
325
|
+
p = kwargs.get("params", {}).get(i)
|
|
326
|
+
if p is not None:
|
|
327
|
+
listed_objects = [obj for obj in listed_objects if obj["spec"][i] == p]
|
|
328
|
+
|
|
329
|
+
return listed_objects
|
|
330
|
+
|
|
331
|
+
def list_first_object(self, api: str, **kwargs) -> dict:
|
|
332
|
+
"""
|
|
333
|
+
List first objects.
|
|
334
|
+
|
|
335
|
+
Parameters
|
|
336
|
+
----------
|
|
337
|
+
api : str
|
|
338
|
+
The api to list the objects with.
|
|
339
|
+
**kwargs : dict
|
|
340
|
+
Keyword arguments passed to the request.
|
|
341
|
+
|
|
342
|
+
Returns
|
|
343
|
+
-------
|
|
344
|
+
dict
|
|
345
|
+
The list of objects.
|
|
346
|
+
"""
|
|
347
|
+
try:
|
|
348
|
+
return self.list_objects(api, **kwargs)[0]
|
|
349
|
+
except IndexError:
|
|
350
|
+
raise IndexError("No objects found")
|
|
351
|
+
|
|
352
|
+
##############################
|
|
353
|
+
# Helpers
|
|
354
|
+
##############################
|
|
355
|
+
|
|
356
|
+
def _parse_api(self, api: str) -> tuple:
|
|
357
|
+
"""
|
|
358
|
+
Parse the given API to extract the entity_type, entity_id
|
|
359
|
+
and if its a context API.
|
|
360
|
+
|
|
361
|
+
Parameters
|
|
362
|
+
----------
|
|
363
|
+
api : str
|
|
364
|
+
API to parse.
|
|
365
|
+
|
|
366
|
+
Returns
|
|
367
|
+
-------
|
|
368
|
+
tuple
|
|
369
|
+
Parsed elements.
|
|
370
|
+
"""
|
|
371
|
+
# Remove prefix from API
|
|
372
|
+
api = api.removeprefix("/api/v1/")
|
|
373
|
+
|
|
374
|
+
# Set context flag by default to False
|
|
375
|
+
context_api = False
|
|
376
|
+
|
|
377
|
+
# Remove context prefix from API and set context flag to True
|
|
378
|
+
if api.startswith("-/"):
|
|
379
|
+
context_api = True
|
|
380
|
+
api = api[2:]
|
|
381
|
+
|
|
382
|
+
# Return parsed elements
|
|
383
|
+
return self._parse_api_elements(api, context_api)
|
|
384
|
+
|
|
385
|
+
@staticmethod
|
|
386
|
+
def _parse_api_elements(api: str, context_api: bool) -> tuple:
|
|
387
|
+
"""
|
|
388
|
+
Parse the elements from the given API.
|
|
389
|
+
Elements returned are: entity_type, entity_id, context_api.
|
|
390
|
+
|
|
391
|
+
Parameters
|
|
392
|
+
----------
|
|
393
|
+
api : str
|
|
394
|
+
Parsed API.
|
|
395
|
+
context_api : bool
|
|
396
|
+
True if the API is a context API.
|
|
397
|
+
|
|
398
|
+
Returns
|
|
399
|
+
-------
|
|
400
|
+
tuple
|
|
401
|
+
Parsed elements from the API.
|
|
402
|
+
"""
|
|
403
|
+
# Split API path
|
|
404
|
+
parsed = api.split("/")
|
|
405
|
+
|
|
406
|
+
# Base API for versioned objects
|
|
407
|
+
|
|
408
|
+
# POST /api/v1/<entity_type>
|
|
409
|
+
# Returns entity_type, None, False
|
|
410
|
+
if len(parsed) == 1 and not context_api:
|
|
411
|
+
return parsed[0], None, context_api
|
|
412
|
+
|
|
413
|
+
# GET/DELETE/UPDATE /api/v1/<entity_type>/<entity_id>
|
|
414
|
+
# Return entity_type, entity_id, False
|
|
415
|
+
if len(parsed) == 2 and not context_api:
|
|
416
|
+
return parsed[0], parsed[1], context_api
|
|
417
|
+
|
|
418
|
+
# Context API for versioned objects
|
|
419
|
+
|
|
420
|
+
# POST /api/v1/-/<project>/<entity_type>
|
|
421
|
+
# Returns entity_type, None, True
|
|
422
|
+
if len(parsed) == 2 and context_api:
|
|
423
|
+
return parsed[1], None, context_api
|
|
424
|
+
|
|
425
|
+
# GET/DELETE/UPDATE /api/v1/-/<project>/<entity_type>/<entity_id>
|
|
426
|
+
# Return entity_type, entity_id, True
|
|
427
|
+
if len(parsed) == 3 and context_api:
|
|
428
|
+
return parsed[1], parsed[2], context_api
|
|
429
|
+
|
|
430
|
+
raise ValueError(f"Invalid API: {api}")
|
|
431
|
+
|
|
432
|
+
def _get_project_spec(self, obj: dict, name: str) -> dict:
|
|
433
|
+
"""
|
|
434
|
+
Enrich project object with spec (artifacts, functions, etc.).
|
|
435
|
+
|
|
436
|
+
Parameters
|
|
437
|
+
----------
|
|
438
|
+
obj : dict
|
|
439
|
+
The project object.
|
|
440
|
+
name : str
|
|
441
|
+
The project name.
|
|
442
|
+
|
|
443
|
+
Returns
|
|
444
|
+
-------
|
|
445
|
+
dict
|
|
446
|
+
The project object with the spec.
|
|
447
|
+
"""
|
|
448
|
+
# Deepcopy to avoid modifying the original object
|
|
449
|
+
project = deepcopy(obj)
|
|
450
|
+
spec = project.get("spec", {})
|
|
451
|
+
|
|
452
|
+
# Get all entities associated with the project specs
|
|
453
|
+
projects_entities = [k for k, _ in self._db.items() if k not in ["projects", "runs", "tasks"]]
|
|
454
|
+
|
|
455
|
+
for entity_type in projects_entities:
|
|
456
|
+
# Get all objects of the entity type for the project
|
|
457
|
+
objs = self._db[entity_type]
|
|
458
|
+
|
|
459
|
+
# Set empty list
|
|
460
|
+
spec[entity_type] = []
|
|
461
|
+
|
|
462
|
+
# Cycle through named objects
|
|
463
|
+
for _, named_entities in objs.items():
|
|
464
|
+
# Get latest version
|
|
465
|
+
for version, entity in named_entities.items():
|
|
466
|
+
if version != "latest":
|
|
467
|
+
continue
|
|
468
|
+
|
|
469
|
+
# Deepcopy to avoid modifying the original object
|
|
470
|
+
copied = deepcopy(entity)
|
|
471
|
+
|
|
472
|
+
# Remove spec if not embedded
|
|
473
|
+
if not copied.get("metadata", {}).get("embedded", False):
|
|
474
|
+
copied.pop("spec", None)
|
|
475
|
+
|
|
476
|
+
# Add to project spec
|
|
477
|
+
if copied["project"] == name:
|
|
478
|
+
spec[entity_type].append(copied)
|
|
479
|
+
|
|
480
|
+
return project
|
|
481
|
+
|
|
482
|
+
##############################
|
|
483
|
+
# Utils
|
|
484
|
+
##############################
|
|
485
|
+
|
|
486
|
+
@staticmethod
|
|
487
|
+
def _format_msg(
|
|
488
|
+
error_code: int,
|
|
489
|
+
entity_type: str | None = None,
|
|
490
|
+
entity_id: str | None = None,
|
|
491
|
+
) -> str:
|
|
492
|
+
"""
|
|
493
|
+
Format a message.
|
|
494
|
+
|
|
495
|
+
Parameters
|
|
496
|
+
----------
|
|
497
|
+
error_code : int
|
|
498
|
+
Error code.
|
|
499
|
+
project : str
|
|
500
|
+
Project name.
|
|
501
|
+
entity_type : str
|
|
502
|
+
Entity type.
|
|
503
|
+
entity_id : str
|
|
504
|
+
Entity ID.
|
|
505
|
+
|
|
506
|
+
Returns
|
|
507
|
+
-------
|
|
508
|
+
str
|
|
509
|
+
The formatted message.
|
|
510
|
+
"""
|
|
511
|
+
msg = {
|
|
512
|
+
1: f"Object '{entity_type}' to create is not valid",
|
|
513
|
+
2: f"Object '{entity_type}' with id '{entity_id}' already exists",
|
|
514
|
+
3: f"Object '{entity_type}' with id '{entity_id}' not found",
|
|
515
|
+
4: "Must provide entity_id to read an object",
|
|
516
|
+
}
|
|
517
|
+
return msg[error_code]
|
|
518
|
+
|
|
519
|
+
##############################
|
|
520
|
+
# Interface methods
|
|
521
|
+
##############################
|
|
522
|
+
|
|
523
|
+
@staticmethod
|
|
524
|
+
def is_local() -> bool:
|
|
525
|
+
"""
|
|
526
|
+
Declare if Client is local.
|
|
527
|
+
|
|
528
|
+
Returns
|
|
529
|
+
-------
|
|
530
|
+
bool
|
|
531
|
+
True
|
|
532
|
+
"""
|
|
533
|
+
return True
|
|
File without changes
|