statsuite-lib 0.1.3__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.
- statsuite_lib-0.1.3/PKG-INFO +66 -0
- statsuite_lib-0.1.3/README.md +51 -0
- statsuite_lib-0.1.3/pyproject.toml +54 -0
- statsuite_lib-0.1.3/statsuite_lib/__init__.py +6 -0
- statsuite_lib-0.1.3/statsuite_lib/auth/__init__.py +1 -0
- statsuite_lib-0.1.3/statsuite_lib/auth/auth.py +96 -0
- statsuite_lib-0.1.3/statsuite_lib/config/__init__.py +1 -0
- statsuite_lib-0.1.3/statsuite_lib/config/config.py +47 -0
- statsuite_lib-0.1.3/statsuite_lib/config/models.py +49 -0
- statsuite_lib-0.1.3/statsuite_lib/keycloak/__init__.py +1 -0
- statsuite_lib-0.1.3/statsuite_lib/keycloak/keycloak.py +147 -0
- statsuite_lib-0.1.3/statsuite_lib/nsi/__init__.py +1 -0
- statsuite_lib-0.1.3/statsuite_lib/nsi/nsi.py +101 -0
- statsuite_lib-0.1.3/statsuite_lib/sfs/__init__.py +1 -0
- statsuite_lib-0.1.3/statsuite_lib/sfs/models.py +39 -0
- statsuite_lib-0.1.3/statsuite_lib/sfs/sfs.py +148 -0
- statsuite_lib-0.1.3/statsuite_lib/transfer/__init__.py +1 -0
- statsuite_lib-0.1.3/statsuite_lib/transfer/transfer.py +218 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: statsuite_lib
|
|
3
|
+
Version: 0.1.3
|
|
4
|
+
Summary:
|
|
5
|
+
Author: Sergio Pena
|
|
6
|
+
Author-email: isergiopena@gmail.com
|
|
7
|
+
Requires-Python: >=3.11,<4.0
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Requires-Dist: httpx (>=0.28.1,<0.29.0)
|
|
13
|
+
Requires-Dist: pydantic (==2.9.2)
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# StatSuite-lib
|
|
17
|
+
|
|
18
|
+
StatSuite-lib is an opinionated library to interact with dotStatSuite components.
|
|
19
|
+
There is no full coverage of the APIs, it will be completed as needed.
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
Use the package manager [pip](https://pip.pypa.io/en/stable/) to install statsuite-lib.
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install statsuite-lib
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
Check the documentation, mainly the statsuite-cli repo can serve as example on how to use this library
|
|
32
|
+
Keycloak client must be initialized and pass to any other client requiring authentication to manage the token creation and renewal
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from statsuite_lib import SFSClient, KeycloakClient
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
OPENID_URL = "http://localhost:8080/auth/realms/demo/.well-known/openid-configuration"
|
|
39
|
+
KEYCLOAK_USER = 'test-admin'
|
|
40
|
+
KEYCLOAK_PASSWORD = 'admin'
|
|
41
|
+
SFS_API_KEY = 'secret'
|
|
42
|
+
SFS_URL = 'http://localhost:3004'
|
|
43
|
+
|
|
44
|
+
keycloak = KeycloakClient(openid_url=OPENID_URL,
|
|
45
|
+
username=KEYCLOAK_USER,
|
|
46
|
+
password=KEYCLOAK_PASSWORD)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
sfs = SFSClient(sfs_url=SFS_URL, sfs_api_key=SFS_API_KEY)
|
|
50
|
+
|
|
51
|
+
id = sfs.index()
|
|
52
|
+
|
|
53
|
+
print(f"Index started with id {id}")
|
|
54
|
+
sfs.wait_for_reindex_to_finish(loading_id=id, tenant='default')
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Contributing
|
|
58
|
+
|
|
59
|
+
Pull requests are welcome. For major changes, please open an issue first
|
|
60
|
+
to discuss what you would like to change.
|
|
61
|
+
|
|
62
|
+
Please make sure to update tests as appropriate.
|
|
63
|
+
|
|
64
|
+
## License
|
|
65
|
+
|
|
66
|
+
[MIT](https://choosealicense.com/licenses/mit/)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# StatSuite-lib
|
|
2
|
+
|
|
3
|
+
StatSuite-lib is an opinionated library to interact with dotStatSuite components.
|
|
4
|
+
There is no full coverage of the APIs, it will be completed as needed.
|
|
5
|
+
|
|
6
|
+
## Installation
|
|
7
|
+
|
|
8
|
+
Use the package manager [pip](https://pip.pypa.io/en/stable/) to install statsuite-lib.
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pip install statsuite-lib
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
Check the documentation, mainly the statsuite-cli repo can serve as example on how to use this library
|
|
17
|
+
Keycloak client must be initialized and pass to any other client requiring authentication to manage the token creation and renewal
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from statsuite_lib import SFSClient, KeycloakClient
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
OPENID_URL = "http://localhost:8080/auth/realms/demo/.well-known/openid-configuration"
|
|
24
|
+
KEYCLOAK_USER = 'test-admin'
|
|
25
|
+
KEYCLOAK_PASSWORD = 'admin'
|
|
26
|
+
SFS_API_KEY = 'secret'
|
|
27
|
+
SFS_URL = 'http://localhost:3004'
|
|
28
|
+
|
|
29
|
+
keycloak = KeycloakClient(openid_url=OPENID_URL,
|
|
30
|
+
username=KEYCLOAK_USER,
|
|
31
|
+
password=KEYCLOAK_PASSWORD)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
sfs = SFSClient(sfs_url=SFS_URL, sfs_api_key=SFS_API_KEY)
|
|
35
|
+
|
|
36
|
+
id = sfs.index()
|
|
37
|
+
|
|
38
|
+
print(f"Index started with id {id}")
|
|
39
|
+
sfs.wait_for_reindex_to_finish(loading_id=id, tenant='default')
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Contributing
|
|
43
|
+
|
|
44
|
+
Pull requests are welcome. For major changes, please open an issue first
|
|
45
|
+
to discuss what you would like to change.
|
|
46
|
+
|
|
47
|
+
Please make sure to update tests as appropriate.
|
|
48
|
+
|
|
49
|
+
## License
|
|
50
|
+
|
|
51
|
+
[MIT](https://choosealicense.com/licenses/mit/)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "statsuite_lib"
|
|
3
|
+
version = "0.1.3"
|
|
4
|
+
description = ""
|
|
5
|
+
authors = ["Sergio Pena <isergiopena@gmail.com>"]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
|
|
8
|
+
[tool.poetry.dependencies]
|
|
9
|
+
python = "^3.11,<4.0"
|
|
10
|
+
httpx = "^0.28.1"
|
|
11
|
+
pydantic = "2.9.2"
|
|
12
|
+
|
|
13
|
+
[tool.poetry.requires-plugins]
|
|
14
|
+
poetry-plugin-export = ">=1.8"
|
|
15
|
+
|
|
16
|
+
[tool.poetry.group.dev.dependencies]
|
|
17
|
+
pytest-httpx = "^0.35.0"
|
|
18
|
+
pytest = "^8.3.4"
|
|
19
|
+
pytest-coverage = "^0.0"
|
|
20
|
+
pytest-mock = "^3.14.0"
|
|
21
|
+
bandit = "^1.8.0"
|
|
22
|
+
flake8-simplify = "^0.21.0"
|
|
23
|
+
black = "^24.10.0"
|
|
24
|
+
flake8-variables-names = "^0.0.6"
|
|
25
|
+
flake8-functions-names = "^0.4.0"
|
|
26
|
+
flake8-cognitive-complexity = "^0.1.0"
|
|
27
|
+
flake8-expression-complexity = "^0.0.11"
|
|
28
|
+
flake8-docstrings-complete = "^1.4.1"
|
|
29
|
+
flake8-black = "^0.3.6"
|
|
30
|
+
flake8-bandit = "^4.1.1"
|
|
31
|
+
flake8-isort = "^6.1.1"
|
|
32
|
+
safety = "^3.2.14"
|
|
33
|
+
flake8-pyproject = "^1.2.3"
|
|
34
|
+
freezegun = "^1.5.3"
|
|
35
|
+
cryptography = ">=44.0.2"
|
|
36
|
+
sphinx = "^8.2.3"
|
|
37
|
+
pdoc = "^15.0.4"
|
|
38
|
+
sphinx-rtd-theme = "^3.0.2"
|
|
39
|
+
myst-parser = "^4.0.1"
|
|
40
|
+
sphinx-toolbox = "^4.0.0"
|
|
41
|
+
pip = "^25.2"
|
|
42
|
+
|
|
43
|
+
[build-system]
|
|
44
|
+
requires = ["poetry-core"]
|
|
45
|
+
build-backend = "poetry.core.masonry.api"
|
|
46
|
+
|
|
47
|
+
[tool.flake8]
|
|
48
|
+
exclude = ['.poetry']
|
|
49
|
+
ignore = ['E231', 'E241']
|
|
50
|
+
per-file-ignores = [
|
|
51
|
+
'__init__.py:F401',
|
|
52
|
+
'tests/*py:S101',
|
|
53
|
+
]
|
|
54
|
+
max-line-length = 95
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .auth import AuthClient
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
|
|
5
|
+
from ..keycloak.keycloak import KeycloakClient
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AuthClient:
|
|
9
|
+
"""A client for managing authorization rules through the Auth API.
|
|
10
|
+
|
|
11
|
+
This client handles the communication with the authorization service,
|
|
12
|
+
allowing for the management of access rules and permissions.
|
|
13
|
+
|
|
14
|
+
Attributes:
|
|
15
|
+
AUTH_URL (str): The complete URL for the Auth API including version.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
auth_url (str): Base URL of the authorization service.
|
|
19
|
+
keycloak_client (KeycloakClient): Client for handling Keycloak authentication.
|
|
20
|
+
api_version (str, optional): API version to use. Defaults to "1.1".
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self, auth_url: str, keycloak_client: KeycloakClient, api_version: str = "1.1"
|
|
25
|
+
) -> None:
|
|
26
|
+
"""Initialize the AuthClient.
|
|
27
|
+
|
|
28
|
+
Creates a new instance of the AuthClient with the specified configuration.
|
|
29
|
+
Sets up an HTTP client and configures logging.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
auth_url (str): Base URL of the authorization service endpoint.
|
|
33
|
+
Should not include the version number.
|
|
34
|
+
keycloak_client (KeycloakClient): An initialized Keycloak client instance
|
|
35
|
+
used for authentication headers.
|
|
36
|
+
api_version (str, optional): API version string to use in URL construction.
|
|
37
|
+
Defaults to "1.1".
|
|
38
|
+
|
|
39
|
+
Example:
|
|
40
|
+
keycloak_client = KeycloakClient(...)
|
|
41
|
+
auth_client = AuthClient(
|
|
42
|
+
auth_url="https://auth.example.com",
|
|
43
|
+
keycloak_client=keycloak_client
|
|
44
|
+
)
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
self._client = httpx.Client()
|
|
48
|
+
self.AUTH_URL = f"{auth_url}/{api_version}"
|
|
49
|
+
self._keycloak_client = keycloak_client
|
|
50
|
+
self._log = logging.getLogger("AuthClient")
|
|
51
|
+
|
|
52
|
+
def add_rule(
|
|
53
|
+
self,
|
|
54
|
+
user_mask: str,
|
|
55
|
+
is_group: bool,
|
|
56
|
+
permission=int,
|
|
57
|
+
dataspace: str = "*",
|
|
58
|
+
artifact_type: int = 0,
|
|
59
|
+
artefact_agency_id: str = "*",
|
|
60
|
+
artefact_id: str = "*",
|
|
61
|
+
artefact_version: str = "*",
|
|
62
|
+
):
|
|
63
|
+
"""Add a new authorization rule to the system.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
user_mask (str): The user or group identifier pattern.
|
|
67
|
+
is_group (bool): Whether the rule applies to a group (True) or user (False).
|
|
68
|
+
permission (int): The permission level to grant.
|
|
69
|
+
dataspace (str, optional): Target dataspace. Defaults to "*" (all dataspaces).
|
|
70
|
+
artifact_type (int, optional): Type of artifact. Defaults to 0.
|
|
71
|
+
artefact_agency_id (str, optional): Agency ID of the artifact. Defaults to "*".
|
|
72
|
+
artefact_id (str, optional): ID of the artifact. Defaults to "*".
|
|
73
|
+
artefact_version (str, optional): Version of the artifact. Defaults to "*".
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
dict: The JSON response from the server containing the created rule.
|
|
77
|
+
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
data = {
|
|
81
|
+
"userMask": user_mask,
|
|
82
|
+
"isGroup": is_group,
|
|
83
|
+
"dataSpace": dataspace,
|
|
84
|
+
"artefactType": artifact_type,
|
|
85
|
+
"artefactAgencyId": artefact_agency_id,
|
|
86
|
+
"artefactId": artefact_id,
|
|
87
|
+
"artefactVersion": artefact_version,
|
|
88
|
+
"permission": permission,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
url = f"{self.AUTH_URL}/AuthorizationRules"
|
|
92
|
+
response = httpx.post(
|
|
93
|
+
url=url, headers=self._keycloak_client.auth_header(), data=data
|
|
94
|
+
)
|
|
95
|
+
response.raise_for_status()
|
|
96
|
+
return response.json()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .config import ConfigClient
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Iterator
|
|
3
|
+
|
|
4
|
+
import httpx
|
|
5
|
+
|
|
6
|
+
from .models import Space, Tenants
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ConfigClient:
|
|
10
|
+
"""Client for the SDMX Faceted search service"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, config_url: str) -> None:
|
|
13
|
+
"""Inits the client
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
config_url: Endpoint url for Config service.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
self._client = httpx.Client()
|
|
20
|
+
self.CONFIG_URL = config_url
|
|
21
|
+
self.log = logging.getLogger("ConfigClient")
|
|
22
|
+
self.log.level = logging.INFO
|
|
23
|
+
|
|
24
|
+
def get_tenants(self) -> str:
|
|
25
|
+
"""Gets tenants config
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
loadingId(str)
|
|
29
|
+
"""
|
|
30
|
+
resp = httpx.get(f"{self.CONFIG_URL}/configs/tenants.json")
|
|
31
|
+
if resp.status_code == 200:
|
|
32
|
+
loading = Tenants.model_validate(resp.json())
|
|
33
|
+
return loading
|
|
34
|
+
|
|
35
|
+
def get_dataspaces(self, tenant: str = "default") -> Iterator[Space]:
|
|
36
|
+
"""Returns a list of dataspaces configured for a tenant
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
tenant: select which tenant
|
|
40
|
+
|
|
41
|
+
Yields:
|
|
42
|
+
Space: A dataspace configuration object for each space in the tenant.
|
|
43
|
+
"""
|
|
44
|
+
tenants = self.get_tenants()
|
|
45
|
+
spaces = tenants.root.get(tenant).spaces
|
|
46
|
+
for space in spaces:
|
|
47
|
+
yield spaces.get(space)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from typing import Dict, Union
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, ConfigDict, RootModel
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Index(RootModel):
|
|
7
|
+
"""Model for return json from indexing request on SFS
|
|
8
|
+
|
|
9
|
+
Attributes:
|
|
10
|
+
root: returns a json in the format { "loadingId": ######### }
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
root: Dict[str, Union[str, Dict]]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Space(BaseModel):
|
|
17
|
+
"""Model for spaces inside tenants
|
|
18
|
+
|
|
19
|
+
Attributes:
|
|
20
|
+
id: Space id
|
|
21
|
+
url: space url
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
id: str # noqa VNE003
|
|
25
|
+
url: str
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Tenant(BaseModel):
|
|
29
|
+
"""Minimun model for Loading Log entity
|
|
30
|
+
|
|
31
|
+
Attributes:
|
|
32
|
+
model_config: Configuration
|
|
33
|
+
id: Loadingid
|
|
34
|
+
spaces: Dict of spaces inside tenant
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
model_config = ConfigDict(extra="allow")
|
|
38
|
+
id: str # noqa VNE003
|
|
39
|
+
spaces: Dict[str, Space]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Tenants(RootModel):
|
|
43
|
+
"""Collection of loading log entries
|
|
44
|
+
|
|
45
|
+
Attributes:
|
|
46
|
+
root: Collection of Loadings without key
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
root: Dict[str, Tenant]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .keycloak import KeycloakClient
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
import httpx
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class KeycloakClient:
|
|
8
|
+
"""
|
|
9
|
+
A client for handling Keycloak authentication and token management.
|
|
10
|
+
|
|
11
|
+
This class manages OAuth2/OpenID Connect authentication with Keycloak,
|
|
12
|
+
including token acquisition, refresh, and management.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
openid_url (str): The OpenID configuration URL for the Keycloak server
|
|
16
|
+
username (str): Username for authentication
|
|
17
|
+
password (str): Password for authentication
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, openid_url: str, username: str, password: str) -> None:
|
|
21
|
+
"""
|
|
22
|
+
Initialize the KeycloakClient with authentication credentials.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
openid_url (str): The OpenID configuration URL for the Keycloak server
|
|
26
|
+
username (str): Username for authentication
|
|
27
|
+
password (str): Password for authentication
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
self._client = httpx.Client()
|
|
31
|
+
self.OPENID_URL = openid_url
|
|
32
|
+
self.log = logging.getLogger("KeycloakClient")
|
|
33
|
+
self._auth_endpoint = None
|
|
34
|
+
self._token_endpoint = None
|
|
35
|
+
self.access_token = None
|
|
36
|
+
self.access_token_expires = None
|
|
37
|
+
self.refresh_token = None
|
|
38
|
+
self._get_openid_configuration()
|
|
39
|
+
self._authenticate(username=username, password=password)
|
|
40
|
+
|
|
41
|
+
def _get_openid_configuration(self) -> None:
|
|
42
|
+
"""
|
|
43
|
+
Retrieve OpenID Connect configuration from Keycloak server.
|
|
44
|
+
|
|
45
|
+
This method fetches the authorization and token endpoints from the
|
|
46
|
+
Keycloak OpenID configuration.
|
|
47
|
+
|
|
48
|
+
Raises:
|
|
49
|
+
ConnectError: If connection to the Keycloak server fails.
|
|
50
|
+
"""
|
|
51
|
+
self.log.info(f"Getting openid configuration from {self.OPENID_URL}")
|
|
52
|
+
try:
|
|
53
|
+
|
|
54
|
+
response = self._client.get(self.OPENID_URL)
|
|
55
|
+
self._auth_endpoint = response.json()["authorization_endpoint"]
|
|
56
|
+
self._token_endpoint = response.json()["token_endpoint"]
|
|
57
|
+
|
|
58
|
+
except httpx.ConnectError as e:
|
|
59
|
+
raise httpx.ConnectError(f"Failed to get openid configuration: {e}")
|
|
60
|
+
|
|
61
|
+
def _authenticate(self, username: str, password: str) -> None:
|
|
62
|
+
"""
|
|
63
|
+
Perform initial authentication with Keycloak using username and password.
|
|
64
|
+
|
|
65
|
+
This method obtains the initial access and refresh tokens using the
|
|
66
|
+
provided credentials.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
username (str): Username for authentication
|
|
70
|
+
password (str): Password for authentication
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
self.log.info(f"Authenticating with {self._auth_endpoint}")
|
|
76
|
+
response = self._client.post(
|
|
77
|
+
self._token_endpoint,
|
|
78
|
+
data={
|
|
79
|
+
"grant_type": "password",
|
|
80
|
+
"client_id": "stat-suite",
|
|
81
|
+
"username": username,
|
|
82
|
+
"password": password,
|
|
83
|
+
},
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
self.refresh_token = response.json()["refresh_token"]
|
|
87
|
+
self.access_token = response.json()["access_token"]
|
|
88
|
+
self.access_token_expires = datetime.datetime.now() + datetime.timedelta(
|
|
89
|
+
seconds=response.json()["expires_in"]
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def trigger_refresh_token(self) -> None:
|
|
93
|
+
"""
|
|
94
|
+
Refresh the access token using the refresh token.
|
|
95
|
+
|
|
96
|
+
This method is called when the access token is expired or about to expire.
|
|
97
|
+
It uses the refresh token to obtain a new access token and refresh token pair.
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
self.log.info("Triggering refresh token")
|
|
103
|
+
response = self._client.post(
|
|
104
|
+
self._token_endpoint,
|
|
105
|
+
data={
|
|
106
|
+
"grant_type": "refresh_token",
|
|
107
|
+
"client_id": "stat-suite",
|
|
108
|
+
"refresh_token": self.refresh_token,
|
|
109
|
+
},
|
|
110
|
+
)
|
|
111
|
+
self.access_token = response.json()["access_token"]
|
|
112
|
+
self.access_token_expires = datetime.datetime.now() + datetime.timedelta(
|
|
113
|
+
seconds=response.json()["expires_in"]
|
|
114
|
+
)
|
|
115
|
+
self.refresh_token = response.json()["refresh_token"]
|
|
116
|
+
|
|
117
|
+
def get_access_token(self) -> str:
|
|
118
|
+
"""
|
|
119
|
+
Get the current valid access token.
|
|
120
|
+
|
|
121
|
+
This method checks if the current access token is valid and not expired.
|
|
122
|
+
If expired, it automatically triggers a token refresh.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Access token string
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
self.log.info("Getting access token")
|
|
129
|
+
if (
|
|
130
|
+
self.access_token_expires is None
|
|
131
|
+
or self.access_token_expires < datetime.datetime.now() # noqa W503
|
|
132
|
+
):
|
|
133
|
+
self.trigger_refresh_token()
|
|
134
|
+
|
|
135
|
+
return self.access_token
|
|
136
|
+
|
|
137
|
+
def auth_header(self) -> dict:
|
|
138
|
+
"""
|
|
139
|
+
Create an authorization header using the current access token.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
dict: A dictionary containing the Authorization header with the Bearer token
|
|
143
|
+
in the format {'Authorization': 'Bearer <token>'}
|
|
144
|
+
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
return {"Authorization": f"Bearer {self.get_access_token()}"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .nsi import NSIClient
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
|
|
5
|
+
from ..keycloak.keycloak import KeycloakClient
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class NSIClient:
|
|
9
|
+
"""Client for interacting with the NSI (Network Service Interface) API.
|
|
10
|
+
|
|
11
|
+
This client handles file operations (upload, download, delete) through the NSI service
|
|
12
|
+
with authentication handled by a Keycloak client.
|
|
13
|
+
|
|
14
|
+
Attributes:
|
|
15
|
+
NSI_URL (str): Base URL for the NSI service.
|
|
16
|
+
log: Logger instance for the NSIClient.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
nsi_url (str): Base URL of the NSI service.
|
|
20
|
+
keycloak_client (KeycloakClient): Client for handling Keycloak authentication.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, nsi_url: str, keycloak_client: KeycloakClient) -> None:
|
|
24
|
+
"""Initialize the NSIClient.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
nsi_url (str): Base URL of the NSI service.
|
|
28
|
+
keycloak_client (KeycloakClient): Initialized Keycloak client for authentication.
|
|
29
|
+
"""
|
|
30
|
+
self._client = httpx.Client()
|
|
31
|
+
self.NSI_URL = nsi_url
|
|
32
|
+
self._keycloak_client = keycloak_client
|
|
33
|
+
self.log = logging.getLogger("NSIClient")
|
|
34
|
+
|
|
35
|
+
def put(self, file_to_upload, path: str, timeout: int = None) -> int:
|
|
36
|
+
"""Upload a file to the NSI service.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
file_to_upload: File content to upload.
|
|
40
|
+
path (str): Target path on the NSI service.
|
|
41
|
+
timeout (int, optional): Request timeout in seconds. Defaults to None.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
int: HTTP status code of the upload response.
|
|
45
|
+
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
headers = self._keycloak_client.auth_header() | {
|
|
49
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
self.log.info(f"Uploading to NSI: {self.NSI_URL + path}")
|
|
53
|
+
|
|
54
|
+
response = httpx.post(
|
|
55
|
+
self.NSI_URL + path,
|
|
56
|
+
content=file_to_upload,
|
|
57
|
+
headers=headers,
|
|
58
|
+
timeout=timeout,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if response.status_code != 207:
|
|
62
|
+
self.log.info(f"NSI response: {response.text}")
|
|
63
|
+
self.log.info(response.text)
|
|
64
|
+
response.raise_for_status()
|
|
65
|
+
return response.status_code
|
|
66
|
+
|
|
67
|
+
def get(self, path: str, headers: dict = {}, timeout: int = None) -> httpx.Response:
|
|
68
|
+
"""Retrieve a file or resource from the NSI service.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
path (str): Path to the resource on the NSI service.
|
|
72
|
+
headers (dict, optional): Additional HTTP headers to include. Defaults to {}.
|
|
73
|
+
timeout (int, optional): Request timeout in seconds. Defaults to None.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
httpx.Response: Response object containing the requested resource.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
headers |= self._keycloak_client.auth_header()
|
|
80
|
+
self.log.info(f"Getting from NSI: {self.NSI_URL + path}")
|
|
81
|
+
resp = httpx.get(self.NSI_URL + path, headers=headers, timeout=timeout)
|
|
82
|
+
resp.raise_for_status()
|
|
83
|
+
return resp
|
|
84
|
+
|
|
85
|
+
def delete(self, path: str, timeout: int = None) -> int:
|
|
86
|
+
"""Delete a file or resource from the NSI service.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
path (str): Path to the resource to delete on the NSI service.
|
|
90
|
+
timeout (int, optional): Request timeout in seconds. Defaults to None.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
int: HTTP status code of the delete response.
|
|
94
|
+
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
headers = self._keycloak_client.auth_header()
|
|
98
|
+
self.log.info(f"Deleting from NSI: {self.NSI_URL + path}")
|
|
99
|
+
response = httpx.delete(self.NSI_URL + path, headers=headers, timeout=timeout)
|
|
100
|
+
response.raise_for_status()
|
|
101
|
+
return response.status_code
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .sfs import SFSClient
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from typing import Dict, List, Optional
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, ConfigDict, RootModel
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Index(RootModel):
|
|
7
|
+
"""Model for return json from indexing request on SFS
|
|
8
|
+
|
|
9
|
+
Attributes:
|
|
10
|
+
root: returns a json in the format { "loadingId": ######### }
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
root: Dict[str, int]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class LoadingLog(BaseModel):
|
|
17
|
+
"""Minimun model for Loading Log entity
|
|
18
|
+
|
|
19
|
+
Attributes:
|
|
20
|
+
model_config: Configuration
|
|
21
|
+
executionStart: Timestamp start of the task
|
|
22
|
+
executionStatus: Status of the task
|
|
23
|
+
id: Loadingid
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
model_config = ConfigDict(extra="allow")
|
|
27
|
+
executionStart: str
|
|
28
|
+
executionStatus: Optional[str] = None
|
|
29
|
+
id: int # noqa
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class LoadingLogs(RootModel):
|
|
33
|
+
"""Collection of loading log entries
|
|
34
|
+
|
|
35
|
+
Attributes:
|
|
36
|
+
root: Collection of Loadings without key
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
root: List[LoadingLog]
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import time
|
|
3
|
+
from enum import IntEnum
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
from .models import LoadingLog, LoadingLogs
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SFSClient:
|
|
12
|
+
"""Client for the SDMX Faceted search service"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, sfs_url: str, sfs_api_key: str) -> None:
|
|
15
|
+
"""Inits the client
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
sfs_url: Endpoint url for SFS service.
|
|
19
|
+
sfs_api_key: API key for the SFS service
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
self._client = httpx.Client()
|
|
23
|
+
self.SFS_URL = sfs_url
|
|
24
|
+
self._sfs_api_key = sfs_api_key
|
|
25
|
+
self.log = logging.getLogger("SFSClient")
|
|
26
|
+
self.log.level = logging.INFO
|
|
27
|
+
|
|
28
|
+
def index(self, tenant: str = "default") -> str:
|
|
29
|
+
"""Triggers a new indexing task
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
tenant: (str) tenant to trigger the index for
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
loadingId(str)
|
|
36
|
+
"""
|
|
37
|
+
resp = httpx.post(
|
|
38
|
+
f"{self.SFS_URL}/admin/dataflows?api-key={self._sfs_api_key}&tenant={tenant}" # noqa
|
|
39
|
+
)
|
|
40
|
+
if resp.status_code == 200:
|
|
41
|
+
from .models import Index
|
|
42
|
+
|
|
43
|
+
loading = Index.model_validate(resp.json())
|
|
44
|
+
return loading.root.get("loadingId")
|
|
45
|
+
|
|
46
|
+
class LoadingStatus(IntEnum):
|
|
47
|
+
"""Enum class to represent status of the loading tasks
|
|
48
|
+
|
|
49
|
+
Attributes:
|
|
50
|
+
COMPLETED: Task finished succesfully
|
|
51
|
+
RETRY: Some recoverable error happened or the task is in a non-complete
|
|
52
|
+
status
|
|
53
|
+
FAILED: Unrecoverable error
|
|
54
|
+
BUG502: There si a bug retrieving loading status that can be retrieve
|
|
55
|
+
fetching the logs without id query param
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
COMPLETED = 1
|
|
59
|
+
RETRY = 2
|
|
60
|
+
FAILED = 3
|
|
61
|
+
BUG502 = 10
|
|
62
|
+
|
|
63
|
+
def get_log(self, tenant: str, loading_id: str) -> Optional[LoadingLog]:
|
|
64
|
+
"""Get log and status from by loading_id, if retrieving a log by id
|
|
65
|
+
fails with a 502 it will fallback to retrieve all available logs and
|
|
66
|
+
filter them
|
|
67
|
+
|
|
68
|
+
Arguments:
|
|
69
|
+
tenant: (str) .stat tenant
|
|
70
|
+
loading_id: (str) id of the loading
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
LoadingLog or None if the loading_id cannot be found
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
resp = httpx.get(
|
|
77
|
+
url=f"{self.SFS_URL}/admin/logs?api-key={self._sfs_api_key}&tenant={tenant}"
|
|
78
|
+
)
|
|
79
|
+
if resp.status_code == 200:
|
|
80
|
+
return LoadingLog.model_validate(resp.json())
|
|
81
|
+
if resp.status_code == 502:
|
|
82
|
+
self.log.error("Error 502 getting loading log, using expensive query")
|
|
83
|
+
resp = httpx.get(
|
|
84
|
+
f"{self.SFS_URL}/admin/logs?api-key={self._sfs_api_key}&tenant={tenant}"
|
|
85
|
+
) # noqa
|
|
86
|
+
loadings = LoadingLogs.model_validate(resp.json())
|
|
87
|
+
for loading in loadings.root:
|
|
88
|
+
if str(loading.id) == loading_id:
|
|
89
|
+
return loading
|
|
90
|
+
self.log.error(f"Error gathering logs {resp.text}")
|
|
91
|
+
|
|
92
|
+
def check_status_loading(self, tenant: str, loading_id: str) -> LoadingStatus:
|
|
93
|
+
"""Check the status of a loading taks
|
|
94
|
+
|
|
95
|
+
Arguments:
|
|
96
|
+
tenant: (str) .stat tenant
|
|
97
|
+
loading_id: (str) id of the loading
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
LoadingStatus enumeration
|
|
101
|
+
"""
|
|
102
|
+
loading = self.get_log(tenant=tenant, loading_id=loading_id)
|
|
103
|
+
if loading.executionStatus == "completed":
|
|
104
|
+
return self.LoadingStatus.COMPLETED
|
|
105
|
+
else:
|
|
106
|
+
self.log.error(f"Mapping outcome of {loading.executionStatus} to RETRY")
|
|
107
|
+
return self.LoadingStatus.RETRY
|
|
108
|
+
|
|
109
|
+
def wait_for_index_to_finish( # noqa FNE005
|
|
110
|
+
self,
|
|
111
|
+
tenant: str,
|
|
112
|
+
loading_id: str,
|
|
113
|
+
startup_sleep: int = 0,
|
|
114
|
+
timeout: int = 600,
|
|
115
|
+
backoff: int = 30,
|
|
116
|
+
) -> bool:
|
|
117
|
+
"""This method will periodically check the status of a loading task and
|
|
118
|
+
until it finishes, error or expires the timeout
|
|
119
|
+
|
|
120
|
+
Arguments:
|
|
121
|
+
tenant: (str) .stat tenant
|
|
122
|
+
loading_id: (str) id of the loading
|
|
123
|
+
startup_sleep: (int) grace period (secs) before start fetching the
|
|
124
|
+
loading state
|
|
125
|
+
timeout: (int) max time (secs) the loop will be running
|
|
126
|
+
backoff: (int) time between iterations fetching the data
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
boolean stating if the loading task finished correctly
|
|
130
|
+
"""
|
|
131
|
+
start = time.time()
|
|
132
|
+
time.sleep(startup_sleep)
|
|
133
|
+
|
|
134
|
+
self.log.info("STARTING")
|
|
135
|
+
while True:
|
|
136
|
+
status = self.check_status_loading(tenant=tenant, loading_id=loading_id)
|
|
137
|
+
if status == self.LoadingStatus.COMPLETED:
|
|
138
|
+
return True
|
|
139
|
+
|
|
140
|
+
if time.time() - start > timeout:
|
|
141
|
+
self.log.error(
|
|
142
|
+
f"Timeout waiting for dataflows to be indexed {timeout} seconds passed" # noqa
|
|
143
|
+
)
|
|
144
|
+
return False
|
|
145
|
+
self.log.error(
|
|
146
|
+
"Still not information retrieved about the loading, waiting..."
|
|
147
|
+
)
|
|
148
|
+
time.sleep(backoff)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .transfer import TransferClient
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import time
|
|
3
|
+
|
|
4
|
+
import httpx
|
|
5
|
+
|
|
6
|
+
from statsuite_lib import KeycloakClient
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TransferClient:
|
|
10
|
+
"""
|
|
11
|
+
A client for handling SDMX file transfers and dataflow operations.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
transfer_url (str): Base URL for the transfer service
|
|
15
|
+
keycloak_client (KeycloakClient): Authentication client instance
|
|
16
|
+
api_version (str, optional): API version to use. Defaults to '3'
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self, transfer_url: str, keycloak_client: KeycloakClient, api_version: str = "3"
|
|
21
|
+
) -> None:
|
|
22
|
+
"""
|
|
23
|
+
Initialize the TransferClient.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
transfer_url (str): Base URL for the transfer service
|
|
27
|
+
keycloak_client (KeycloakClient): Authentication client instance
|
|
28
|
+
api_version (str, optional): API version to use. Defaults to '3'
|
|
29
|
+
"""
|
|
30
|
+
self._client = httpx.Client()
|
|
31
|
+
self.TRANSFER_URL = f"{transfer_url}/{api_version}"
|
|
32
|
+
self._keycloak_client = keycloak_client
|
|
33
|
+
self._log = logging.getLogger("TransferClient")
|
|
34
|
+
|
|
35
|
+
def import_sdmx_file(
|
|
36
|
+
self,
|
|
37
|
+
file_object,
|
|
38
|
+
dataspace: str,
|
|
39
|
+
target_version: int = 0,
|
|
40
|
+
restoration_option_required: bool = False,
|
|
41
|
+
validation_type: int = 1,
|
|
42
|
+
timeout: int = None,
|
|
43
|
+
) -> int:
|
|
44
|
+
"""
|
|
45
|
+
Import an SDMX file into the specified dataspace.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
file_object: The SDMX file to import
|
|
49
|
+
dataspace (str): Target dataspace name
|
|
50
|
+
target_version (int, optional): Version number for the import. Defaults to 0
|
|
51
|
+
restoration_option_required (bool, optional): Whether restoration is required. Defaults to False # noqa E501
|
|
52
|
+
validation_type (int, optional): Type of validation to perform. Defaults to 1
|
|
53
|
+
timeout (int, optional): Request timeout in seconds. Defaults to None
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
int: The ID of the import request
|
|
57
|
+
|
|
58
|
+
Raises:
|
|
59
|
+
httpx.HTTPError: If the request fails
|
|
60
|
+
|
|
61
|
+
Example:
|
|
62
|
+
for csv_file in sample_data_dir.rglob("*.csv"):
|
|
63
|
+
with open(csv_file, 'rb') as file:
|
|
64
|
+
id = transfer.import_sdmx_file(file=file, dataspace='design')
|
|
65
|
+
"""
|
|
66
|
+
data = {
|
|
67
|
+
"dataspace": dataspace,
|
|
68
|
+
"targetVersion": target_version,
|
|
69
|
+
"restorationOptionRequired": restoration_option_required,
|
|
70
|
+
"validationType": validation_type,
|
|
71
|
+
}
|
|
72
|
+
files = {"file": file_object}
|
|
73
|
+
url = f"{self.TRANSFER_URL}/import/sdmxFile"
|
|
74
|
+
resp = httpx.post(
|
|
75
|
+
url=url,
|
|
76
|
+
headers=self._keycloak_client.auth_header(),
|
|
77
|
+
data=data,
|
|
78
|
+
files=files,
|
|
79
|
+
timeout=timeout,
|
|
80
|
+
)
|
|
81
|
+
return resp.json().get("message").split(" ")[2]
|
|
82
|
+
|
|
83
|
+
def check_request_status(self, dataspace: str, id: int) -> str: # noqa VNE003
|
|
84
|
+
"""
|
|
85
|
+
Check the status of a request for a given dataspace and ID.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
dataspace (str): The dataspace name
|
|
89
|
+
id (int): The request ID to check
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
str: The execution status of the request
|
|
93
|
+
|
|
94
|
+
"""
|
|
95
|
+
self._log.info(f"Checking request status for dataspace {dataspace} and id {id}")
|
|
96
|
+
data = {"dataspace": dataspace, "id": id}
|
|
97
|
+
resp = httpx.post(
|
|
98
|
+
url=f"{self.TRANSFER_URL}/status/request",
|
|
99
|
+
headers=self._keycloak_client.auth_header(),
|
|
100
|
+
data=data,
|
|
101
|
+
)
|
|
102
|
+
return resp.json().get("executionStatus")
|
|
103
|
+
|
|
104
|
+
def wait_for_request(
|
|
105
|
+
self,
|
|
106
|
+
dataspace: str,
|
|
107
|
+
id: int, # noqa VNE003
|
|
108
|
+
timeout: int = 300,
|
|
109
|
+
backoff: int = 30, # noqa VNE003
|
|
110
|
+
) -> None:
|
|
111
|
+
"""
|
|
112
|
+
Wait for a request to complete with timeout and backoff mechanism.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
dataspace (str): The dataspace name
|
|
116
|
+
id (int): The request ID to wait for
|
|
117
|
+
timeout (int, optional): Maximum time to wait in seconds. Defaults to 300
|
|
118
|
+
backoff (int, optional): Time between status checks in seconds. Defaults to 30
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
bool: True if timeout occurred, False if request completed successfully
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
"""
|
|
125
|
+
start = time.time()
|
|
126
|
+
while True:
|
|
127
|
+
status = self.check_request_status(dataspace=dataspace, id=id)
|
|
128
|
+
if status == "Completed":
|
|
129
|
+
return False
|
|
130
|
+
|
|
131
|
+
if time.time() - start > timeout:
|
|
132
|
+
self._log.error(
|
|
133
|
+
f"Timeout waiting for request to be completed {timeout} seconds passed"
|
|
134
|
+
)
|
|
135
|
+
return True
|
|
136
|
+
time.sleep(backoff)
|
|
137
|
+
|
|
138
|
+
def transfer_dataflow(
|
|
139
|
+
self, source_dataspace: str, destination_dataspace: str, dataflow: str
|
|
140
|
+
):
|
|
141
|
+
"""
|
|
142
|
+
Transfer a dataflow from source dataspace to destination dataspace.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
source_dataspace (str): Source dataspace name
|
|
146
|
+
destination_dataspace (str): Destination dataspace name
|
|
147
|
+
dataflow (str): Name of the dataflow to transfer
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
str: The ID of the transfer request
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
"""
|
|
154
|
+
self._log.info(
|
|
155
|
+
f"Transferring dataflow {dataflow} from {source_dataspace} to {destination_dataspace}" # noqa E501
|
|
156
|
+
)
|
|
157
|
+
data = {
|
|
158
|
+
"sourceDataspace": source_dataspace,
|
|
159
|
+
"destinationDataspace": destination_dataspace,
|
|
160
|
+
"sourceDataflow": dataflow,
|
|
161
|
+
"destinationDataflow": dataflow,
|
|
162
|
+
"transferContent": 0,
|
|
163
|
+
"sourceVersion": 0,
|
|
164
|
+
"targetVersion": 0,
|
|
165
|
+
"restorationOptionRequired": False,
|
|
166
|
+
"validationType": 0,
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
resp = httpx.post(
|
|
170
|
+
url=f"{self.TRANSFER_URL}/transfer/dataflow",
|
|
171
|
+
headers=self._keycloak_client.auth_header(),
|
|
172
|
+
data=data,
|
|
173
|
+
)
|
|
174
|
+
return resp.json().get("message").split(" ")[2]
|
|
175
|
+
|
|
176
|
+
def get_tune(self, dataspace: str, dsd_id: str):
|
|
177
|
+
"""
|
|
178
|
+
Retrieve tune information for a specific DSD in a dataspace.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
dataspace (str): The dataspace name
|
|
182
|
+
dsd_id (str): The ID of the Data Structure Definition
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
dict: Tune information for the specified DSD
|
|
186
|
+
|
|
187
|
+
"""
|
|
188
|
+
self._log.info(f"Getting DSD {dsd_id} tune information in ds {dataspace}")
|
|
189
|
+
data = {"dataspace": dataspace, "dsd": dsd_id}
|
|
190
|
+
resp = httpx.post(
|
|
191
|
+
url=f"{self.TRANSFER_URL}/tune/info",
|
|
192
|
+
headers=self._keycloak_client.auth_header(),
|
|
193
|
+
data=data,
|
|
194
|
+
)
|
|
195
|
+
return resp.json()
|
|
196
|
+
|
|
197
|
+
def set_tune(self, dataspace: str, dsd_id: str, index_type: int):
|
|
198
|
+
"""
|
|
199
|
+
Set tune parameters for a specific DSD in a dataspace.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
dataspace (str): The dataspace name
|
|
203
|
+
dsd_id (str): The ID of the Data Structure Definition
|
|
204
|
+
index_type (int): The type of index to set
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
dict: Response containing the result of the tune operation
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
"""
|
|
211
|
+
self._log.info(f"Getting DSD {dsd_id} tune information in ds {dataspace}")
|
|
212
|
+
data = {"dataspace": dataspace, "dsd": dsd_id, "indexType": index_type}
|
|
213
|
+
resp = httpx.post(
|
|
214
|
+
url=f"{self.TRANSFER_URL}/tune/dsd",
|
|
215
|
+
headers=self._keycloak_client.auth_header(),
|
|
216
|
+
data=data,
|
|
217
|
+
)
|
|
218
|
+
return resp.json()
|