dotstat_io 1.0.2__tar.gz → 1.0.4__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.

Potentially problematic release.


This version of dotstat_io might be problematic. Click here for more details.

@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: dotstat_io
3
- Version: 1.0.2
3
+ Version: 1.0.4
4
4
  Summary: Utility to download or upload data from/to .Stat Suite using ADFS authentication to connect to it
5
5
  License: MIT
6
6
  Author: Gyorgy Gyomai
@@ -147,12 +147,25 @@ returned_result = client_obj.upload_data_file(
147
147
  transfer_url, Path(file_path), space, validationType, use_filepath)
148
148
  ```
149
149
  * `transfer_url:` URL of the transfer service
150
- * `file_path:` The full path of the SDMX-CSV file, which will be uploaded to .Stat Suite
150
+ * `file_path:` The full path of the SDMX-CSV file to be imported
151
151
  * `space:` Data space where the file will be uploaded
152
152
  * `validationType:` The type of validation to use during upload. Possible values: Basic Validation (0), Advanced Validation (1)
153
153
  * `use_filepath:` Use a file path of a shared folder accessible by the .Stat Suite data upload engine (for unlimited file sizes)
154
154
 
155
- #### 6. To upload a structure to .Stat Suite:
155
+ #### 6. To upload an Excel data file to .Stat Suite:
156
+ ```python
157
+ from dotstat_io.client import Client
158
+ client_obj = Client.init_with_authentication_obj(Authentication_obj)
159
+ returned_result = client_obj.upload_excel_data_file(
160
+ transfer_url, Path(excelfile_path), Path(eddfile_path), space, validationType)
161
+ ```
162
+ * `transfer_url:` URL of the transfer service
163
+ * `excelfile_path:` The full path of the Excel file containing the data/referential metadata values to be imported
164
+ * `eddfile_path:` The full path of the XML edd file containing the description of the Excel file to be imported
165
+ * `space:` Data space where the file will be uploaded
166
+ * `validationType:` The type of validation to use during upload. Possible values: Basic Validation (0), Advanced Validation (1)
167
+
168
+ #### 7. To upload a structure to .Stat Suite:
156
169
  ```python
157
170
  from dotstat_io.client import Client
158
171
  client_obj = Client.init_with_authentication_obj(Authentication_obj)
@@ -160,7 +173,7 @@ returned_result = client_obj.upload_structure(
160
173
  transfer_url, Path(file_path))
161
174
  ```
162
175
  * `transfer_url:` URL of the transfer service
163
- * `file_path:` The full path of the SDMX-ML file, which will be uploaded to .Stat Suite
176
+ * `file_path:` The full path of the SDMX-ML file to be imported
164
177
 
165
178
  ### 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)
166
179
 
@@ -126,12 +126,25 @@ returned_result = client_obj.upload_data_file(
126
126
  transfer_url, Path(file_path), space, validationType, use_filepath)
127
127
  ```
128
128
  * `transfer_url:` URL of the transfer service
129
- * `file_path:` The full path of the SDMX-CSV file, which will be uploaded to .Stat Suite
129
+ * `file_path:` The full path of the SDMX-CSV file to be imported
130
130
  * `space:` Data space where the file will be uploaded
131
131
  * `validationType:` The type of validation to use during upload. Possible values: Basic Validation (0), Advanced Validation (1)
132
132
  * `use_filepath:` Use a file path of a shared folder accessible by the .Stat Suite data upload engine (for unlimited file sizes)
133
133
 
134
- #### 6. To upload a structure to .Stat Suite:
134
+ #### 6. To upload an Excel data file to .Stat Suite:
135
+ ```python
136
+ from dotstat_io.client import Client
137
+ client_obj = Client.init_with_authentication_obj(Authentication_obj)
138
+ returned_result = client_obj.upload_excel_data_file(
139
+ transfer_url, Path(excelfile_path), Path(eddfile_path), space, validationType)
140
+ ```
141
+ * `transfer_url:` URL of the transfer service
142
+ * `excelfile_path:` The full path of the Excel file containing the data/referential metadata values to be imported
143
+ * `eddfile_path:` The full path of the XML edd file containing the description of the Excel file to be imported
144
+ * `space:` Data space where the file will be uploaded
145
+ * `validationType:` The type of validation to use during upload. Possible values: Basic Validation (0), Advanced Validation (1)
146
+
147
+ #### 7. To upload a structure to .Stat Suite:
135
148
  ```python
136
149
  from dotstat_io.client import Client
137
150
  client_obj = Client.init_with_authentication_obj(Authentication_obj)
@@ -139,6 +152,6 @@ returned_result = client_obj.upload_structure(
139
152
  transfer_url, Path(file_path))
140
153
  ```
