dotstat_io 1.0.7__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.
@@ -0,0 +1,181 @@
1
+ Metadata-Version: 2.4
2
+ Name: dotstat_io
3
+ Version: 1.0.7
4
+ Summary: Utility to download or upload data from/to .Stat Suite using ADFS authentication to connect to it
5
+ License: MIT
6
+ Author: Gyorgy Gyomai
7
+ Author-email: gyorgy.gyomai@oecd.org
8
+ Requires-Python: >=3.10,<4.0
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Requires-Dist: chardet (>=5.1.0,<6.0.0)
17
+ Requires-Dist: msal (>=1.22.0,<2.0.0)
18
+ Requires-Dist: requests (>=2.29.0,<3.0.0)
19
+ Requires-Dist: requests-kerberos (>=0.14.0,<0.15.0)
20
+ Requires-Dist: setuptools (>=75.6.0,<76.0.0)
21
+ Description-Content-Type: text/markdown
22
+
23
+ ### DotStat_IO package:
24
+ A generic python package which could be integrated with other end-user applications and Gitlab runner to perform basic io with .Stat Suite.
25
+ Its role is to mask the complexities of authentication to connect to .Stat Suite.
26
+ The user needs to provide a set of parameters and the package exposes a couple of methods which will download or upload data from/to .Stat Suite.
27
+
28
+ ### This package contains three modules:
29
+ - ADFSAuthentication module
30
+ - KeycloakAuthentication module
31
+ - Client module
32
+
33
+ ### In ADFSAuthentication module, four methods are available:
34
+ #### 1. To initialise the module for interactive use:
35
+ ```python
36
+ from dotstat_io.authentication import AdfsAuthentication
37
+ interactive_obj = AdfsAuthentication.interactive(
38
+ client_id=client_id,
39
+ sdmx_resource_id=sdmx_resource_id,
40
+ scopes=scopes,
41
+ authority_url=authority_url,
42
+ redirect_port=redirect_port)
43
+
44
+ ```
45
+ * `client_id:` The Application (client) ID that the ADFS assigned to your app
46
+ * `sdmx_resource_id:` The ID of the application to be accessed such as .Stat Suite
47
+ * `scopes:` Scopes requested to access a protected API (a resource defined by sdmx_resource_id)
48
+ * `authority_url:` URL that identifies a token authority
49
+ * `redirect_port:` The port of the address to return to upon receiving a response from the authority
50
+
51
+ #### 2. To initialise the module for non-interactive use using a secret:
52
+ ```python
53
+ from dotstat_io.authentication import AdfsAuthentication
54
+ noninteractive_with_secret_obj = AdfsAuthentication.noninteractive_with_secret(
55
+ client_id=client_id,
56
+ sdmx_resource_id=sdmx_resource_id,
57
+ scopes=scopes,
58
+ authority_url=authority_url,
59
+ client_secret=client_secret)
60
+
61
+ ```
62
+ * `client_id:` The Application (client) ID that the ADFS assigned to your app
63
+ * `sdmx_resource_id:` The ID of the application to be accessed such as .Stat Suite
64
+ * `scopes:` Scopes requested to access a protected API (a resource defined by sdmx_resource_id)
65
+ * `authority_url:` URL that identifies a token authority
66
+ * `client_secret:` The application secret that you created during app registration in ADFS
67
+
68
+ #### 3. To initialise the module for non-interactive use using windows client authentication:
69
+ ```python
70
+ from dotstat_io.authentication import AdfsAuthentication
71
+ noninteractive_with_adfs_obj = AdfsAuthentication.noninteractive_with_adfs(
72
+ client_id=client_id,
73
+ sdmx_resource_id=sdmx_resource_id,
74
+ token_url=token_url)
75
+
76
+ ```
77
+ * `client_id:` The Application (client) ID that the ADFS assigned to your app
78
+ * `sdmx_resource_id:` The ID of the application to be accessed such as .Stat Suite
79
+ * `token_url:` URL of the authentication service
80
+
81
+ #### 4. To get a token after initialisation of "AdfsAuthentication" object as shown above:
82
+ ```python
83
+ access_token = [Authentication Object Name].get_token()
84
+ ```
85
+
86
+ ### In KeycloakAuthentication module, two methods are available:
87
+ #### 1. To initialise the module for Keycloak authentication:
88
+ ```python
89
+ from dotstat_io.authentication import KeycloakAuthentication
90
+ noninteractive_with_keycloak_obj = KeycloakAuthentication.noninteractive_with_secret(
91
+ user=user,
92
+ password=password,
93
+ token_url=token_url,
94
+ proxy=proxy)
95
+
96
+ ```
97
+ * `user:` User name for .Stat Suite authentication
98
+ * `password:` User name's password
99
+ * `token_url:` URL of the authentication service
100
+ * `proxy:` URL of the SSL certificates
101
+
102
+ #### 2. To get a token after initialisation of "KeycloakAuthentication" object as shown above:
103
+ ```python
104
+ access_token = [Authentication Object Name].get_token()
105
+ ```
106
+
107
+ ### In Client module, six methods are available:
108
+ #### 1. To initialise the module using an Authentication object type AdfsAuthentication or KeycloakAuthentication:
109
+ ```python
110
+ from dotstat_io.client import Client
111
+ client_obj = Client.init_with_authentication_obj(Authentication_obj)
112
+ ```
113
+ * `Authentication_obj:` An initialized authentication object type AdfsAuthentication or KeycloakAuthentication
114
+
115
+ #### 2. To initialise the module using an access token:
116
+ ```python
117
+ from dotstat_io.client import Client
118
+ client_obj = Client.init_with_access_token(access_token)
119
+ ```
120
+ * `access_token:` An access token to make requests on .Stat Suite services (nsiws) using the authorisation service and underlying permission rules
121
+
122
+ #### 3. To download a file from .Stat Suite:
123
+ ```python
124
+ from dotstat_io.client import Client
125
+ client_obj = Client.init_with_authentication_obj(Authentication_obj)
126
+ returned_result = client_obj.download_data_file(
127
+ dotstat_url, content_format, Path(file_path))
128
+ ```
129
+ * `dotstat_url:` URL of data to be downloaded from .Stat Suite
130
+ * `content_format:` Format of the downloaded content
131
+ * `file_path:` The full path where the file will downloaded
132
+
133
+ #### 4. To download streamed content from .Stat Suite:
134
+ ```python
135
+ from dotstat_io.client import Client
136
+ client_obj = Client.init_with_authentication_obj(Authentication_obj)
137
+ returned_result = client_obj.download_data_stream(
138
+ dotstat_url, content_format)
139
+ ```
140
+ * `dotstat_url:` URL of data to be downloaded from .Stat Suite
141
+ * `content_format:` Format of the downloaded content
142
+
143
+ #### 5. To upload a data file to .Stat Suite:
144
+ ```python
145
+ from dotstat_io.client import Client
146
+ client_obj = Client.init_with_authentication_obj(Authentication_obj)
147
+ returned_result = client_obj.upload_data_file(
148
+ transfer_url, Path(file_path), space, validationType, use_filepath, optimize)
149
+ ```
150
+ * `transfer_url:` URL of the transfer service
151
+ * `file_path:` The full path of the SDMX-CSV file to be imported
152
+ * `space:` Data space where the file will be uploaded
153
+ * `validationType:` The type of validation to use during upload. Possible values: Basic Validation (0), Advanced Validation (1)
154
+ * `use_filepath:` Use a file path of a shared folder accessible by the .Stat Suite data upload engine (for unlimited file sizes)
155
+ * `optimize:` Controls whether optimization is applied after data changes
156
+
157
+ #### 6. To upload an Excel data file to .Stat Suite:
158
+ ```python
159
+ from dotstat_io.client import Client
160
+ client_obj = Client.init_with_authentication_obj(Authentication_obj)
161
+ returned_result = client_obj.upload_excel_data_file(
162
+ transfer_url, Path(excelfile_path), Path(eddfile_path), space, validationType)
163
+ ```
164
+ * `transfer_url:` URL of the transfer service
165
+ * `excelfile_path:` The full path of the Excel file containing the data/referential metadata values to be imported
166
+ * `eddfile_path:` The full path of the XML edd file containing the description of the Excel file to be imported
167
+ * `space:` Data space where the file will be uploaded
168
+ * `validationType:` The type of validation to use during upload. Possible values: Basic Validation (0), Advanced Validation (1)
169
+
170
+ #### 7. To upload a structure to .Stat Suite:
171
+ ```python
172
+ from dotstat_io.client import Client
173
+ client_obj = Client.init_with_authentication_obj(Authentication_obj)
174
+ returned_result = client_obj.upload_structure(
175
+ transfer_url, Path(file_path))
176
+ ```
177
+ * `transfer_url:` URL of the transfer service
178
+ * `file_path:` The full path of the SDMX-ML file to be imported
179
+
180
+ ### For more information about how to use this package, all test cases can be accessed from this [`link`](https://gitlab.algobank.oecd.org/SD_ENGINEERING/dotstat_io/dotstat_io-package/-/blob/main/tests/test_cases.py)
181
+
@@ -0,0 +1,158 @@
1
+ ### DotStat_IO package:
2
+ A generic python package which could be integrated with other end-user applications and Gitlab runner to perform basic io with .Stat Suite.
3
+ Its role is to mask the complexities of authentication to connect to .Stat Suite.
4
+ The user needs to provide a set of parameters and the package exposes a couple of methods which will download or upload data from/to .Stat Suite.
5
+
6
+ ### This package contains three modules:
7
+ - ADFSAuthentication module
8
+ - KeycloakAuthentication module
9
+ - Client module
10
+
11
+ ### In ADFSAuthentication module, four methods are available:
12
+ #### 1. To initialise the module for interactive use:
13
+ ```python
14
+ from dotstat_io.authentication import AdfsAuthentication
15
+ interactive_obj = AdfsAuthentication.interactive(
16
+ client_id=client_id,
17
+ sdmx_resource_id=sdmx_resource_id,
18
+ scopes=scopes,
19
+ authority_url=authority_url,
20
+ redirect_port=redirect_port)
21
+
22
+ ```
23
+ * `client_id:` The Application (client) ID that the ADFS assigned to your app
24
+ * `sdmx_resource_id:` The ID of the application to be accessed such as .Stat Suite
25
+ * `scopes:` Scopes requested to access a protected API (a resource defined by sdmx_resource_id)
26
+ * `authority_url:` URL that identifies a token authority
27
+ * `redirect_port:` The port of the address to return to upon receiving a response from the authority
28
+
29
+ #### 2. To initialise the module for non-interactive use using a secret:
30
+ ```python
31
+ from dotstat_io.authentication import AdfsAuthentication
32
+ noninteractive_with_secret_obj = AdfsAuthentication.noninteractive_with_secret(
33
+ client_id=client_id,
34
+ sdmx_resource_id=sdmx_resource_id,
35
+ scopes=scopes,
36
+ authority_url=authority_url,
37
+ client_secret=client_secret)
38
+
39
+ ```
40
+ * `client_id:` The Application (client) ID that the ADFS assigned to your app
41
+ * `sdmx_resource_id:` The ID of the application to be accessed such as .Stat Suite
42
+ * `scopes:` Scopes requested to access a protected API (a resource defined by sdmx_resource_id)
43
+ * `authority_url:` URL that identifies a token authority
44
+ * `client_secret:` The application secret that you created during app registration in ADFS
45
+
46
+ #### 3. To initialise the module for non-interactive use using windows client authentication:
47
+ ```python
48
+ from dotstat_io.authentication import AdfsAuthentication
49
+ noninteractive_with_adfs_obj = AdfsAuthentication.noninteractive_with_adfs(
50
+ client_id=client_id,
51
+ sdmx_resource_id=sdmx_resource_id,
52
+ token_url=token_url)
53
+
54
+ ```
55
+ * `client_id:` The Application (client) ID that the ADFS assigned to your app
56
+ * `sdmx_resource_id:` The ID of the application to be accessed such as .Stat Suite
57
+ * `token_url:` URL of the authentication service
58
+
59
+ #### 4. To get a token after initialisation of "AdfsAuthentication" object as shown above:
60
+ ```python
61
+ access_token = [Authentication Object Name].get_token()
62
+ ```
63
+
64
+ ### In KeycloakAuthentication module, two methods are available:
65
+ #### 1. To initialise the module for Keycloak authentication:
66
+ ```python
67
+ from dotstat_io.authentication import KeycloakAuthentication
68
+ noninteractive_with_keycloak_obj = KeycloakAuthentication.noninteractive_with_secret(
69
+ user=user,
70
+ password=password,
71
+ token_url=token_url,
72
+ proxy=proxy)
73
+
74
+ ```
75
+ * `user:` User name for .Stat Suite authentication
76
+ * `password:` User name's password
77
+ * `token_url:` URL of the authentication service
78
+ * `proxy:` URL of the SSL certificates
79
+
80
+ #### 2. To get a token after initialisation of "KeycloakAuthentication" object as shown above:
81
+ ```python
82
+ access_token = [Authentication Object Name].get_token()
83
+ ```
84
+
85
+ ### In Client module, six methods are available:
86
+ #### 1. To initialise the module using an Authentication object type AdfsAuthentication or KeycloakAuthentication:
87
+ ```python
88
+ from dotstat_io.client import Client
89
+ client_obj = Client.init_with_authentication_obj(Authentication_obj)
90
+ ```
91
+ * `Authentication_obj:` An initialized authentication object type AdfsAuthentication or KeycloakAuthentication
92
+
93
+ #### 2. To initialise the module using an access token:
94
+ ```python
95
+ from dotstat_io.client import Client
96
+ client_obj = Client.init_with_access_token(access_token)
97
+ ```
98
+ * `access_token:` An access token to make requests on .Stat Suite services (nsiws) using the authorisation service and underlying permission rules
99
+
100
+ #### 3. To download a file from .Stat Suite:
101
+ ```python
102
+ from dotstat_io.client import Client
103
+ client_obj = Client.init_with_authentication_obj(Authentication_obj)
104
+ returned_result = client_obj.download_data_file(
105
+ dotstat_url, content_format, Path(file_path))
106
+ ```
107
+ * `dotstat_url:` URL of data to be downloaded from .Stat Suite
108
+ * `content_format:` Format of the downloaded content
109
+ * `file_path:` The full path where the file will downloaded
110
+
111
+ #### 4. To download streamed content from .Stat Suite:
112
+ ```python
113
+ from dotstat_io.client import Client
114
+ client_obj = Client.init_with_authentication_obj(Authentication_obj)
115
+ returned_result = client_obj.download_data_stream(
116
+ dotstat_url, content_format)
117
+ ```
118
+ * `dotstat_url:` URL of data to be downloaded from .Stat Suite
119
+ * `content_format:` Format of the downloaded content
120
+
121
+ #### 5. To upload a data file to .Stat Suite:
122
+ ```python
123
+ from dotstat_io.client import Client
124
+ client_obj = Client.init_with_authentication_obj(Authentication_obj)
125
+ returned_result = client_obj.upload_data_file(
126
+ transfer_url, Path(file_path), space, validationType, use_filepath, optimize)
127
+ ```
128
+ * `transfer_url:` URL of the transfer service
129
+ * `file_path:` The full path of the SDMX-CSV file to be imported
130
+ * `space:` Data space where the file will be uploaded
131
+ * `validationType:` The type of validation to use during upload. Possible values: Basic Validation (0), Advanced Validation (1)
132
+ * `use_filepath:` Use a file path of a shared folder accessible by the .Stat Suite data upload engine (for unlimited file sizes)
133
+ * `optimize:` Controls whether optimization is applied after data changes
134
+
135
+ #### 6. To upload an Excel data file to .Stat Suite:
136
+ ```python
137
+ from dotstat_io.client import Client
138
+ client_obj = Client.init_with_authentication_obj(Authentication_obj)
139
+ returned_result = client_obj.upload_excel_data_file(
140
+ transfer_url, Path(excelfile_path), Path(eddfile_path), space, validationType)
141
+ ```
142
+ * `transfer_url:` URL of the transfer service
143
+ * `excelfile_path:` The full path of the Excel file containing the data/referential metadata values to be imported
144
+ * `eddfile_path:` The full path of the XML edd file containing the description of the Excel file to be imported
145
+ * `space:` Data space where the file will be uploaded
146
+ * `validationType:` The type of validation to use during upload. Possible values: Basic Validation (0), Advanced Validation (1)
147
+
148
+ #### 7. To upload a structure to .Stat Suite:
149
+ ```python
150
+ from dotstat_io.client import Client
151
+ client_obj = Client.init_with_authentication_obj(Authentication_obj)
152
+ returned_result = client_obj.upload_structure(
153
+ transfer_url, Path(file_path))
154
+ ```
155
+ * `transfer_url:` URL of the transfer service
156
+ * `file_path:` The full path of the SDMX-ML file to be imported
157
+
158
+ ### For more information about how to use this package, all test cases can be accessed from this [`link`](https://gitlab.algobank.oecd.org/SD_ENGINEERING/dotstat_io/dotstat_io-package/-/blob/main/tests/test_cases.py)
File without changes
@@ -0,0 +1,354 @@
1
+ import requests
2
+ import os
3
+ import msal
4
+ import requests_kerberos
5
+ import json
6
+ import time
7
+
8
+ from abc import ABC
9
+ from enum import IntEnum
10
+ from datetime import datetime
11
+
12
+ #
13
+ class AuthenticationMode(IntEnum):
14
+ INTERACTIVE = 1
15
+ NONINTERACTIVE_WITH_SECRET = 2
16
+ NONINTERACTIVE_WITH_ADFS = 3
17
+
18
+ # super class to manage authentication
19
+ class Authentication(ABC):
20
+
21
+ # protected variables
22
+ _access_token = None
23
+ _refresh_token = None
24
+ _creation_time = None
25
+ _expiration_time = None
26
+
27
+ # public variables
28
+ init_status = None
29
+
30
+ # Initialise authentication
31
+ def __init__(
32
+ self,
33
+ client_id: str,
34
+ mode: AuthenticationMode,
35
+ client_secret: str = None,
36
+ scopes: list[str] = [],
37
+ token_url: str = None,
38
+ sdmx_resource_id: str = None,
39
+ authority_url: str = None,
40
+ redirect_port: int = 3000,
41
+ user: str = None,
42
+ password: str = None,
43
+ proxy: str = None
44
+ ):
45
+ self._client_id = client_id
46
+ self._mode = mode
47
+ self._client_secret = client_secret
48
+ self._scopes = scopes
49
+ self._token_url = token_url
50
+ self._sdmx_resource_id = sdmx_resource_id
51
+ self._authority_url = authority_url
52
+ self._redirect_port = redirect_port
53
+ self._user = user
54
+ self._password = password
55
+
56
+ if proxy:
57
+ self._proxies = {
58
+ "http": proxy,
59
+ "https": proxy
60
+ }
61
+ else:
62
+ self._proxies = None
63
+
64
+ #
65
+ self._initialize_token()
66
+
67
+ #
68
+ def __enter__(self):
69
+ return self
70
+
71
+ #
72
+ def __exit__(self, exc_type, exc_value, traceback):
73
+ self._access_token = None
74
+ self._refresh_token = None
75
+ self._creation_time = None
76
+ self._expiration_time = None
77
+ self.init_status = None
78
+
79
+ #
80
+ def _initialize_token(self):
81
+ pass
82
+
83
+ #
84
+ def get_token(self):
85
+ if (self._access_token is None) \
86
+ or (datetime.fromtimestamp(self._expiration_time) is not None \
87
+ and datetime.now() >= datetime.fromtimestamp(self._expiration_time)):
88
+ self._initialize_token()
89
+
90
+ return self._access_token
91
+
92
+
93
+ # sub class to manage ADFS authentication using OIDC flows
94
+ class AdfsAuthentication(Authentication):
95
+ # private constants
96
+ __ERROR_OCCURRED = "An error occurred: "
97
+ __SUCCESSFUL_AUTHENTICATION = "Successful authentication"
98
+
99
+ #
100
+ def _initialize_token(self):
101
+ self._access_token = None
102
+ self._refresh_token = None
103
+ self._creation_time = None
104
+ self._expiration_time = None
105
+ self.init_status = None
106
+ match self._mode:
107
+ case AuthenticationMode.INTERACTIVE:
108
+ self._app = msal.PublicClientApplication(
109
+ self._client_id, authority=self._authority_url)
110
+ self.__acquire_token_interactive()
111
+ case AuthenticationMode.NONINTERACTIVE_WITH_SECRET:
112
+ self._app = msal.ConfidentialClientApplication(
113
+ self._client_id, authority=self._authority_url, client_credential=self._client_secret)
114
+ self.__acquire_token_noninteractive_with_secret()
115
+ case AuthenticationMode.NONINTERACTIVE_WITH_ADFS:
116
+ self.__acquire_token_noninteractive_with_adfs()
117
+
118
+ #
119
+ @classmethod
120
+ def interactive(
121
+ cls,
122
+ client_id: str,
123
+ sdmx_resource_id: str,
124
+ scopes: list[str],
125
+ authority_url: str,
126
+ redirect_port: int = 3000,
127
+ mode: AuthenticationMode = AuthenticationMode.INTERACTIVE
128
+ ):
129
+ return cls(
130
+ client_id=client_id,
131
+ sdmx_resource_id=sdmx_resource_id,
132
+ scopes=scopes,
133
+ authority_url=authority_url,
134
+ redirect_port=redirect_port,
135
+ mode=mode
136
+ )
137
+
138
+ #
139
+ @classmethod
140
+ def noninteractive_with_secret(
141
+ cls,
142
+ client_id: str,
143
+ sdmx_resource_id: str,
144
+ scopes: list[str],
145
+ authority_url: str,
146
+ client_secret: str,
147
+ mode: AuthenticationMode = AuthenticationMode.NONINTERACTIVE_WITH_SECRET
148
+ ):
149
+ return cls(
150
+ client_id=client_id,
151
+ sdmx_resource_id=sdmx_resource_id,
152
+ scopes=scopes,
153
+ authority_url=authority_url,
154
+ client_secret=client_secret,
155
+ mode=mode
156
+ )
157
+
158
+ #
159
+ @classmethod
160
+ def noninteractive_with_adfs(
161
+ cls,
162
+ client_id: str,
163
+ sdmx_resource_id: str,
164
+ token_url: str,
165
+ mode: AuthenticationMode = AuthenticationMode.NONINTERACTIVE_WITH_ADFS
166
+ ):
167
+ return cls(
168
+ client_id=client_id,
169
+ sdmx_resource_id=sdmx_resource_id,
170
+ token_url=token_url,
171
+ mode=mode)
172
+
173
+ # Authentication interactively - aka Authorization Code flow
174
+ def __acquire_token_interactive(self):
175
+ try:
176
+ # We now check the cache to see
177
+ # whether we already have some accounts that the end user already used to sign in before.
178
+ accounts = self._app.get_accounts()
179
+ if accounts:
180
+ account = accounts[0]
181
+ else:
182
+ account = None
183
+
184
+ # Firstly, looks up a access_token from cache, or using a refresh token
185
+ response_silent = self._app.acquire_token_silent(
186
+ self._scopes, account=account)
187
+ if not response_silent:
188
+ # Prompt the user to sign in interactively
189
+ response_interactive = self._app.acquire_token_interactive(
190
+ scopes=self._scopes, port=self._redirect_port)
191
+ if "access_token" in response_interactive:
192
+ self._access_token = response_interactive.get("access_token")
193
+ self._refresh_token = response_interactive.get("refresh_token")
194
+ self._creation_time = time.time()
195
+ self._expiration_time = time.time() + int(response_interactive.get("expires_in")) - 60 # one minute margin
196
+ self.init_status = self.__SUCCESSFUL_AUTHENTICATION
197
+ else:
198
+ self.init_status = f'{self.__ERROR_OCCURRED}{response_interactive.get("error")} Error description: {response_interactive.get("error_description")}'
199
+ else:
200
+ if "access_token" in response_silent:
201
+ self._access_token = response_silent.get("access_token")
202
+ self._refresh_token = response_silent.get("refresh_token")
203
+ self._creation_time = time.time()
204
+ self._expiration_time = time.time() + int(response_silent.get("expires_in")) - 60 # one minute margin
205
+ self.init_status = self.__SUCCESSFUL_AUTHENTICATION
206
+ else:
207
+ self.init_status = f'{self.__ERROR_OCCURRED}{response_silent.get("error")} Error description: {response_silent.get("error_description")}'
208
+ except Exception as err:
209
+ self.init_status = f'{self.__ERROR_OCCURRED}{err}\n'
210
+
211
+ # Authentication non-interactively using any account - aka Client Credentials flow
212
+ def __acquire_token_noninteractive_with_secret(self):
213
+ try:
214
+ response = self._app.acquire_token_for_client(scopes=self._scopes)
215
+ if "access_token" in response:
216
+ self._access_token = response.get("access_token")
217
+ self._creation_time = time.time()
218
+ self._expiration_time = time.time() + int(response.get("expires_in")) - 60 # one minute margin
219
+ self.init_status = self.__SUCCESSFUL_AUTHENTICATION
220
+ else:
221
+ self.init_status = f'{self.__ERROR_OCCURRED}{response.get("error")} Error description: {response.get("error_description")}'
222
+
223
+ except Exception as err:
224
+ self.init_status = f'{self.__ERROR_OCCURRED}{err}\n'
225
+
226
+ # Authentication non-interactively using service account - aka Windows Client Authentication
227
+ def __acquire_token_noninteractive_with_adfs(self):
228
+ try:
229
+ headers = {
230
+ "Content-Type": "application/x-www-form-urlencoded"
231
+ }
232
+
233
+ payload = {
234
+ 'client_id': self._client_id,
235
+ 'use_windows_client_authentication': 'true',
236
+ 'grant_type': 'client_credentials',
237
+ 'scope': 'openid',
238
+ 'resource': self._sdmx_resource_id
239
+ }
240
+
241
+ kerberos_auth = requests_kerberos.HTTPKerberosAuth(
242
+ mutual_authentication=requests_kerberos.OPTIONAL, force_preemptive=True)
243
+ response = requests.post(url=self._token_url, data=payload, auth=kerberos_auth)
244
+
245
+ # If the response object cannot be converted to json, return an error
246
+ results_json = None
247
+ try:
248
+ results_json = json.loads(response.text)
249
+ if response.status_code == 200:
250
+ self._access_token = results_json['access_token']
251
+ self._creation_time = time.time()
252
+ self._expiration_time = time.time() + int(results_json['expires_in']) - 60 # one minute margin
253
+ self.init_status = self.__SUCCESSFUL_AUTHENTICATION
254
+ else:
255
+ message = f'{self.__ERROR_OCCURRED} Error code: {response.status_code}'
256
+ if len(str(response.reason)) > 0:
257
+ message += os.linesep + 'Reason: ' + str(response.reason) + os.linesep
258
+ if len(response.text) > 0:
259
+ message += f'{self.__ERROR_OCCURRED}{results_json.get("error")} Error description: {results_json.get("error_description")}\n'
260
+
261
+ self.init_status = message
262
+ except ValueError as err:
263
+ self.init_status = self.__ERROR_OCCURRED + os.linesep
264
+ if len(str(response.status_code)) > 0:
265
+ self.init_status += 'Error code: ' + str(response.status_code) + os.linesep
266
+ if len(str(response.reason)) > 0:
267
+ self.init_status += 'Reason: ' + str(response.reason) + os.linesep
268
+ if len(response.text) > 0:
269
+ self.init_status += str(response.text)
270
+ else:
271
+ self.init_status += str(err)
272
+ except Exception as err:
273
+ self.init_status = f'{self.__ERROR_OCCURRED} {err}\n'
274
+
275
+
276
+ # sub class to manage Keycloak authentication
277
+ class KeycloakAuthentication(Authentication):
278
+ # private constants
279
+ __ERROR_OCCURRED = "An error occurred: "
280
+ __SUCCESSFUL_AUTHENTICATION = "Successful authentication"
281
+
282
+ #
283
+ def _initialize_token(self):
284
+ self._access_token = None
285
+ self._refresh_token = None
286
+ self._creation_time = None
287
+ self._expiration_time = None
288
+ self.init_status = None
289
+ match self._mode:
290
+ case AuthenticationMode.NONINTERACTIVE_WITH_SECRET:
291
+ self.__acquire_token_noninteractive_with_secret()
292
+
293
+ #
294
+ @classmethod
295
+ def noninteractive_with_secret(
296
+ cls,
297
+ token_url: str,
298
+ user: str,
299
+ password: str,
300
+ client_id: str = "app",
301
+ client_secret: str = "",
302
+ proxy: str | None = None,
303
+ scopes: list[str] = [],
304
+ mode: AuthenticationMode = AuthenticationMode.NONINTERACTIVE_WITH_SECRET
305
+ ):
306
+ return cls(
307
+ token_url=token_url,
308
+ user=user,
309
+ password=password,
310
+ client_id=client_id,
311
+ client_secret=client_secret,
312
+ scopes=scopes,
313
+ proxy=proxy,
314
+ mode=mode
315
+ )
316
+
317
+ # Authentication non-interactively using any account - aka Client Credentials flow
318
+ def __acquire_token_noninteractive_with_secret(self):
319
+ try:
320
+ payload = {
321
+ 'grant_type': 'password',
322
+ 'client_id': self._client_id,
323
+ 'client_secret': self._client_secret,
324
+ 'username': self._user,
325
+ 'password': self._password
326
+ }
327
+
328
+ headers = {"Content-Type": "application/x-www-form-urlencoded"}
329
+
330
+ response = requests.post(self._token_url, proxies=self._proxies, headers=headers, data=payload)
331
+ if response.status_code not in {200, 201}:
332
+ self.init_status = f'{self.__ERROR_OCCURRED}{response}'
333
+ else:
334
+ # If the response object cannot be converted to json, return an error
335
+ results_json = None
336
+ try:
337
+ results_json = json.loads(response.text)
338
+ self._access_token = results_json['access_token']
339
+ self._refresh_token = results_json['refresh_token']
340
+ self._creation_time = time.time()
341
+ self._expiration_time = time.time() + int(results_json['expires_in']) - 60 # one minute margin
342
+ self.init_status = self.__SUCCESSFUL_AUTHENTICATION
343
+ except ValueError as err:
344
+ self.init_status = self.__ERROR_OCCURRED + os.linesep
345
+ if len(str(response.status_code)) > 0:
346
+ self.init_status += 'Error code: ' + str(response.status_code) + os.linesep
347
+ if len(str(response.reason)) > 0:
348
+ self.init_status += 'Reason: ' + str(response.reason) + os.linesep
349
+ if len(response.text) > 0:
350
+ self.init_status += str(response.text)
351
+ else:
352
+ self.init_status += str(err)
353
+ except Exception as err:
354
+ self.init_status = f'{self.__ERROR_OCCURRED}{err}\n'
@@ -0,0 +1,552 @@
1
+ from enum import IntEnum
2
+ import os
3
+ import requests
4
+ import chardet
5
+ import json
6
+ import logging
7
+ import xml.etree.ElementTree as ET
8
+
9
+ from pathlib import Path
10
+ from time import sleep
11
+
12
+ from dotstat_io.authentication import Authentication
13
+
14
+ class ValidationType(IntEnum):
15
+ BASIC = 0
16
+ ADVANCED = 1
17
+
18
+
19
+ # class to download or upload data from/to .Stat Suite
20
+ class Client():
21
+
22
+ # private constants
23
+ __ERROR_OCCURRED = "An error occurred: "
24
+ __EXECUTION_IN_QUEUED = "Queued"
25
+ __EXECUTION_IN_PROGRESS = "InProgress"
26
+ __CONNECTION_ABORTED = "An existing connection was forcibly closed by the remote host"
27
+ __DOWNLOAD_SUCCESS = "Successful download"
28
+ __UPLOAD_SUCCESS = "The request was successfully processed "
29
+ __UPLOAD_FAILED = "The request failed with status code "
30
+
31
+ __NAMESPACE_MESSAGE = "{http://www.sdmx.org/resources/sdmxml/schemas/v2_1/message}"
32
+ __NAMESPACE_COMMON = "{http://www.sdmx.org/resources/sdmxml/schemas/v2_1/common}"
33
+
34
+ # private variables
35
+ __access_token = None
36
+ __authentication_obj = None
37
+
38
+ # Prepare logging format
39
+ FORMAT = '%(message)s'
40
+ logging.basicConfig(format=FORMAT, level=logging.INFO)
41
+ __log = logging.getLogger(__name__)
42
+
43
+ # Initialise Client
44
+ def __init__(self,
45
+ access_token: None | str = None,
46
+ authentication_obj: None | Authentication = None):
47
+ Client.__access_token = access_token
48
+ Client.__authentication_obj = authentication_obj
49
+
50
+
51
+ #
52
+ def __enter__(self):
53
+ return self
54
+
55
+ #
56
+ def __exit__(self, exc_type, exc_value, traceback):
57
+ self.__access_token = None
58
+ self.__authentication_obj = None
59
+
60
+
61
+ #
62
+ @classmethod
63
+ def init_with_access_token(
64
+ cls,
65
+ access_token: str
66
+ ):
67
+ return cls(
68
+ access_token=access_token
69
+ )
70
+
71
+ #
72
+ @classmethod
73
+ def init_with_authentication_obj(
74
+ cls,
75
+ authentication_obj: Authentication
76
+ ):
77
+ return cls(
78
+ authentication_obj=authentication_obj
79
+ )
80
+
81
+
82
+ # Download a data file from .Stat Suite
83
+ def download_data_file(self, dotstat_url: str, content_format: str, file_path: Path):
84
+ try:
85
+ returned_result = ""
86
+
87
+ #
88
+ if Client.__authentication_obj is not None:
89
+ Client.__access_token = Client.__authentication_obj.get_token()
90
+
91
+ headers = {
92
+ 'accept': content_format,
93
+ 'authorization': 'Bearer '+Client.__access_token
94
+ }
95
+
96
+ #
97
+ response = requests.get(dotstat_url, verify=True, headers=headers)
98
+ except Exception as err:
99
+ returned_result = Client.__ERROR_OCCURRED + str(err) + os.linesep
100
+
101
+ # Write the result to the log
102
+ for line in returned_result.split(os.linesep):
103
+ if len(line) > 0:
104
+ self.__log.info(' ' + line)
105
+ return returned_result
106
+ else:
107
+ if response.status_code != 200:
108
+ returned_result = Client.__ERROR_OCCURRED
109
+ if len(str(response.status_code)) > 0:
110
+ returned_result += 'Error code: ' + str(response.status_code) + os.linesep
111
+ if len(str(response.reason)) > 0:
112
+ returned_result += 'Reason: ' + str(response.reason) + os.linesep
113
+ if len(response.text) > 0:
114
+ returned_result += 'Text: ' + response.text
115
+
116
+ returned_result += os.linesep
117
+ else:
118
+ if os.path.isfile(file_path):
119
+ os.remove(file_path)
120
+ with open(file_path, "wb") as file:
121
+ file.write(response.content)
122
+ returned_result = Client.__DOWNLOAD_SUCCESS
123
+
124
+ # Write the result to the log
125
+ for line in returned_result.split(os.linesep):
126
+ if len(line) > 0:
127
+ self.__log.info(' ' + line)
128
+ return returned_result
129
+
130
+
131
+ # Download streamed content from .Stat Suite
132
+ def download_data_stream(self, dotstat_url: str, content_format: str):
133
+ try:
134
+ returned_result = ""
135
+
136
+ #
137
+ if Client.__authentication_obj is not None:
138
+ Client.__access_token = Client.__authentication_obj.get_token()
139
+
140
+ headers = {
141
+ 'accept': content_format,
142
+ 'Transfer-Encoding': 'chunked',
143
+ 'authorization': 'Bearer '+Client.__access_token
144
+ }
145
+
146
+ #
147
+ return requests.get(dotstat_url, verify=True, headers=headers, stream=True)
148
+ except Exception as err:
149
+ returned_result = Client.__ERROR_OCCURRED + str(err) + os.linesep
150
+ return returned_result
151
+
152
+
153
+ # Upload a data file to .Stat Suite
154
+ def upload_data_file(self,
155
+ transfer_url: str,
156
+ file_path: Path,
157
+ space: str,
158
+ validationType: int,
159
+ use_filepath: bool = False,
160
+ optimize: bool = True):
161
+ try:
162
+ returned_result = ""
163
+
164
+ #
165
+ if Client.__authentication_obj is not None:
166
+ Client.__access_token = Client.__authentication_obj.get_token()
167
+
168
+ payload = {
169
+ 'dataspace': space,
170
+ 'validationType': validationType,
171
+ 'optimize': optimize
172
+ }
173
+
174
+ headers = {
175
+ 'accept': 'application/json',
176
+ 'authorization': "Bearer "+Client.__access_token
177
+ }
178
+
179
+ if use_filepath:
180
+ files = {
181
+ 'dataspace': (None, payload['dataspace']),
182
+ 'validationType': (None, payload['validationType']),
183
+ 'optimize': (None, payload['optimize']),
184
+ 'filepath': (None, str(file_path))
185
+ }
186
+ else:
187
+ files = {
188
+ 'dataspace': (None, payload['dataspace']),
189
+ 'validationType': (None, payload['validationType']),
190
+ 'optimize': (None, payload['optimize']),
191
+ 'file': (os.path.realpath(file_path), open(os.path.realpath(file_path), 'rb'), 'text/csv', '')
192
+ }
193
+
194
+ #
195
+ response = requests.post(transfer_url, verify=True, headers=headers, files=files)
196
+ except Exception as err:
197
+ returned_result = Client.__ERROR_OCCURRED + str(err) + os.linesep
198
+
199
+ # Write the result to the log
200
+ for line in returned_result.split(os.linesep):
201
+ if len(line) > 0:
202
+ self.__log.info(' ' + line)
203
+ return returned_result
204
+ else:
205
+ # If the response object cannot be converted to json, return an error
206
+ results_json = None
207
+ try:
208
+ results_json = json.loads(response.text)
209
+ if response.status_code == 200:
210
+ result = results_json['message']
211
+ # Write the result to the log
212
+ for line in result.split(os.linesep):
213
+ if len(line) > 0:
214
+ self.__log.info(' ' + line)
215
+
216
+ returned_result = result + os.linesep
217
+
218
+ # Check the request status
219
+ if (result != "" and result.find(Client.__ERROR_OCCURRED ) == -1):
220
+ # Extract the request ID the returned message
221
+ start = 'with ID'
222
+ end = 'was successfully'
223
+ requestId = result[result.find(
224
+ start)+len(start):result.rfind(end)]
225
+
226
+ # Sleep a little bit before checking the request status
227
+ sleep(3)
228
+
229
+ # To avoid this error: maximum recursion depth exceeded while calling a Python object
230
+ # replace the recursive calls with while loops.
231
+ result = self.__check_request_status(transfer_url, requestId, space)
232
+
233
+ # Write the result to the log
234
+ for line in result.split(os.linesep):
235
+ if len(line) > 0:
236
+ self.__log.info(' ' + line)
237
+ sleep(3)
238
+
239
+ previous_result = result
240
+ while (result in [Client.__EXECUTION_IN_PROGRESS, Client.__EXECUTION_IN_QUEUED]
241
+ or Client.__CONNECTION_ABORTED in result):
242
+ result = self.__check_request_status(transfer_url, requestId, space)
243
+
244
+ # Prevent loging again the same information such as "Queued" or "InProgress"
245
+ if previous_result != result:
246
+ previous_result = result
247
+
248
+ # Write the result to the log
249
+ for line in previous_result.split(os.linesep):
250
+ if (len(line) > 0 and line not in [Client.__EXECUTION_IN_PROGRESS, Client.__EXECUTION_IN_QUEUED]
251
+ and Client.__CONNECTION_ABORTED not in line):
252
+ self.__log.info(' ' + line)
253
+ sleep(3)
254
+
255
+ returned_result = returned_result + result + os.linesep
256
+ else:
257
+ returned_result = Client.__ERROR_OCCURRED
258
+ if len(str(response.status_code)) > 0:
259
+ returned_result += 'Error code: ' + str(response.status_code) + os.linesep
260
+ if len(str(response.reason)) > 0:
261
+ returned_result += 'Reason: ' + str(response.reason) + os.linesep
262
+ if len(response.text) > 0:
263
+ returned_result += 'Text: ' + response.text
264
+
265
+ returned_result += os.linesep
266
+ # Write the result to the log
267
+ for line in returned_result.split(os.linesep):
268
+ if len(line) > 0:
269
+ self.__log.info(' ' + line)
270
+ except ValueError as err:
271
+ returned_result = Client.__ERROR_OCCURRED
272
+ if len(str(response.status_code)) > 0:
273
+ returned_result += 'Error code: ' + str(response.status_code) + os.linesep
274
+ if len(str(response.reason)) > 0:
275
+ returned_result += 'Reason: ' + str(response.reason) + os.linesep
276
+ if len(response.text) > 0:
277
+ returned_result += 'Text: ' + str(response.text)
278
+ else:
279
+ returned_result += str(err)
280
+ returned_result += os.linesep
281
+ return returned_result
282
+
283
+
284
+ # Upload an Excel data file to .Stat Suite
285
+ def upload_excel_data_file(self,
286
+ transfer_url: str,
287
+ excelfile_path: Path,
288
+ eddfile_path: Path,
289
+ space: str,
290
+ validationType: int):
291
+ try:
292
+ returned_result = ""
293
+
294
+ #
295
+ if Client.__authentication_obj is not None:
296
+ Client.__access_token = Client.__authentication_obj.get_token()
297
+
298
+ payload = {
299
+ 'dataspace': space,
300
+ 'validationType': validationType
301
+ }
302
+
303
+ headers = {
304
+ 'accept': 'application/json',
305
+ 'authorization': "Bearer "+Client.__access_token
306
+ }
307
+
308
+ excel_file = open(os.path.realpath(excelfile_path), 'rb')
309
+ eddfile_file = open(os.path.realpath(eddfile_path), 'rb')
310
+ files = {
311
+ 'dataspace': (None, payload['dataspace']),
312
+ 'validationType': (None, payload['validationType']),
313
+ 'excelFile': (str(excelfile_path), excel_file, 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', ''),
314
+ 'eddFile': (str(eddfile_path), eddfile_file, 'text/xml', '')
315
+ }
316
+
317
+ #
318
+ response = requests.post(transfer_url, verify=True, headers=headers, files=files)
319
+ except Exception as err:
320
+ returned_result = Client.__ERROR_OCCURRED + str(err) + os.linesep
321
+
322
+ # Write the result to the log
323
+ for line in returned_result.split(os.linesep):
324
+ if len(line) > 0:
325
+ self.__log.info(' ' + line)
326
+ return returned_result
327
+ else:
328
+ # If the response object cannot be converted to json, return an error
329
+ results_json = None
330
+ try:
331
+ results_json = json.loads(response.text)
332
+ if response.status_code == 200:
333
+ result = results_json['message']
334
+ # Write the result to the log
335
+ for line in result.split(os.linesep):
336
+ if len(line) > 0:
337
+ self.__log.info(' ' + line)
338
+
339
+ returned_result = result + os.linesep
340
+
341
+ # Check the request status
342
+ if (result != "" and result.find(Client.__ERROR_OCCURRED ) == -1):
343
+ # Extract the request ID the returned message
344
+ start = 'with ID'
345
+ end = 'was successfully'
346
+ requestId = result[result.find(
347
+ start)+len(start):result.rfind(end)]
348
+
349
+ # Sleep a little bit before checking the request status
350
+ sleep(3)
351
+
352
+ # To avoid this error: maximum recursion depth exceeded while calling a Python object
353
+ # replace the recursive calls with while loops.
354
+ result = self.__check_request_status(transfer_url, requestId, space)
355
+
356
+ # Write the result to the log
357
+ for line in result.split(os.linesep):
358
+ if len(line) > 0:
359
+ self.__log.info(' ' + line)
360
+ sleep(3)
361
+
362
+ previous_result = result
363
+ while (result in [Client.__EXECUTION_IN_PROGRESS, Client.__EXECUTION_IN_QUEUED]
364
+ or Client.__CONNECTION_ABORTED in result):
365
+ result = self.__check_request_status(transfer_url, requestId, space)
366
+
367
+ # Prevent loging again the same information such as "Queued" or "InProgress"
368
+ if previous_result != result:
369
+ previous_result = result
370
+
371
+ # Write the result to the log
372
+ for line in previous_result.split(os.linesep):
373
+ if (len(line) > 0 and line not in [Client.__EXECUTION_IN_PROGRESS, Client.__EXECUTION_IN_QUEUED]
374
+ and Client.__CONNECTION_ABORTED not in line):
375
+ self.__log.info(' ' + line)
376
+ sleep(3)
377
+
378
+ returned_result = returned_result + result + os.linesep
379
+ else:
380
+ returned_result = Client.__ERROR_OCCURRED
381
+ if len(str(response.status_code)) > 0:
382
+ returned_result += 'Error code: ' + str(response.status_code) + os.linesep
383
+ if len(str(response.reason)) > 0:
384
+ returned_result += 'Reason: ' + str(response.reason) + os.linesep
385
+ if len(response.text) > 0:
386
+ returned_result += 'Text: ' + response.text
387
+
388
+ returned_result += os.linesep
389
+ # Write the result to the log
390
+ for line in returned_result.split(os.linesep):
391
+ if len(line) > 0:
392
+ self.__log.info(' ' + line)
393
+ except ValueError as err:
394
+ returned_result = Client.__ERROR_OCCURRED
395
+ if len(str(response.status_code)) > 0:
396
+ returned_result += 'Error code: ' + str(response.status_code) + os.linesep
397
+ if len(str(response.reason)) > 0:
398
+ returned_result += 'Reason: ' + str(response.reason) + os.linesep
399
+ if len(response.text) > 0:
400
+ returned_result += 'Text: ' + str(response.text)
401
+ else:
402
+ returned_result += str(err)
403
+ returned_result += os.linesep
404
+ return returned_result
405
+
406
+
407
+ # Upload a structure file to .Stat Suite
408
+ def upload_structure(self, transfer_url: str, file_path: Path):
409
+ try:
410
+ returned_result = ""
411
+
412
+ #
413
+ if Client.__authentication_obj is not None:
414
+ Client.__access_token = Client.__authentication_obj.get_token()
415
+
416
+ # Detect the encoding used in file
417
+ detected_encoding = self.__detect_encode(file_path)
418
+
419
+ # Read file as a string "r+" with the detected encoding
420
+ with open(file=file_path, mode="r+", encoding=detected_encoding.get("encoding")) as file:
421
+ xml_data = file.read()
422
+
423
+ # Make sure the encoding is "utf-8"
424
+ tree = ET.fromstring(xml_data)
425
+ xml_data = ET.tostring(tree, encoding="utf-8", method='xml', xml_declaration=True)
426
+
427
+ headers = {
428
+ 'Content-Type': 'application/xml',
429
+ 'authorization': "Bearer "+Client.__access_token
430
+ }
431
+
432
+ #
433
+ response = requests.post(transfer_url, verify=True, headers=headers, data=xml_data)
434
+ except Exception as err:
435
+ returned_result = Client.__ERROR_OCCURRED + str(err) + os.linesep
436
+
437
+ # Write the result to the log
438
+ for line in returned_result.split(os.linesep):
439
+ if len(line) > 0:
440
+ self.__log.info(' ' + line)
441
+ return returned_result
442
+ else:
443
+ try:
444
+ response.raise_for_status()
445
+ except requests.exceptions.HTTPError as e:
446
+ returned_result = f'{Client.__UPLOAD_FAILED}{response.status_code}: {e}'
447
+
448
+ # Write the result to the log
449
+ for line in returned_result.split(os.linesep):
450
+ if len(line) > 0:
451
+ self.__log.info(' ' + line)
452
+ else:
453
+ response_tree = ET.XML(response.content)
454
+ for element in response_tree.findall("./{0}ErrorMessage".format(Client.__NAMESPACE_MESSAGE)):
455
+ text_element = element.find("./{0}Text".format(Client.__NAMESPACE_COMMON))
456
+ if (text_element is not None):
457
+ if returned_result == "":
458
+ returned_result = f'{Client.__UPLOAD_SUCCESS}with status code: {response.status_code}' + os.linesep
459
+ returned_result = returned_result + text_element.text + os.linesep
460
+
461
+ # Write the result to the log
462
+ for line in returned_result.split(os.linesep):
463
+ if len(line) > 0:
464
+ self.__log.info(' ' + line)
465
+ return returned_result
466
+
467
+
468
+ # Detect the encoding used in file
469
+ def __detect_encode(self, file_path):
470
+ detector = chardet.UniversalDetector()
471
+ detector.reset()
472
+ with open(file=file_path, mode="rb") as file:
473
+ for row in file:
474
+ detector.feed(row)
475
+ if detector.done:
476
+ break
477
+
478
+ detector.close()
479
+
480
+ return detector.result
481
+
482
+
483
+ # Check request sent to .Stat Suite status
484
+ # To avoid this error: maximum recursion depth exceeded while calling a Python object
485
+ # replace the recursive calls with while loops.
486
+ def __check_request_status(self, transfer_url, requestId, space):
487
+ try:
488
+ returned_result = ""
489
+
490
+ #
491
+ if Client.__authentication_obj is not None:
492
+ Client.__access_token = Client.__authentication_obj.get_token()
493
+
494
+ headers = {
495
+ 'accept': 'application/json',
496
+ 'authorization': "Bearer "+Client.__access_token
497
+ }
498
+
499
+ payload = {
500
+ 'dataspace': space,
501
+ 'id': requestId
502
+ }
503
+
504
+ transfer_url = transfer_url.replace("import", "status")
505
+ if "sdmxFile" in transfer_url:
506
+ transfer_url = transfer_url.replace("sdmxFile", "request")
507
+ elif "excel" in transfer_url:
508
+ transfer_url = transfer_url.replace("excel", "request")
509
+
510
+ #
511
+ response = requests.post(transfer_url, verify=True, headers=headers, data=payload)
512
+ except Exception as err:
513
+ returned_result = Client.__ERROR_OCCURRED + str(err)
514
+ return returned_result
515
+ else:
516
+ # If the response object cannot be converted to json, return an error
517
+ results_json = None
518
+ try:
519
+ results_json = json.loads(response.text)
520
+ if response.status_code == 200:
521
+ executionStatus = 'Execution status: ' + results_json['executionStatus']
522
+ if (results_json['executionStatus'] in [Client.__EXECUTION_IN_PROGRESS, Client.__EXECUTION_IN_QUEUED]
523
+ or Client.__CONNECTION_ABORTED in results_json['executionStatus']):
524
+ returned_result = results_json['executionStatus']
525
+ else:
526
+ returned_result = executionStatus + os.linesep + 'Outcome: ' + results_json['outcome'] + os.linesep
527
+ index = 0
528
+ while index < len(results_json['logs']):
529
+ returned_result = returned_result + 'Log' + str(index) + ': ' + results_json['logs'][index]['message'] + os.linesep
530
+ index += 1
531
+ else:
532
+ returned_result = Client.__ERROR_OCCURRED
533
+ if len(str(response.status_code)) > 0:
534
+ returned_result += 'Error code: ' + str(response.status_code) + os.linesep
535
+ if len(str(response.reason)) > 0:
536
+ returned_result += 'Reason: ' + str(response.reason) + os.linesep
537
+ if len(response.text) > 0:
538
+ returned_result += 'Text: ' + str(response.text)
539
+
540
+ returned_result += os.linesep
541
+ except ValueError as err:
542
+ returned_result = Client.__ERROR_OCCURRED
543
+ if len(str(response.status_code)) > 0:
544
+ returned_result += 'Error code: ' + str(response.status_code) + os.linesep
545
+ if len(str(response.reason)) > 0:
546
+ returned_result += 'Reason: ' + str(response.reason) + os.linesep
547
+ if len(response.text) > 0:
548
+ returned_result += 'Text: ' + str(response.text)
549
+ else:
550
+ returned_result += str(err)
551
+ returned_result += os.linesep
552
+ return returned_result
@@ -0,0 +1,35 @@
1
+ [tool.poetry]
2
+ name = "dotstat_io"
3
+ version = "1.0.7"
4
+ description = "Utility to download or upload data from/to .Stat Suite using ADFS authentication to connect to it"
5
+ license = "MIT"
6
+ authors = ["Gyorgy Gyomai <gyorgy.gyomai@oecd.org>", "Abdel Aliaoui <abdel.aliaoui@oecd.org>"]
7
+ readme = "README.md"
8
+ packages = [{include = "dotstat_io"}]
9
+
10
+ [tool.poetry.dependencies]
11
+ python = "^3.10"
12
+ requests = "^2.29.0"
13
+ requests-kerberos = "^0.14.0"
14
+ msal = "^1.22.0"
15
+ chardet = "^5.1.0"
16
+ setuptools = "^75.6.0"
17
+
18
+
19
+ [tool.poetry.group.dev.dependencies]
20
+ pytest = "^9.0.2"
21
+
22
+
23
+ [[tool.poetry.source]]
24
+ name = "fpho"
25
+ url = "https://files.pythonhosted.org"
26
+ priority = "primary"
27
+
28
+
29
+ [[tool.poetry.source]]
30
+ name = "PyPI"
31
+ priority = "primary"
32
+
33
+ [build-system]
34
+ requires = ["poetry-core"]
35
+ build-backend = "poetry.core.masonry.api"