dotstat_io 0.2.7__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 dotstat_io might be problematic. Click here for more details.
- dotstat_io/__init__.py +0 -0
- dotstat_io/authentication.py +335 -0
- dotstat_io/download_upload.py +376 -0
- dotstat_io-0.2.7.dist-info/METADATA +150 -0
- dotstat_io-0.2.7.dist-info/RECORD +6 -0
- dotstat_io-0.2.7.dist-info/WHEEL +4 -0
dotstat_io/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
import msal
|
|
3
|
+
import requests_kerberos
|
|
4
|
+
import json
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
from enum import IntEnum
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AuthenticationMode(IntEnum):
|
|
12
|
+
INTERACTIVE = 1
|
|
13
|
+
NONINTERACTIVE_WITH_SECRET = 2
|
|
14
|
+
NONINTERACTIVE_WITH_ADFS = 3
|
|
15
|
+
|
|
16
|
+
# class to manage ADFS authentication using OIDC flows
|
|
17
|
+
class AdfsAuthentication():
|
|
18
|
+
|
|
19
|
+
# Declare constants
|
|
20
|
+
__ERROR = "An error occurred: "
|
|
21
|
+
__SUCCESS = "Successful authentication"
|
|
22
|
+
|
|
23
|
+
# Initialise Adfs_authentication
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
mode: AuthenticationMode,
|
|
27
|
+
client_id: str,
|
|
28
|
+
sdmx_resource_id: str,
|
|
29
|
+
scopes: list[str] = [],
|
|
30
|
+
authority_url: str = None,
|
|
31
|
+
token_url: str = None,
|
|
32
|
+
redirect_port: int = 3000,
|
|
33
|
+
client_secret: str = None
|
|
34
|
+
):
|
|
35
|
+
self.__access_token = None
|
|
36
|
+
|
|
37
|
+
self.__refresh_token = None
|
|
38
|
+
self.__creation_time = None
|
|
39
|
+
self.__expiration_time = None
|
|
40
|
+
|
|
41
|
+
self.result = None
|
|
42
|
+
self.mode = mode
|
|
43
|
+
self.client_id = client_id
|
|
44
|
+
self.sdmx_resource_id = sdmx_resource_id
|
|
45
|
+
self.scopes = scopes
|
|
46
|
+
self.authority_url = authority_url
|
|
47
|
+
self.token_url = token_url
|
|
48
|
+
self.redirect_port = redirect_port
|
|
49
|
+
self.client_secret = client_secret
|
|
50
|
+
|
|
51
|
+
match self.mode:
|
|
52
|
+
case AuthenticationMode.INTERACTIVE:
|
|
53
|
+
self.app = msal.PublicClientApplication(
|
|
54
|
+
self.client_id, authority=self.authority_url)
|
|
55
|
+
case AuthenticationMode.NONINTERACTIVE_WITH_SECRET:
|
|
56
|
+
self.app = msal.ConfidentialClientApplication(
|
|
57
|
+
self.client_id, authority=self.authority_url, client_credential=self.client_secret)
|
|
58
|
+
|
|
59
|
+
def __enter__(self):
|
|
60
|
+
return self
|
|
61
|
+
|
|
62
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
63
|
+
self.__access_token = None
|
|
64
|
+
self.__refresh_token = None
|
|
65
|
+
self.__creation_time = None
|
|
66
|
+
self.__expiration_time = None
|
|
67
|
+
|
|
68
|
+
@classmethod
|
|
69
|
+
def interactive(
|
|
70
|
+
cls,
|
|
71
|
+
client_id: str,
|
|
72
|
+
sdmx_resource_id: str,
|
|
73
|
+
scopes: list[str],
|
|
74
|
+
authority_url: str,
|
|
75
|
+
redirect_port: int = 3000,
|
|
76
|
+
mode: AuthenticationMode = AuthenticationMode.INTERACTIVE
|
|
77
|
+
):
|
|
78
|
+
return cls(
|
|
79
|
+
client_id=client_id,
|
|
80
|
+
sdmx_resource_id=sdmx_resource_id,
|
|
81
|
+
scopes=scopes,
|
|
82
|
+
authority_url=authority_url,
|
|
83
|
+
redirect_port=redirect_port,
|
|
84
|
+
mode=mode
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
@classmethod
|
|
88
|
+
def nointeractive_with_secret(
|
|
89
|
+
cls,
|
|
90
|
+
client_id: str,
|
|
91
|
+
sdmx_resource_id: str,
|
|
92
|
+
scopes: list[str],
|
|
93
|
+
authority_url: str,
|
|
94
|
+
client_secret: str,
|
|
95
|
+
mode: AuthenticationMode = AuthenticationMode.NONINTERACTIVE_WITH_SECRET
|
|
96
|
+
):
|
|
97
|
+
return cls(
|
|
98
|
+
client_id=client_id,
|
|
99
|
+
sdmx_resource_id=sdmx_resource_id,
|
|
100
|
+
scopes=scopes,
|
|
101
|
+
authority_url=authority_url,
|
|
102
|
+
client_secret=client_secret,
|
|
103
|
+
mode=mode
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
@classmethod
|
|
107
|
+
def nointeractive_with_adfs(
|
|
108
|
+
cls,
|
|
109
|
+
client_id: str,
|
|
110
|
+
sdmx_resource_id: str,
|
|
111
|
+
token_url: str,
|
|
112
|
+
mode: AuthenticationMode = AuthenticationMode.NONINTERACTIVE_WITH_ADFS
|
|
113
|
+
):
|
|
114
|
+
return cls(
|
|
115
|
+
client_id=client_id,
|
|
116
|
+
sdmx_resource_id=sdmx_resource_id,
|
|
117
|
+
token_url=token_url,
|
|
118
|
+
mode=mode)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def get_token(self):
|
|
122
|
+
self.__access_token = None
|
|
123
|
+
self.__refresh_token = None
|
|
124
|
+
self.__creation_time = None
|
|
125
|
+
self.__expiration_time = None
|
|
126
|
+
self.result = None
|
|
127
|
+
match self.mode:
|
|
128
|
+
case AuthenticationMode.INTERACTIVE:
|
|
129
|
+
self._acquire_token_interactive()
|
|
130
|
+
case AuthenticationMode.NONINTERACTIVE_WITH_SECRET:
|
|
131
|
+
self._acquire_token_noninteractive_with_secret()
|
|
132
|
+
case AuthenticationMode.NONINTERACTIVE_WITH_ADFS:
|
|
133
|
+
self._acquire_token_noninteractive_with_adfs()
|
|
134
|
+
|
|
135
|
+
return self.__access_token
|
|
136
|
+
|
|
137
|
+
# Check the token is not expired
|
|
138
|
+
def is_access_token_expired(self):
|
|
139
|
+
# convert the timestamp to a datetime object
|
|
140
|
+
if datetime.fromtimestamp(self.__expiration_time) is not None:
|
|
141
|
+
if datetime.now() <= datetime.fromtimestamp(self.__expiration_time):
|
|
142
|
+
return False
|
|
143
|
+
else:
|
|
144
|
+
return True
|
|
145
|
+
else:
|
|
146
|
+
return True
|
|
147
|
+
|
|
148
|
+
# Authentication interactively - aka Authorization Code flow
|
|
149
|
+
def _acquire_token_interactive(self):
|
|
150
|
+
try:
|
|
151
|
+
|
|
152
|
+
# We now check the cache to see
|
|
153
|
+
# whether we already have some accounts that the end user already used to sign in before.
|
|
154
|
+
accounts = self.app.get_accounts()
|
|
155
|
+
if accounts:
|
|
156
|
+
account = accounts[0]
|
|
157
|
+
else:
|
|
158
|
+
account = None
|
|
159
|
+
|
|
160
|
+
# Firstly, looks up a access_token from cache, or using a refresh token
|
|
161
|
+
response_silent = self.app.acquire_token_silent(
|
|
162
|
+
self.scopes, account=account)
|
|
163
|
+
if not response_silent:
|
|
164
|
+
# Prompt the user to sign in interactively
|
|
165
|
+
response_interactive = self.app.acquire_token_interactive(
|
|
166
|
+
scopes=self.scopes, port=self.redirect_port)
|
|
167
|
+
if "access_token" in response_interactive:
|
|
168
|
+
self.__access_token = response_interactive.get("access_token")
|
|
169
|
+
self.__refresh_token = response_interactive.get("refresh_token")
|
|
170
|
+
self.__creation_time = time.time()
|
|
171
|
+
self.__expiration_time = time.time() + int(response_interactive.get("expires_in")) - 60 # one minute margin
|
|
172
|
+
self.result = self.__SUCCESS
|
|
173
|
+
else:
|
|
174
|
+
self.result = f'{self.__ERROR}{response_interactive.get("error")} Error description: {response_interactive.get("error_description")}'
|
|
175
|
+
else:
|
|
176
|
+
if "access_token" in response_silent:
|
|
177
|
+
self.__access_token = response_silent.get("access_token")
|
|
178
|
+
self.__refresh_token = response_silent.get("refresh_token")
|
|
179
|
+
self.__creation_time = time.time()
|
|
180
|
+
self.__expiration_time = time.time() + int(response_silent.get("expires_in")) - 60 # one minute margin
|
|
181
|
+
self.result = self.__SUCCESS
|
|
182
|
+
else:
|
|
183
|
+
self.result = f'{self.__ERROR}{response_silent.get("error")} Error description: {response_silent.get("error_description")}'
|
|
184
|
+
except Exception as err:
|
|
185
|
+
self.result = f'{self.__ERROR}{err}\n'
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# Authentication non-interactively using any account - aka Client Credentials flow
|
|
189
|
+
def _acquire_token_noninteractive_with_secret(self):
|
|
190
|
+
try:
|
|
191
|
+
response = self.app.acquire_token_for_client(scopes=self.scopes)
|
|
192
|
+
if "access_token" in response:
|
|
193
|
+
self.__access_token = response.get("access_token")
|
|
194
|
+
self.__creation_time = time.time()
|
|
195
|
+
self.__expiration_time = time.time() + int(response.get("expires_in")) - 60 # one minute margin
|
|
196
|
+
self.result = self.__SUCCESS
|
|
197
|
+
else:
|
|
198
|
+
self.result = f'{self.__ERROR}{response.get("error")} Error description: {response.get("error_description")}'
|
|
199
|
+
|
|
200
|
+
except Exception as err:
|
|
201
|
+
self.result = f'{self.__ERROR}{err}\n'
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
# Authentication non-interactively using service account - aka Windows Client Authentication
|
|
205
|
+
def _acquire_token_noninteractive_with_adfs(self):
|
|
206
|
+
try:
|
|
207
|
+
headers = {
|
|
208
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
payload = {
|
|
212
|
+
'client_id': self.client_id,
|
|
213
|
+
'use_windows_client_authentication': 'true',
|
|
214
|
+
'grant_type': 'client_credentials',
|
|
215
|
+
'scope': 'openid',
|
|
216
|
+
'resource': self.sdmx_resource_id
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
kerberos_auth = requests_kerberos.HTTPKerberosAuth(
|
|
220
|
+
mutual_authentication=requests_kerberos.OPTIONAL, force_preemptive=True)
|
|
221
|
+
response = requests.post(url=self.token_url, data=payload, auth=kerberos_auth)
|
|
222
|
+
|
|
223
|
+
if response.status_code != 200:
|
|
224
|
+
message = f'{self.__ERROR} Error code: {response.status_code} Reason: {response.reason}\n'
|
|
225
|
+
if len(response.text) > 0:
|
|
226
|
+
# Use the json module to load response into a dictionary.
|
|
227
|
+
response_dict = json.loads(response.text)
|
|
228
|
+
message += f'{self.__ERROR}{response_dict.get("error")} Error description: {response_dict.get("error_description")}\n'
|
|
229
|
+
|
|
230
|
+
self.result = message
|
|
231
|
+
else:
|
|
232
|
+
self.__access_token = response.json()['access_token']
|
|
233
|
+
self.__creation_time = time.time()
|
|
234
|
+
self.__expiration_time = time.time() + int(response.json()['expires_in']) - 60 # one minute margin
|
|
235
|
+
self.result = self.__SUCCESS
|
|
236
|
+
|
|
237
|
+
except Exception as err:
|
|
238
|
+
self.result = f'{self.__ERROR} {err}\n'
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
# class to manage ADFS authentication using OIDC flows
|
|
242
|
+
class KeycloakAuthentication():
|
|
243
|
+
|
|
244
|
+
# Declare constants
|
|
245
|
+
__ERROR = "An error occurred: "
|
|
246
|
+
__SUCCESS = "Successful authentication"
|
|
247
|
+
|
|
248
|
+
# Initialise Adfs_authentication
|
|
249
|
+
def __init__(
|
|
250
|
+
self,
|
|
251
|
+
mode: AuthenticationMode,
|
|
252
|
+
token_url: str,
|
|
253
|
+
user: str,
|
|
254
|
+
password: str,
|
|
255
|
+
client_id: str,
|
|
256
|
+
client_secret: str,
|
|
257
|
+
proxy: str | None,
|
|
258
|
+
scopes: list[str] = []
|
|
259
|
+
):
|
|
260
|
+
self.__token = None
|
|
261
|
+
self.result = None
|
|
262
|
+
self.mode = mode
|
|
263
|
+
self.client_id = client_id
|
|
264
|
+
self.client_secret = client_secret
|
|
265
|
+
self.token_url = token_url
|
|
266
|
+
self.user = user
|
|
267
|
+
self.password = password
|
|
268
|
+
self.scopes = scopes #TODO check if needs explicit adjustment
|
|
269
|
+
|
|
270
|
+
if proxy:
|
|
271
|
+
self.proxies = {
|
|
272
|
+
"http": proxy,
|
|
273
|
+
"https": proxy
|
|
274
|
+
}
|
|
275
|
+
else:
|
|
276
|
+
self.proxies = None
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def __enter__(self):
|
|
280
|
+
return self
|
|
281
|
+
|
|
282
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
283
|
+
self.__token = None
|
|
284
|
+
|
|
285
|
+
@classmethod
|
|
286
|
+
def nointeractive_with_secret(
|
|
287
|
+
cls,
|
|
288
|
+
token_url: str,
|
|
289
|
+
user: str,
|
|
290
|
+
password: str,
|
|
291
|
+
client_id: str = "app",
|
|
292
|
+
client_secret: str = "",
|
|
293
|
+
proxy: str | None = None,
|
|
294
|
+
scopes: list[str] = [],
|
|
295
|
+
mode: AuthenticationMode = AuthenticationMode.NONINTERACTIVE_WITH_SECRET
|
|
296
|
+
):
|
|
297
|
+
return cls(
|
|
298
|
+
token_url=token_url,
|
|
299
|
+
user=user,
|
|
300
|
+
password=password,
|
|
301
|
+
client_id=client_id,
|
|
302
|
+
client_secret=client_secret,
|
|
303
|
+
scopes=scopes,
|
|
304
|
+
proxy=proxy,
|
|
305
|
+
mode=mode
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
def get_token(self):
|
|
309
|
+
self.__token = None
|
|
310
|
+
self.result = None
|
|
311
|
+
match self.mode:
|
|
312
|
+
case AuthenticationMode.NONINTERACTIVE_WITH_SECRET:
|
|
313
|
+
self._acquire_token_noninteractive_with_secret()
|
|
314
|
+
|
|
315
|
+
return self.__token
|
|
316
|
+
|
|
317
|
+
# Authentication non-interactively using any account - aka Client Credentials flow
|
|
318
|
+
def _acquire_token_noninteractive_with_secret(self):
|
|
319
|
+
payload = {
|
|
320
|
+
'grant_type': 'password',
|
|
321
|
+
'client_id': self.client_id,
|
|
322
|
+
'client_secret': self.client_secret,
|
|
323
|
+
'username': self.user,
|
|
324
|
+
'password': self.password
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
headers = {"Content-Type": "application/x-www-form-urlencoded"}
|
|
328
|
+
|
|
329
|
+
response = requests.post(self.token_url, proxies=self.proxies, headers=headers, data=payload)
|
|
330
|
+
if response.status_code not in {200, 201}:
|
|
331
|
+
self.result = f'{self.__ERROR}{response}'
|
|
332
|
+
else:
|
|
333
|
+
self.__token = response.json()['access_token']
|
|
334
|
+
self.result = self.__SUCCESS
|
|
335
|
+
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import requests
|
|
3
|
+
import chardet
|
|
4
|
+
import logging
|
|
5
|
+
import xml.etree.ElementTree as ET
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from time import sleep
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
FORMAT = '%(message)s'
|
|
12
|
+
logging.basicConfig(format=FORMAT, level=logging.INFO)
|
|
13
|
+
log = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# class to download or upload data from/to .Stat Suite
|
|
17
|
+
class Download_upload():
|
|
18
|
+
|
|
19
|
+
# Declare constants
|
|
20
|
+
__ERROR = "An error occurred: "
|
|
21
|
+
__NO_ACCESS_TOKEN = "No access token"
|
|
22
|
+
__EXECUTION_IN_QUEUED = "Queued"
|
|
23
|
+
__EXECUTION_IN_PROGRESS = "InProgress"
|
|
24
|
+
__CONNECTION_ABORTED = "An existing connection was forcibly closed by the remote host"
|
|
25
|
+
__DOWNLOAD_SUCCESS = "Successful download"
|
|
26
|
+
__UPLOAD_SUCCESS = "The request was successfully processed "
|
|
27
|
+
__UPLOAD_FAILED = "The request failed with status code "
|
|
28
|
+
|
|
29
|
+
__NAMESPACE_MESSAGE = "{http://www.sdmx.org/resources/sdmxml/schemas/v2_1/message}"
|
|
30
|
+
__NAMESPACE_COMMON = "{http://www.sdmx.org/resources/sdmxml/schemas/v2_1/common}"
|
|
31
|
+
|
|
32
|
+
# Initialise Download_upload
|
|
33
|
+
def __init__(self, adfsAuthentication_obj, access_token):
|
|
34
|
+
self.adfsAuthentication_obj = adfsAuthentication_obj
|
|
35
|
+
self.access_token = access_token
|
|
36
|
+
|
|
37
|
+
def __enter__(self):
|
|
38
|
+
return self
|
|
39
|
+
|
|
40
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
41
|
+
self.adfsAuthentication_obj = None
|
|
42
|
+
self.access_token = None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# Download a file from .STAT
|
|
46
|
+
def download_file(self, dotstat_url: str, content_format: str, file_path: Path):
|
|
47
|
+
try:
|
|
48
|
+
Returned_Message = ""
|
|
49
|
+
|
|
50
|
+
if (self.access_token == None):
|
|
51
|
+
Returned_Message = self.__ERROR + self.__NO_ACCESS_TOKEN + os.linesep
|
|
52
|
+
|
|
53
|
+
# Write the result to the log
|
|
54
|
+
for line in Returned_Message.split(os.linesep):
|
|
55
|
+
if len(line) > 0:
|
|
56
|
+
log.info(' ' + line)
|
|
57
|
+
else:
|
|
58
|
+
if self.adfsAuthentication_obj.is_access_token_expired():
|
|
59
|
+
self.access_token = self.adfsAuthentication_obj.get_token()
|
|
60
|
+
|
|
61
|
+
headers = {
|
|
62
|
+
'accept': content_format,
|
|
63
|
+
'authorization': 'Bearer '+self.access_token
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
#
|
|
67
|
+
response = requests.get(dotstat_url, verify=True, headers=headers)
|
|
68
|
+
except Exception as err:
|
|
69
|
+
Returned_Message = self.__ERROR + str(err) + os.linesep
|
|
70
|
+
|
|
71
|
+
# Write the result to the log
|
|
72
|
+
for line in Returned_Message.split(os.linesep):
|
|
73
|
+
if len(line) > 0:
|
|
74
|
+
log.info(' ' + line)
|
|
75
|
+
else:
|
|
76
|
+
if response.status_code != 200:
|
|
77
|
+
Returned_Message = self.__ERROR + 'Error code: ' + \
|
|
78
|
+
str(response.status_code) + ' Reason: ' + str(response.reason)
|
|
79
|
+
if len(response.text) > 0:
|
|
80
|
+
Returned_Message += os.linesep + 'Text: ' + response.text
|
|
81
|
+
else:
|
|
82
|
+
if os.path.isfile(file_path):
|
|
83
|
+
os.remove(file_path)
|
|
84
|
+
with open(file_path, "wb") as file:
|
|
85
|
+
file.write(response.content)
|
|
86
|
+
Returned_Message = self.__DOWNLOAD_SUCCESS
|
|
87
|
+
|
|
88
|
+
# Write the result to the log
|
|
89
|
+
for line in Returned_Message.split(os.linesep):
|
|
90
|
+
if len(line) > 0:
|
|
91
|
+
log.info(' ' + line)
|
|
92
|
+
finally:
|
|
93
|
+
return Returned_Message
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# Download streamed content from .STAT
|
|
97
|
+
def download_stream(self, dotstat_url: str, content_format: str):
|
|
98
|
+
try:
|
|
99
|
+
Returned_Message = ""
|
|
100
|
+
|
|
101
|
+
if (self.access_token == None):
|
|
102
|
+
Returned_Message = self.__ERROR + self.__NO_ACCESS_TOKEN + os.linesep
|
|
103
|
+
else:
|
|
104
|
+
if self.adfsAuthentication_obj.is_access_token_expired():
|
|
105
|
+
self.access_token = self.adfsAuthentication_obj.get_token()
|
|
106
|
+
|
|
107
|
+
headers = {
|
|
108
|
+
'accept': content_format,
|
|
109
|
+
'Transfer-Encoding': 'chunked',
|
|
110
|
+
'authorization': 'Bearer '+self.access_token
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
#
|
|
114
|
+
return requests.get(dotstat_url, verify=True, headers=headers, stream=True)
|
|
115
|
+
except Exception as err:
|
|
116
|
+
Returned_Message = self.__ERROR + str(err) + os.linesep
|
|
117
|
+
return Returned_Message
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# Upload a file to .STAT
|
|
121
|
+
def upload_file(self,
|
|
122
|
+
transfer_url: str,
|
|
123
|
+
file_path: Path,
|
|
124
|
+
space: str,
|
|
125
|
+
validationType: int,
|
|
126
|
+
use_filepath: bool = False):
|
|
127
|
+
try:
|
|
128
|
+
Returned_Message = ""
|
|
129
|
+
|
|
130
|
+
if (self.access_token == None):
|
|
131
|
+
Returned_Message = self.__ERROR + self.__NO_ACCESS_TOKEN + os.linesep
|
|
132
|
+
|
|
133
|
+
# Write the result to the log
|
|
134
|
+
for line in Returned_Message.split(os.linesep):
|
|
135
|
+
if len(line) > 0:
|
|
136
|
+
log.info(' ' + line)
|
|
137
|
+
else:
|
|
138
|
+
if self.adfsAuthentication_obj.is_access_token_expired():
|
|
139
|
+
self.access_token = self.adfsAuthentication_obj.get_token()
|
|
140
|
+
|
|
141
|
+
payload = {
|
|
142
|
+
'dataspace': space,
|
|
143
|
+
'validationType': validationType
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
headers = {
|
|
147
|
+
'accept': 'application/json',
|
|
148
|
+
'authorization': "Bearer "+self.access_token
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if use_filepath:
|
|
152
|
+
files = {
|
|
153
|
+
'dataspace': (None, payload['dataspace']),
|
|
154
|
+
'validationType': (None, payload['validationType']),
|
|
155
|
+
'filepath': (None, str(file_path))
|
|
156
|
+
}
|
|
157
|
+
else:
|
|
158
|
+
files = {
|
|
159
|
+
'dataspace': (None, payload['dataspace']),
|
|
160
|
+
'validationType': (None, payload['validationType']),
|
|
161
|
+
'file': (os.path.realpath(file_path), open(os.path.realpath(file_path), 'rb'), 'text/csv', '')
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
#
|
|
165
|
+
response = requests.post(transfer_url, verify=True, headers=headers, files=files)
|
|
166
|
+
except Exception as err:
|
|
167
|
+
Returned_Message = self.__ERROR + str(err) + os.linesep
|
|
168
|
+
|
|
169
|
+
# Write the result to the log
|
|
170
|
+
for line in Returned_Message.split(os.linesep):
|
|
171
|
+
if len(line) > 0:
|
|
172
|
+
log.info(' ' + line)
|
|
173
|
+
else:
|
|
174
|
+
if response.status_code != 200:
|
|
175
|
+
Returned_Message = self.__ERROR + 'Error code: ' + \
|
|
176
|
+
str(response.status_code) + ' Reason: ' + str(response.reason)
|
|
177
|
+
if len(response.text) > 0:
|
|
178
|
+
Returned_Message = Returned_Message + os.linesep + 'Text: ' + response.text
|
|
179
|
+
|
|
180
|
+
Returned_Message = Returned_Message + os.linesep
|
|
181
|
+
# Write the result to the log
|
|
182
|
+
for line in Returned_Message.split(os.linesep):
|
|
183
|
+
if len(line) > 0:
|
|
184
|
+
log.info(' ' + line)
|
|
185
|
+
else:
|
|
186
|
+
try:
|
|
187
|
+
Result = response.json()['message']
|
|
188
|
+
|
|
189
|
+
# Write the result to the log
|
|
190
|
+
for line in Result.split(os.linesep):
|
|
191
|
+
if len(line) > 0:
|
|
192
|
+
log.info(' ' + line)
|
|
193
|
+
|
|
194
|
+
Returned_Message = Result + os.linesep
|
|
195
|
+
|
|
196
|
+
# Check the request status
|
|
197
|
+
if (Result != "" and Result.find(self.__ERROR ) == -1):
|
|
198
|
+
# Extract the request ID the returned message
|
|
199
|
+
start = 'with ID'
|
|
200
|
+
end = 'was successfully'
|
|
201
|
+
requestId = Result[Result.find(
|
|
202
|
+
start)+len(start):Result.rfind(end)]
|
|
203
|
+
|
|
204
|
+
# Sleep a little bit before checking the request status
|
|
205
|
+
sleep(3)
|
|
206
|
+
|
|
207
|
+
# To avoid this error: maximum recursion depth exceeded while calling a Python object
|
|
208
|
+
# replace the recursive calls with while loops.
|
|
209
|
+
Result = self.__check_request_status(transfer_url, requestId, space)
|
|
210
|
+
|
|
211
|
+
# Write the result to the log
|
|
212
|
+
for line in Result.split(os.linesep):
|
|
213
|
+
if len(line) > 0:
|
|
214
|
+
log.info(' ' + line)
|
|
215
|
+
sleep(3)
|
|
216
|
+
|
|
217
|
+
Previous_Result = Result
|
|
218
|
+
while Result in [self.__EXECUTION_IN_PROGRESS, self.__EXECUTION_IN_QUEUED, self.__CONNECTION_ABORTED]:
|
|
219
|
+
Result = self.__check_request_status(transfer_url, requestId, space)
|
|
220
|
+
|
|
221
|
+
# Prevent loging again the same information such as "Queued" or "InProgress"
|
|
222
|
+
if Previous_Result != Result:
|
|
223
|
+
Previous_Result = Result
|
|
224
|
+
|
|
225
|
+
# Write the result to the log
|
|
226
|
+
for line in Previous_Result.split(os.linesep):
|
|
227
|
+
if (len(line) > 0 and line not in [self.__EXECUTION_IN_PROGRESS, self.__EXECUTION_IN_QUEUED, self.__CONNECTION_ABORTED]):
|
|
228
|
+
log.info(' ' + line)
|
|
229
|
+
sleep(3)
|
|
230
|
+
|
|
231
|
+
Returned_Message = Returned_Message + Result + os.linesep
|
|
232
|
+
except Exception as err:
|
|
233
|
+
Returned_Message = self.__ERROR + str(err) + os.linesep
|
|
234
|
+
if len(response.text) > 0:
|
|
235
|
+
Returned_Message = Returned_Message + 'Text: ' + response.text + os.linesep
|
|
236
|
+
|
|
237
|
+
# Write the result to the log
|
|
238
|
+
for line in Returned_Message.split(os.linesep):
|
|
239
|
+
if len(line) > 0:
|
|
240
|
+
log.info(' ' + line)
|
|
241
|
+
finally:
|
|
242
|
+
return Returned_Message
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
# Upload a structure to .STAT
|
|
246
|
+
def upload_structure(self, transfer_url: str, file_path: Path):
|
|
247
|
+
try:
|
|
248
|
+
Returned_Message = ""
|
|
249
|
+
|
|
250
|
+
if (self.access_token == None):
|
|
251
|
+
Returned_Message = self.__ERROR + self.__NO_ACCESS_TOKEN + os.linesep
|
|
252
|
+
|
|
253
|
+
# Write the result to the log
|
|
254
|
+
for line in Returned_Message.split(os.linesep):
|
|
255
|
+
if len(line) > 0:
|
|
256
|
+
log.info(' ' + line)
|
|
257
|
+
else:
|
|
258
|
+
if self.adfsAuthentication_obj.is_access_token_expired():
|
|
259
|
+
self.access_token = self.adfsAuthentication_obj.get_token()
|
|
260
|
+
|
|
261
|
+
# Detect the encoding used in file
|
|
262
|
+
detected_encoding = self.__detect_encode(file_path)
|
|
263
|
+
|
|
264
|
+
# Read file as a string "r+" with the detected encoding
|
|
265
|
+
with open(file=file_path, mode="r+", encoding=detected_encoding.get("encoding")) as file:
|
|
266
|
+
xml_data = file.read()
|
|
267
|
+
|
|
268
|
+
# Make sure the encoding is "utf-8"
|
|
269
|
+
tree = ET.fromstring(xml_data)
|
|
270
|
+
xml_data = ET.tostring(tree, encoding="utf-8", method='xml', xml_declaration=True)
|
|
271
|
+
|
|
272
|
+
headers = {
|
|
273
|
+
'Content-Type': 'application/xml',
|
|
274
|
+
'authorization': "Bearer "+self.access_token
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
#
|
|
278
|
+
response = requests.post(transfer_url, verify=True, headers=headers, data=xml_data)
|
|
279
|
+
except Exception as err:
|
|
280
|
+
Returned_Message = self.__ERROR + str(err) + os.linesep
|
|
281
|
+
|
|
282
|
+
# Write the result to the log
|
|
283
|
+
for line in Returned_Message.split(os.linesep):
|
|
284
|
+
if len(line) > 0:
|
|
285
|
+
log.info(' ' + line)
|
|
286
|
+
else:
|
|
287
|
+
try:
|
|
288
|
+
response.raise_for_status()
|
|
289
|
+
except requests.exceptions.HTTPError as e:
|
|
290
|
+
Returned_Message = f'{self.__UPLOAD_FAILED}{response.status_code}: {e}'
|
|
291
|
+
|
|
292
|
+
# Write the result to the log
|
|
293
|
+
for line in Returned_Message.split(os.linesep):
|
|
294
|
+
if len(line) > 0:
|
|
295
|
+
log.info(' ' + line)
|
|
296
|
+
else:
|
|
297
|
+
response_tree = ET.XML(response.content)
|
|
298
|
+
for error_message in response_tree.findall("./{0}ErrorMessage".format(self.__NAMESPACE_MESSAGE)):
|
|
299
|
+
text_element = error_message.find("./{0}Text".format(self.__NAMESPACE_COMMON))
|
|
300
|
+
if (text_element is not None):
|
|
301
|
+
if Returned_Message == "":
|
|
302
|
+
Returned_Message = f'{self.__UPLOAD_SUCCESS}with status code: {response.status_code}' + os.linesep
|
|
303
|
+
Returned_Message = Returned_Message + text_element.text + os.linesep
|
|
304
|
+
|
|
305
|
+
# Write the result to the log
|
|
306
|
+
for line in Returned_Message.split(os.linesep):
|
|
307
|
+
if len(line) > 0:
|
|
308
|
+
log.info(' ' + line)
|
|
309
|
+
finally:
|
|
310
|
+
return Returned_Message
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
# Detect the encoding used in file
|
|
314
|
+
def __detect_encode(self, file_path):
|
|
315
|
+
detector = chardet.UniversalDetector()
|
|
316
|
+
detector.reset()
|
|
317
|
+
with open(file=file_path, mode="rb") as file:
|
|
318
|
+
for row in file:
|
|
319
|
+
detector.feed(row)
|
|
320
|
+
if detector.done:
|
|
321
|
+
break
|
|
322
|
+
|
|
323
|
+
detector.close()
|
|
324
|
+
|
|
325
|
+
return detector.result
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
# Check request sent to .STAT status
|
|
329
|
+
# To avoid this error: maximum recursion depth exceeded while calling a Python object
|
|
330
|
+
# replace the recursive calls with while loops.
|
|
331
|
+
def __check_request_status(self, transfer_url, requestId, space):
|
|
332
|
+
try:
|
|
333
|
+
Returned_Message = ""
|
|
334
|
+
|
|
335
|
+
if (self.access_token == None):
|
|
336
|
+
Returned_Message = self.__ERROR + self.__NO_ACCESS_TOKEN + os.linesep
|
|
337
|
+
else:
|
|
338
|
+
if self.adfsAuthentication_obj.is_access_token_expired():
|
|
339
|
+
self.access_token = self.adfsAuthentication_obj.get_token()
|
|
340
|
+
|
|
341
|
+
headers = {
|
|
342
|
+
'accept': 'application/json',
|
|
343
|
+
'authorization': "Bearer "+self.access_token
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
payload = {
|
|
347
|
+
'dataspace': space,
|
|
348
|
+
'id': requestId
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
transfer_url = transfer_url.replace("import", "status")
|
|
352
|
+
transfer_url = transfer_url.replace("sdmxFile", "request")
|
|
353
|
+
|
|
354
|
+
#
|
|
355
|
+
response = requests.post(transfer_url, verify=True, headers=headers, data=payload)
|
|
356
|
+
except Exception as err:
|
|
357
|
+
Returned_Message = self.__ERROR + str(err)
|
|
358
|
+
else:
|
|
359
|
+
if response.status_code != 200:
|
|
360
|
+
Returned_Message = self.__ERROR + 'Error code: ' + \
|
|
361
|
+
str(response.status_code) + ' Reason: ' + str(response.reason)
|
|
362
|
+
if len(response.text) > 0:
|
|
363
|
+
Returned_Message = Returned_Message + os.linesep + 'Text: ' + response.text
|
|
364
|
+
else:
|
|
365
|
+
executionStatus = 'Execution status: ' + response.json()['executionStatus']
|
|
366
|
+
if response.json()['executionStatus'] in [self.__EXECUTION_IN_PROGRESS, self.__EXECUTION_IN_QUEUED, self.__CONNECTION_ABORTED]:
|
|
367
|
+
Returned_Message = response.json()['executionStatus']
|
|
368
|
+
else:
|
|
369
|
+
Returned_Message = executionStatus + os.linesep + 'Outcome: ' + response.json()['outcome'] + os.linesep
|
|
370
|
+
index = 0
|
|
371
|
+
while index < len(response.json()['logs']):
|
|
372
|
+
Returned_Message = Returned_Message + 'Log' + str(index) + ': ' + response.json()['logs'][index]['message'] + os.linesep
|
|
373
|
+
index += 1
|
|
374
|
+
finally:
|
|
375
|
+
return Returned_Message
|
|
376
|
+
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: dotstat_io
|
|
3
|
+
Version: 0.2.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
|
+
Requires-Dist: chardet (>=5.1.0,<6.0.0)
|
|
15
|
+
Requires-Dist: msal (>=1.22.0,<2.0.0)
|
|
16
|
+
Requires-Dist: requests (>=2.29.0,<3.0.0)
|
|
17
|
+
Requires-Dist: requests-kerberos (>=0.14.0,<0.15.0)
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
### DotStat_IO:
|
|
21
|
+
A generic python package which could be integrated with other end-user applications and Gitlab runner to perform basic io with .Stat Suite.
|
|
22
|
+
Its role is to mask the complexities of authentication to connect to .Stat Suite.
|
|
23
|
+
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.
|
|
24
|
+
|
|
25
|
+
### This package contains three modules:
|
|
26
|
+
- ADFSAuthentication module
|
|
27
|
+
- KeycloakAuthentication module
|
|
28
|
+
- Download_upload module
|
|
29
|
+
|
|
30
|
+
### In ADFSAuthentication module, four methods are available:
|
|
31
|
+
#### 1. To initialise the module for interactive use:
|
|
32
|
+
```python
|
|
33
|
+
from dotstat_io.authentication import AdfsAuthentication
|
|
34
|
+
with AdfsAuthentication.interactive(
|
|
35
|
+
client_id=client_id,
|
|
36
|
+
sdmx_resource_id=sdmx_resource_id,
|
|
37
|
+
scopes=scopes,
|
|
38
|
+
authority_url=authority_url,
|
|
39
|
+
redirect_port=redirect_port) as Interactive_obj:
|
|
40
|
+
access_token = Interactive_obj.get_token()
|
|
41
|
+
```
|
|
42
|
+
* `client_id:` The Application (client) ID that the ADFS assigned to your app
|
|
43
|
+
* `sdmx_resource_id:` The ID of the application to be accessed such as .Stat Suite
|
|
44
|
+
* `scopes:` Scopes requested to access a protected API (a resource defined by sdmx_resource_id)
|
|
45
|
+
* `authority_url:` URL that identifies a token authority
|
|
46
|
+
* `redirect_port:` The port of the address to return to upon receiving a response from the authority
|
|
47
|
+
|
|
48
|
+
#### 2. To initialise the module for non-interactive use using a secret:
|
|
49
|
+
```python
|
|
50
|
+
from dotstat_io.authentication import AdfsAuthentication
|
|
51
|
+
with AdfsAuthentication.nointeractive_with_secret(
|
|
52
|
+
client_id=client_id,
|
|
53
|
+
sdmx_resource_id=sdmx_resource_id,
|
|
54
|
+
scopes=scopes,
|
|
55
|
+
authority_url=authority_url,
|
|
56
|
+
client_secret=client_secret) as Nointeractive_with_secret_obj:
|
|
57
|
+
access_token = Nointeractive_with_secret_obj.get_token()
|
|
58
|
+
```
|
|
59
|
+
* `client_id:` The Application (client) ID that the ADFS assigned to your app
|
|
60
|
+
* `sdmx_resource_id:` The ID of the application to be accessed such as .Stat Suite
|
|
61
|
+
* `scopes:` Scopes requested to access a protected API (a resource defined by sdmx_resource_id)
|
|
62
|
+
* `authority_url:` URL that identifies a token authority
|
|
63
|
+
* `client_secret:` The application secret that you created during app registration in ADFS
|
|
64
|
+
|
|
65
|
+
#### 3. To initialise the module for non-interactive use using windows client authentication:
|
|
66
|
+
```python
|
|
67
|
+
from dotstat_io.authentication import AdfsAuthentication
|
|
68
|
+
with AdfsAuthentication.nointeractive_with_adfs(
|
|
69
|
+
client_id=client_id,
|
|
70
|
+
sdmx_resource_id=sdmx_resource_id,
|
|
71
|
+
token_url=token_url) as Nointeractive_with_adfs_obj:
|
|
72
|
+
access_token = Nointeractive_with_adfs_obj.get_token()
|
|
73
|
+
```
|
|
74
|
+
* `client_id:` The Application (client) ID that the ADFS assigned to your app
|
|
75
|
+
* `sdmx_resource_id:` The ID of the application to be accessed such as .Stat Suite
|
|
76
|
+
* `token_url:` URL of the authentication service
|
|
77
|
+
|
|
78
|
+
#### 4. To get a token after initialisation of "AdfsAuthentication" object as shown above:
|
|
79
|
+
```python
|
|
80
|
+
access_token = [Authentication Object Name].get_token()
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### In KeycloakAuthentication module, two methods are available:
|
|
84
|
+
#### 1. To initialise the module for Keycloak authentication:
|
|
85
|
+
```python
|
|
86
|
+
from dotstat_io.authentication import KeycloakAuthentication
|
|
87
|
+
with KeycloakAuthentication.nointeractive_with_secret(
|
|
88
|
+
user=user,
|
|
89
|
+
password=password,
|
|
90
|
+
token_url=token_url,
|
|
91
|
+
proxy=proxy) as Nointeractive_with_keycloak_obj:
|
|
92
|
+
access_token = Nointeractive_with_keycloak_obj.get_token()
|
|
93
|
+
```
|
|
94
|
+
* `user:` User name for .Stat Suite authentication
|
|
95
|
+
* `password:` User name's password
|
|
96
|
+
* `token_url:` URL of the authentication service
|
|
97
|
+
* `proxy:` URL of the SSL certificates
|
|
98
|
+
|
|
99
|
+
#### 2. To get a token after initialisation of "KeycloakAuthentication" object as shown above:
|
|
100
|
+
```python
|
|
101
|
+
access_token = [Authentication Object Name].get_token()
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### In Download_upload module, four methods are available:
|
|
105
|
+
#### 1. To download a file from .Stat Suite:
|
|
106
|
+
```python
|
|
107
|
+
from dotstat_io.download_upload import Download_upload
|
|
108
|
+
with Download_upload(adfsAuthentication_obj, access_token) as Download_upload_obj:
|
|
109
|
+
Returned_Message = Download_upload_obj.download_file(
|
|
110
|
+
dotstat_url, content_format, Path(file_path))
|
|
111
|
+
```
|
|
112
|
+
* `dotstat_url:` URL of data to be downloaded from .Stat Suite
|
|
113
|
+
* `content_format:` Format of the downloaded content
|
|
114
|
+
* `file_path:` The full path where the file will downloaded
|
|
115
|
+
|
|
116
|
+
#### 2. To download streamed content from .Stat Suite:
|
|
117
|
+
```python
|
|
118
|
+
from dotstat_io.download_upload import Download_upload
|
|
119
|
+
with Download_upload(adfsAuthentication_obj, access_token) as Download_upload_obj:
|
|
120
|
+
Returned_Message = Download_upload_obj.download_stream(
|
|
121
|
+
dotstat_url, content_format)
|
|
122
|
+
```
|
|
123
|
+
* `dotstat_url:` URL of data to be downloaded from .Stat Suite
|
|
124
|
+
* `content_format:` Format of the downloaded content
|
|
125
|
+
|
|
126
|
+
#### 3. To upload a data file to .Stat Suite:
|
|
127
|
+
```python
|
|
128
|
+
from dotstat_io.download_upload import Download_upload
|
|
129
|
+
with Download_upload(adfsAuthentication_obj, access_token) as Download_upload_obj:
|
|
130
|
+
Returned_Message = Download_upload_obj.upload_file(
|
|
131
|
+
transfer_url, Path(file_path), space, validationType, use_filepath)
|
|
132
|
+
```
|
|
133
|
+
* `transfer_url:` URL of the transfer service
|
|
134
|
+
* `file_path:` The full path of the SDMX-CSV file, which will be uploaded to .Stat Suite
|
|
135
|
+
* `space:` Data space where the file will be uploaded
|
|
136
|
+
* `validationType:` The type of validation to use during upload. Possible values: Basic Validation (0), Advanced Validation (1)
|
|
137
|
+
* `use_filepath:` Use a file path of a shared folder accessible by the .Stat Suite data upload engine (for unlimited file sizes)
|
|
138
|
+
|
|
139
|
+
#### 4. To upload a structure to .Stat Suite:
|
|
140
|
+
```python
|
|
141
|
+
from dotstat_io.download_upload import Download_upload
|
|
142
|
+
with Download_upload(adfsAuthentication_obj, access_token) as Download_upload_obj:
|
|
143
|
+
Returned_Message = Download_upload_obj.upload_structure(
|
|
144
|
+
transfer_url, Path(file_path))
|
|
145
|
+
```
|
|
146
|
+
* `transfer_url:` URL of the transfer service
|
|
147
|
+
* `file_path:` The full path of the SDMX-ML file, which will be uploaded to .Stat Suite
|
|
148
|
+
|
|
149
|
+
### For more information about how to use this package, all test cases can be accessed from this [`link`](https://gitlab.algobank.oecd.org/sdd-legacy/dotstat_io/-/blob/main/tests/test_cases.py)
|
|
150
|
+
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
dotstat_io/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
dotstat_io/authentication.py,sha256=_OCTbSUcdD1jFoDbtMi48xC5U5ZsizC7w9K0Ar7mSAk,12236
|
|
3
|
+
dotstat_io/download_upload.py,sha256=ss-eZRhSL4iHzmv9vkY6x1BgMUgwMqKLkET2XmFC4-Q,16163
|
|
4
|
+
dotstat_io-0.2.7.dist-info/METADATA,sha256=tuGvwxeiR7CsKPLhY1yA3Dpz6h1ftK9MmyXkVcN1f0E,6930
|
|
5
|
+
dotstat_io-0.2.7.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
6
|
+
dotstat_io-0.2.7.dist-info/RECORD,,
|