141
154
  * `transfer_url:` URL of the transfer service
142
- * `file_path:` The full path of the SDMX-ML file, which will be uploaded to .Stat Suite
155
+ * `file_path:` The full path of the SDMX-ML file to be imported
143
156
 
144
157
  ### 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)
@@ -1,3 +1,4 @@
1
+ from enum import IntEnum
1
2
  import os
2
3
  import requests
3
4
  import chardet
@@ -8,6 +9,12 @@ import xml.etree.ElementTree as ET
8
9
  from pathlib import Path
9
10
  from time import sleep
10
11
 
12
+ from dotstat_io.authentication import Authentication
13
+
14
+ class ValidationType(IntEnum):
15
+ BASIC = 0
16
+ ADVANCED = 1
17
+
11
18
 
12
19
  # class to download or upload data from/to .Stat Suite
13
20
  class Client():
@@ -35,10 +42,10 @@ class Client():
35
42
 
36
43
  # Initialise Client
37
44
  def __init__(self,
38
- access_token: str = None,
39
- authentication_obj: object = None):
40
- self.__access_token = access_token
41
- self.__authentication_obj = authentication_obj
45
+ access_token: None | str = None,
46
+ authentication_obj: None | Authentication = None):
47
+ Client.__access_token = access_token
48
+ Client.__authentication_obj = authentication_obj
42
49
 
43
50
 
44
51
  #
@@ -65,7 +72,7 @@ class Client():
65
72
  @classmethod
66
73
  def init_with_authentication_obj(
67
74
  cls,
68
- authentication_obj: object
75
+ authentication_obj: Authentication
69
76
  ):
