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 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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 1.9.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any