70
77
  return cls(
71
78
  authentication_obj=authentication_obj
@@ -78,18 +85,18 @@ class Client():
78
85
  returned_result = ""
79
86
 
80
87
  #
81
- if self.__authentication_obj is not None:
82
- self.__access_token = self.__authentication_obj.get_token()
88
+ if Client.__authentication_obj is not None:
89
+ Client.__access_token = Client.__authentication_obj.get_token()
83
90
 
84
91
  headers = {
85
92
  'accept': content_format,
86
- 'authorization': 'Bearer '+self.__access_token
93
+ 'authorization': 'Bearer '+Client.__access_token
87
94
  }
88
95
 
89
96
  #
90
97
  response = requests.get(dotstat_url, verify=True, headers=headers)
91
98
  except Exception as err:
92
- returned_result = self.__ERROR_OCCURRED + str(err) + os.linesep
99
+ returned_result = Client.__ERROR_OCCURRED + str(err) + os.linesep
93
100
 
94
101
  # Write the result to the log
95
102
  for line in returned_result.split(os.linesep):
@@ -97,7 +104,7 @@ class Client():
97
104
  self.__log.info(' ' + line)
98
105
  else:
99
106
  if response.status_code != 200:
100
- returned_result = self.__ERROR_OCCURRED
107
+ returned_result = Client.__ERROR_OCCURRED
101
108
  if len(str(response.status_code)) > 0:
102
109
  returned_result += 'Error code: ' + str(response.status_code) + os.linesep
103
110
  if len(str(response.reason)) > 0:
@@ -111,7 +118,7 @@ class Client():
111
118
  os.remove(file_path)
112
119
  with open(file_path, "wb") as file:
113
120
  file.write(response.content)
114
- returned_result = self.__DOWNLOAD_SUCCESS
121
+ returned_result = Client.__DOWNLOAD_SUCCESS
115
122
 
116
123
  # Write the result to the log
117
124
  for line in returned_result.split(os.linesep):
@@ -127,19 +134,19 @@ class Client():
127
134
  returned_result = ""
128
135
 
129
136
  #
130
- if self.__authentication_obj is not None:
131
- __access_token = self.__authentication_obj.get_token()
137
+ if Client.__authentication_obj is not None:
138
+ Client.__access_token = Client.__authentication_obj.get_token()
132
139
 
133
140
  headers = {
134
141
  'accept': content_format,
135
142
  'Transfer-Encoding': 'chunked',
136
- 'authorization': 'Bearer '+self.__access_token
143
+ 'authorization': 'Bearer '+Client.__access_token
137
144
  }
138
145
 
139
146
  #
140
147
  return requests.get(dotstat_url, verify=True, headers=headers, stream=True)
141
148
  except Exception as err:
142
- returned_result = self.__ERROR_OCCURRED + str(err) + os.linesep
149
+ returned_result = Client.__ERROR_OCCURRED + str(err) + os.linesep
143
150
  return returned_result
144
151
 
145
152
 
@@ -154,8 +161,8 @@ class Client():
154
161
  returned_result = ""
155
162
 
156
163
  #
157
- if self.__authentication_obj is not None:
158
- self.__access_token = self.__authentication_obj.get_token()
164
+ if Client.__authentication_obj is not None:
165
+ Client.__access_token = Client.__authentication_obj.get_token()
159
166
 
160
167
  payload = {
161
168
  'dataspace': space,
@@ -164,7 +171,7 @@ class Client():
164
171
 
165
172
  headers = {
166
173
  'accept': 'application/json',
167
- 'authorization': "Bearer "+self.__access_token
174
+ 'authorization': "Bearer "+Client.__access_token
168
175
  }
169
176
 
170
177
  if use_filepath:
@@ -183,7 +190,7 @@ class Client():
183
190
  #
184
191
  response = requests.post(transfer_url, verify=True, headers=headers, files=files)
185
192
  except Exception as err:
186
- returned_result = self.__ERROR_OCCURRED + str(err) + os.linesep
193
+ returned_result = Client.__ERROR_OCCURRED + str(err) + os.linesep
187
194
 
188
195
  # Write the result to the log
189
196
  for line in returned_result.split(os.linesep):
@@ -204,7 +211,7 @@ class Client():
204
211
  returned_result = result + os.linesep
205
212
 
206
213
  # Check the request status
207
- if (result != "" and result.find(self.__ERROR_OCCURRED ) == -1):
214
+ if (result != "" and result.find(Client.__ERROR_OCCURRED ) == -1):
208
215
  # Extract the request ID the returned message
209
216
  start = 'with ID'
210
217
  end = 'was successfully'
@@ -225,8 +232,8 @@ class Client():
225
232
  sleep(3)
226
233
 
227
234
  previous_result = result
228
- while (result in [self.__EXECUTION_IN_PROGRESS, self.__EXECUTION_IN_QUEUED]
229
- or self.__CONNECTION_ABORTED in result):
235
+ while (result in [Client.__EXECUTION_IN_PROGRESS, Client.__EXECUTION_IN_QUEUED]
236
+ or Client.__CONNECTION_ABORTED in result):
230
237
  result = self.__check_request_status(transfer_url, requestId, space)
231
238
 
232
239
  # Prevent loging again the same information such as "Queued" or "InProgress"
@@ -235,14 +242,14 @@ class Client():
235
242
 
236
243
  # Write the result to the log
237
244
  for line in previous_result.split(os.linesep):
238
- if (len(line) > 0 and line not in [self.__EXECUTION_IN_PROGRESS, self.__EXECUTION_IN_QUEUED]
239
- and self.__CONNECTION_ABORTED not in line):
245
+ if (len(line) > 0 and line not in [Client.__EXECUTION_IN_PROGRESS, Client.__EXECUTION_IN_QUEUED]
246
+ and Client.__CONNECTION_ABORTED not in line):
240
247
  self.__log.info(' ' + line)
241
248
  sleep(3)
242
249
 
243
250
  returned_result = returned_result + result + os.linesep
244
251
  else:
245
- returned_result = self.__ERROR_OCCURRED
252
+ returned_result = Client.__ERROR_OCCURRED
246
253
  if len(str(response.status_code)) > 0:
247
254
  returned_result += 'Error code: ' + str(response.status_code) + os.linesep
248
255
  if len(str(response.reason)) > 0:
@@ -256,7 +263,7 @@ class Client():
256
263
  if len(line) > 0:
257
264
  self.__log.info(' ' + line)
258
265
  except ValueError as err:
259
- returned_result = self.__ERROR_OCCURRED
266
+ returned_result = Client.__ERROR_OCCURRED
260
267
  if len(str(response.status_code)) > 0:
261
268
  returned_result += 'Error code: ' + str(response.status_code) + os.linesep
262
269
  if len(str(response.reason)) > 0:
@@ -271,14 +278,138 @@ class Client():
271
278
  return returned_result
272
279
 
273
280
 
281
+ # Upload an Excel data file to .Stat Suite
282
+ def upload_excel_data_file(self,
283
+ transfer_url: str,
284
+ excelfile_path: Path,
285
+ eddfile_path: Path,
286
+ space: str,
287
+ validationType: int):
288
+ try:
289
+ returned_result = ""
290
+
291
+ #
292
+ if Client.__authentication_obj is not None:
293
+ Client.__access_token = Client.__authentication_obj.get_token()
294
+
295
+ payload = {
296
+ 'dataspace': space,
297
+ 'validationType': validationType
298
+ }
299
+
300
+ headers = {
301
+ 'accept': 'application/json',
302
+ 'authorization': "Bearer "+Client.__access_token
303
+ }
304
+
305
+ excel_file = open(os.path.realpath(excelfile_path), 'rb')
306
+ eddfile_file = open(os.path.realpath(eddfile_path), 'rb')
307
+ files = {
308
+ 'dataspace': (None, payload['dataspace']),
309
+ 'validationType': (None, payload['validationType']),
310
+ 'excelFile': (str(excelfile_path), excel_file, 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', ''),
311
+ 'eddFile': (str(eddfile_path), eddfile_file, 'text/xml', '')
312
+ }
313
+
314
+ #
315
+ response = requests.post(transfer_url, verify=True, headers=headers, files=files)
316
+ except Exception as err:
317
+ returned_result = Client.__ERROR_OCCURRED + str(err) + os.linesep
318
+
319
+ # Write the result to the log
320
+ for line in returned_result.split(os.linesep):
321
+ if len(line) > 0:
322
+ self.__log.info(' ' + line)
323
+ else:
324
+ # If the response object cannot be converted to json, return an error
325
+ results_json = None
326
+ try:
327
+ results_json = json.loads(response.text)
328
+ if response.status_code == 200:
329
+ result = results_json['message']
330
+ # Write the result to the log
331
+ for line in result.split(os.linesep):
332
+ if len(line) > 0:
333
+ self.__log.info(' ' + line)
334
+
335
+ returned_result = result + os.linesep
336
+
337
+ # Check the request status
338
+ if (result != "" and result.find(Client.__ERROR_OCCURRED ) == -1):
339
+ # Extract the request ID the returned message
340
+ start = 'with ID'
341
+ end = 'was successfully'
342
+ requestId = result[result.find(
343
+ start)+len(start):result.rfind(end)]
344
+
345
+ # Sleep a little bit before checking the request status
346
+ sleep(3)
347
+
348
+ # To avoid this error: maximum recursion depth exceeded while calling a Python object
349
+ # replace the recursive calls with while loops.
350
+ result = self.__check_request_status(transfer_url, requestId, space)
351
+
352
+ # Write the result to the log
353
+ for line in result.split(os.linesep):
354
+ if len(line) > 0:
355
+ self.__log.info(' ' + line)
356
+ sleep(3)
357
+
358
+ previous_result = result
359
+ while (result in [Client.__EXECUTION_IN_PROGRESS, Client.__EXECUTION_IN_QUEUED]
360
+ or Client.__CONNECTION_ABORTED in result):
361
+ result = self.__check_request_status(transfer_url, requestId, space)
362
+
363
+ # Prevent loging again the same information such as "Queued" or "InProgress"
364
+ if previous_result != result:
365
+ previous_result = result
366
+
367
+ # Write the result to the log
368
+ for line in previous_result.split(os.linesep):
369
+ if (len(line) > 0 and line not in [Client.__EXECUTION_IN_PROGRESS, Client.__EXECUTION_IN_QUEUED]
370
+ and Client.__CONNECTION_ABORTED not in line):
371
+ self.__log.info(' ' + line)
372
+ sleep(3)
373
+
374
+ returned_result = returned_result + result + os.linesep
375
+ else:
376
+ returned_result = Client.__ERROR_OCCURRED
377
+ if len(str(response.status_code)) > 0:
378
+ returned_result += 'Error code: ' + str(response.status_code) + os.linesep
379
+ if len(str(response.reason)) > 0:
380
+ returned_result += 'Reason: ' + str(response.reason) + os.linesep
381
+ if len(response.text) > 0:
382
+ returned_result += 'Text: ' + response.text
383
+
384
+ returned_result += os.linesep
385
+ # Write the result to the log
386
+ for line in returned_result.split(os.linesep):
387
+ if len(line) > 0:
388
+ self.__log.info(' ' + line)
389
+ except ValueError as err:
390
+ returned_result = Client.__ERROR_OCCURRED
391
+ if len(str(response.status_code)) > 0:
392
+ returned_result += 'Error code: ' + str(response.status_code) + os.linesep
393
+ if len(str(response.reason)) > 0:
394
+ returned_result += 'Reason: ' + str(response.reason) + os.linesep
395
+ if len(response.text) > 0:
396
+ returned_result += 'Text: ' + str(response.text)
397
+ else:
398
+ returned_result += str(err)
399
+
400
+ returned_result += os.linesep
401
+ finally:
402
+ return returned_result
403
+
404
+
274
405
  # Upload a structure file to .Stat Suite
275
406
  def upload_structure(self, transfer_url: str, file_path: Path):
276
407
  try:
277
408
  returned_result = ""
278
409
 
279
410
  #
280
- if self.__authentication_obj is not None:
281
- self.__access_token = self.__authentication_obj.get_token()
411
+ if Client.__authentication_obj is not None:
412
+ Client.__access_token = Client.__authentication_obj.get_token()
282
413
 
283
414
  # Detect the encoding used in file
284
415
  detected_encoding = self.__detect_encode(file_path)
@@ -293,13 +424,13 @@ class Client():
293
424
 
294
425
  headers = {
295
426
  'Content-Type': 'application/xml',
296
- 'authorization': "Bearer "+self.__access_token
427
+ 'authorization': "Bearer "+Client.__access_token
297
428
  }
298
429
 
299
430
  #
300
431
  response = requests.post(transfer_url, verify=True, headers=headers, data=xml_data)
301
432
  except Exception as err:
302
- returned_result = self.__ERROR_OCCURRED + str(err) + os.linesep
433
+ returned_result = Client.__ERROR_OCCURRED + str(err) + os.linesep
303
434
 
304
435
  # Write the result to the log
305
436
  for line in returned_result.split(os.linesep):
@@ -309,7 +440,7 @@ class Client():
309
440
  try:
310
441
  response.raise_for_status()
311
442
  except requests.exceptions.HTTPError as e:
312
- returned_result = f'{self.__UPLOAD_FAILED}{response.status_code}: {e}'
443
+ returned_result = f'{Client.__UPLOAD_FAILED}{response.status_code}: {e}'
313
444
 
314
445
  # Write the result to the log
315
446
  for line in returned_result.split(os.linesep):
@@ -317,11 +448,11 @@ class Client():
317
448
  self.__log.info(' ' + line)
318
449
  else:
319
450
  response_tree = ET.XML(response.content)
320
- for self.__ERROR_OCCURRED in response_tree.findall("./{0}ErrorMessage".format(self.__NAMESPACE_MESSAGE)):
321
- text_element = self.__ERROR_OCCURRED.find("./{0}Text".format(self.__NAMESPACE_COMMON))
451
+ for element in response_tree.findall("./{0}ErrorMessage".format(Client.__NAMESPACE_MESSAGE)):
452
+ text_element = element.find("./{0}Text".format(Client.__NAMESPACE_COMMON))
322
453
  if (text_element is not None):
323
454
  if returned_result == "":
324
- returned_result = f'{self.__UPLOAD_SUCCESS}with status code: {response.status_code}' + os.linesep
455
+ returned_result = f'{Client.__UPLOAD_SUCCESS}with status code: {response.status_code}' + os.linesep
325
456
  returned_result = returned_result + text_element.text + os.linesep
326
457
 
327
458
  # Write the result to the log
@@ -355,12 +486,12 @@ class Client():
355
486
  returned_result = ""
356
487
 
357
488
  #
358
- if self.__authentication_obj is not None:
359
- self.__access_token = self.__authentication_obj.get_token()
489
+ if Client.__authentication_obj is not None:
490
+ Client.__access_token = Client.__authentication_obj.get_token()
360
491
 
361
492
  headers = {
362
493
  'accept': 'application/json',
363
- 'authorization': "Bearer "+self.__access_token
494
+ 'authorization': "Bearer "+Client.__access_token
364
495
  }
365
496
 
366
497
  payload = {
@@ -369,12 +500,15 @@ class Client():
369
500
  }
370
501
 
371
502
  transfer_url = transfer_url.replace("import", "status")
372
- transfer_url = transfer_url.replace("sdmxFile", "request")
373
-
503
+ if "sdmxFile" in transfer_url:
504
+ transfer_url = transfer_url.replace("sdmxFile", "request")
505
+ elif "excel" in transfer_url:
506
+ transfer_url = transfer_url.replace("excel", "request")
507
+
374
508
  #
375
509
  response = requests.post(transfer_url, verify=True, headers=headers, data=payload)
376
510
  except Exception as err:
377
- returned_result = self.__ERROR_OCCURRED + str(err)
511
+ returned_result = Client.__ERROR_OCCURRED + str(err)
378
512
  else:
379
513
  # If the response object cannot be converted to json, return an error
380
514
  results_json = None
@@ -382,8 +516,8 @@ class Client():
382
516
  results_json = json.loads(response.text)
383
517
  if response.status_code == 200:
384
518
  executionStatus = 'Execution status: ' + results_json['executionStatus']
385
- if (results_json['executionStatus'] in [self.__EXECUTION_IN_PROGRESS, self.__EXECUTION_IN_QUEUED]
386
- or self.__CONNECTION_ABORTED in results_json['executionStatus']):
519
+ if (results_json['executionStatus'] in [Client.__EXECUTION_IN_PROGRESS, Client.__EXECUTION_IN_QUEUED]
520
+ or Client.__CONNECTION_ABORTED in results_json['executionStatus']):
387
521
  returned_result = results_json['executionStatus']
388
522
  else:
389
523
  returned_result = executionStatus + os.linesep + 'Outcome: ' + results_json['outcome'] + os.linesep
@@ -392,7 +526,7 @@ class Client():
392
526
  returned_result = returned_result + 'Log' + str(index) + ': ' + results_json['logs'][index]['message'] + os.linesep
393
527
  index += 1
394
528
  else:
395
- returned_result = self.__ERROR_OCCURRED
529
+ returned_result = Client.__ERROR_OCCURRED
396
530
  if len(str(response.status_code)) > 0:
397
531
  returned_result += 'Error code: ' + str(response.status_code) + os.linesep
398
532
  if len(str(response.reason)) > 0:
@@ -402,7 +536,7 @@ class Client():
402
536
 
403
537
  returned_result += os.linesep
404
538
  except ValueError as err:
405
- returned_result = self.__ERROR_OCCURRED
539
+ returned_result = Client.__ERROR_OCCURRED
406
540
  if len(str(response.status_code)) > 0:
407
541
  returned_result += 'Error code: ' + str(response.status_code) + os.linesep
408
542
  if len(str(response.reason)) > 0:
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "dotstat_io"
3
- version = "1.0.2"
3
+ version = "1.0.4"
4
4
  description = "Utility to download or upload data from/to .Stat Suite using ADFS authentication to connect to it"
5
5
  license = "MIT"
6
6
  authors = ["Gyorgy Gyomai <gyorgy.gyomai@oecd.org>", "Abdel Aliaoui <abdel.aliaoui@oecd.org>"]