dotstat_io 0.2.7__py3-none-any.whl → 1.0.0__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/client.py ADDED
@@ -0,0 +1,392 @@
1
+ import os
2
+ import requests
3
+ import chardet
4
+ import json
5
+ import logging
6
+ import xml.etree.ElementTree as ET
7
+
8
+ from pathlib import Path
9
+ from time import sleep
10
+
11
+
12
+ # class to download or upload data from/to .Stat Suite
13
+ class Client():
14
+
15
+ # private constants
16
+ __ERROR_OCCURRED = "An error occurred: "
17
+ __EXECUTION_IN_QUEUED = "Queued"
18
+ __EXECUTION_IN_PROGRESS = "InProgress"
19
+ __CONNECTION_ABORTED = "An existing connection was forcibly closed by the remote host"
20
+ __DOWNLOAD_SUCCESS = "Successful download"
21
+ __UPLOAD_SUCCESS = "The request was successfully processed "
22
+ __UPLOAD_FAILED = "The request failed with status code "
23
+
24
+ __NAMESPACE_MESSAGE = "{http://www.sdmx.org/resources/sdmxml/schemas/v2_1/message}"
25
+ __NAMESPACE_COMMON = "{http://www.sdmx.org/resources/sdmxml/schemas/v2_1/common}"
26
+
27
+ # private variables
28
+ __access_token = None
29
+ __authentication_obj = None
30
+
31
+ # Prepare logging format
32
+ FORMAT = '%(message)s'
33
+ logging.basicConfig(format=FORMAT, level=logging.INFO)
34
+ __log = logging.getLogger(__name__)
35
+
36
+ # Initialise Client
37
+ 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
42
+
43
+
44
+ #
45
+ def __enter__(self):
46
+ return self
47
+
48
+ #
49
+ def __exit__(self, exc_type, exc_value, traceback):
50
+ self.__access_token = None
51
+ self.__authentication_obj = None
52
+
53
+
54
+ #
55
+ @classmethod
56
+ def init_with_access_token(
57
+ cls,
58
+ access_token: str
59
+ ):
60
+ return cls(
61
+ access_token=access_token
62
+ )
63
+
64
+ #
65
+ @classmethod
66
+ def init_with_authentication_obj(
67
+ cls,
68
+ authentication_obj: object
69
+ ):
70
+ return cls(
71
+ authentication_obj=authentication_obj
72
+ )
73
+
74
+
75
+ # Download a data file from .Stat Suite
76
+ def download_data_file(self, dotstat_url: str, content_format: str, file_path: Path):
77
+ try:
78
+ returned_result = ""
79
+
80
+ #
81
+ if self.__authentication_obj is not None:
82
+ self.__access_token = self.__authentication_obj.get_token()
83
+
84
+ headers = {
85
+ 'accept': content_format,
86
+ 'authorization': 'Bearer '+self.__access_token
87
+ }
88
+
89
+ #
90
+ response = requests.get(dotstat_url, verify=True, headers=headers)
91
+ except Exception as err:
92
+ returned_result = self.__ERROR_OCCURRED + str(err) + os.linesep
93
+
94
+ # Write the result to the log
95
+ for line in returned_result.split(os.linesep):
96
+ if len(line) > 0:
97
+ self.__log.info(' ' + line)
98
+ else:
99
+ if response.status_code != 200:
100
+ returned_result = self.__ERROR_OCCURRED + 'Error code: ' + \
101
+ str(response.status_code) + ' Reason: ' + str(response.reason)
102
+ if len(response.text) > 0:
103
+ returned_result += os.linesep + 'Text: ' + response.text
104
+ else:
105
+ if os.path.isfile(file_path):
106
+ os.remove(file_path)
107
+ with open(file_path, "wb") as file:
108
+ file.write(response.content)
109
+ returned_result = self.__DOWNLOAD_SUCCESS
110
+
111
+ # Write the result to the log
112
+ for line in returned_result.split(os.linesep):
113
+ if len(line) > 0:
114
+ self.__log.info(' ' + line)
115
+ finally:
116
+ return returned_result
117
+
118
+
119
+ # Download streamed content from .Stat Suite
120
+ def download_data_stream(self, dotstat_url: str, content_format: str):
121
+ try:
122
+ returned_result = ""
123
+
124
+ #
125
+ if self.__authentication_obj is not None:
126
+ __access_token = self.__authentication_obj.get_token()
127
+
128
+ headers = {
129
+ 'accept': content_format,
130
+ 'Transfer-Encoding': 'chunked',
131
+ 'authorization': 'Bearer '+self.__access_token
132
+ }
133
+
134
+ #
135
+ return requests.get(dotstat_url, verify=True, headers=headers, stream=True)
136
+ except Exception as err:
137
+ returned_result = self.__ERROR_OCCURRED + str(err) + os.linesep
138
+ return returned_result
139
+
140
+
141
+ # Upload a data file to .Stat Suite
142
+ def upload_data_file(self,
143
+ transfer_url: str,
144
+ file_path: Path,
145
+ space: str,
146
+ validationType: int,
147
+ use_filepath: bool = False):
148
+ try:
149
+ returned_result = ""
150
+
151
+ #
152
+ if self.__authentication_obj is not None:
153
+ self.__access_token = self.__authentication_obj.get_token()
154
+
155
+ payload = {
156
+ 'dataspace': space,
157
+ 'validationType': validationType
158
+ }
159
+
160
+ headers = {
161
+ 'accept': 'application/json',
162
+ 'authorization': "Bearer "+self.__access_token
163
+ }
164
+
165
+ if use_filepath:
166
+ files = {
167
+ 'dataspace': (None, payload['dataspace']),
168
+ 'validationType': (None, payload['validationType']),
169
+ 'filepath': (None, str(file_path))
170
+ }
171
+ else:
172
+ files = {
173
+ 'dataspace': (None, payload['dataspace']),
174
+ 'validationType': (None, payload['validationType']),
175
+ 'file': (os.path.realpath(file_path), open(os.path.realpath(file_path), 'rb'), 'text/csv', '')
176
+ }
177
+
178
+ #
179
+ response = requests.post(transfer_url, verify=True, headers=headers, files=files)
180
+ except Exception as err:
181
+ returned_result = self.__ERROR_OCCURRED + str(err) + os.linesep
182
+
183
+ # Write the result to the log
184
+ for line in returned_result.split(os.linesep):
185
+ if len(line) > 0:
186
+ self.__log.info(' ' + line)
187
+ else:
188
+ # If the response object cannot be converted to json, return an error
189
+ results_json = None
190
+ try:
191
+ results_json = json.loads(response.text)
192
+ if response.status_code == 200:
193
+ result = results_json['message']
194
+ # Write the result to the log
195
+ for line in result.split(os.linesep):
196
+ if len(line) > 0:
197
+ self.__log.info(' ' + line)
198
+
199
+ returned_result = result + os.linesep
200
+
201
+ # Check the request status
202
+ if (result != "" and result.find(self.__ERROR_OCCURRED ) == -1):
203
+ # Extract the request ID the returned message
204
+ start = 'with ID'
205
+ end = 'was successfully'
206
+ requestId = result[result.find(
207
+ start)+len(start):result.rfind(end)]
208
+
209
+ # Sleep a little bit before checking the request status
210
+ sleep(3)
211
+
212
+ # To avoid this error: maximum recursion depth exceeded while calling a Python object
213
+ # replace the recursive calls with while loops.
214
+ result = self.__check_request_status(transfer_url, requestId, space)
215
+
216
+ # Write the result to the log
217
+ for line in result.split(os.linesep):
218
+ if len(line) > 0:
219
+ self.__log.info(' ' + line)
220
+ sleep(3)
221
+
222
+ previous_result = result
223
+ while (result in [self.__EXECUTION_IN_PROGRESS, self.__EXECUTION_IN_QUEUED]
224
+ or self.__CONNECTION_ABORTED in result):
225
+ result = self.__check_request_status(transfer_url, requestId, space)
226
+
227
+ # Prevent loging again the same information such as "Queued" or "InProgress"
228
+ if previous_result != result:
229
+ previous_result = result
230
+
231
+ # Write the result to the log
232
+ for line in previous_result.split(os.linesep):
233
+ if (len(line) > 0 and line not in [self.__EXECUTION_IN_PROGRESS, self.__EXECUTION_IN_QUEUED]
234
+ and self.__CONNECTION_ABORTED not in line):
235
+ self.__log.info(' ' + line)
236
+ sleep(3)
237
+
238
+ returned_result = returned_result + result + os.linesep
239
+ else:
240
+ returned_result = self.__ERROR_OCCURRED + 'Error code: ' + str(response.status_code) + os.linesep
241
+ if len(str(response.reason)) > 0:
242
+ returned_result += 'Reason: ' + str(response.reason) + os.linesep
243
+ if len(response.text) > 0:
244
+ returned_result = returned_result + os.linesep + 'Text: ' + response.text
245
+
246
+ returned_result = returned_result + os.linesep
247
+ # Write the result to the log
248
+ for line in returned_result.split(os.linesep):
249
+ if len(line) > 0:
250
+ self.__log.info(' ' + line)
251
+ except ValueError as err:
252
+ if len(response.text) > 0:
253
+ self.init_status = self._ERROR + os.linesep + str(response.text)
254
+ else:
255
+ self.init_status = self._ERROR + os.linesep + str(err)
256
+ finally:
257
+ return returned_result
258
+
259
+
260
+ # Upload a structure file to .Stat Suite
261
+ def upload_structure(self, transfer_url: str, file_path: Path):
262
+ try:
263
+ returned_result = ""
264
+
265
+ #
266
+ if self.__authentication_obj is not None:
267
+ self.__access_token = self.__authentication_obj.get_token()
268
+
269
+ # Detect the encoding used in file
270
+ detected_encoding = self.__detect_encode(file_path)
271
+
272
+ # Read file as a string "r+" with the detected encoding
273
+ with open(file=file_path, mode="r+", encoding=detected_encoding.get("encoding")) as file:
274
+ xml_data = file.read()
275
+
276
+ # Make sure the encoding is "utf-8"
277
+ tree = ET.fromstring(xml_data)
278
+ xml_data = ET.tostring(tree, encoding="utf-8", method='xml', xml_declaration=True)
279
+
280
+ headers = {
281
+ 'Content-Type': 'application/xml',
282
+ 'authorization': "Bearer "+self.__access_token
283
+ }
284
+
285
+ #
286
+ response = requests.post(transfer_url, verify=True, headers=headers, data=xml_data)
287
+ except Exception as err:
288
+ returned_result = self.__ERROR_OCCURRED + str(err) + os.linesep
289
+
290
+ # Write the result to the log
291
+ for line in returned_result.split(os.linesep):
292
+ if len(line) > 0:
293
+ self.__log.info(' ' + line)
294
+ else:
295
+ try:
296
+ response.raise_for_status()
297
+ except requests.exceptions.HTTPError as e:
298
+ returned_result = f'{self.__UPLOAD_FAILED}{response.status_code}: {e}'
299
+
300
+ # Write the result to the log
301
+ for line in returned_result.split(os.linesep):
302
+ if len(line) > 0:
303
+ self.__log.info(' ' + line)
304
+ else:
305
+ response_tree = ET.XML(response.content)
306
+ for self.__ERROR_OCCURRED in response_tree.findall("./{0}ErrorMessage".format(self.__NAMESPACE_MESSAGE)):
307
+ text_element = self.__ERROR_OCCURRED.find("./{0}Text".format(self.__NAMESPACE_COMMON))
308
+ if (text_element is not None):
309
+ if returned_result == "":
310
+ returned_result = f'{self.__UPLOAD_SUCCESS}with status code: {response.status_code}' + os.linesep
311
+ returned_result = returned_result + text_element.text + os.linesep
312
+
313
+ # Write the result to the log
314
+ for line in returned_result.split(os.linesep):
315
+ if len(line) > 0:
316
+ self.__log.info(' ' + line)
317
+ finally:
318
+ return returned_result
319
+
320
+
321
+ # Detect the encoding used in file
322
+ def __detect_encode(self, file_path):
323
+ detector = chardet.UniversalDetector()
324
+ detector.reset()
325
+ with open(file=file_path, mode="rb") as file:
326
+ for row in file:
327
+ detector.feed(row)
328
+ if detector.done:
329
+ break
330
+
331
+ detector.close()
332
+
333
+ return detector.result
334
+
335
+
336
+ # Check request sent to .Stat Suite status
337
+ # To avoid this error: maximum recursion depth exceeded while calling a Python object
338
+ # replace the recursive calls with while loops.
339
+ def __check_request_status(self, transfer_url, requestId, space):
340
+ try:
341
+ returned_result = ""
342
+
343
+ #
344
+ if self.__authentication_obj is not None:
345
+ self.__access_token = self.__authentication_obj.get_token()
346
+
347
+ headers = {
348
+ 'accept': 'application/json',
349
+ 'authorization': "Bearer "+self.__access_token
350
+ }
351
+
352
+ payload = {
353
+ 'dataspace': space,
354
+ 'id': requestId
355
+ }
356
+
357
+ transfer_url = transfer_url.replace("import", "status")
358
+ transfer_url = transfer_url.replace("sdmxFile", "request")
359
+
360
+ #
361
+ response = requests.post(transfer_url, verify=True, headers=headers, data=payload)
362
+ except Exception as err:
363
+ returned_result = self.__ERROR_OCCURRED + str(err)
364
+ else:
365
+ # If the response object cannot be converted to json, return an error
366
+ results_json = None
367
+ try:
368
+ results_json = json.loads(response.text)
369
+ if response.status_code == 200:
370
+ executionStatus = 'Execution status: ' + results_json['executionStatus']
371
+ if (results_json['executionStatus'] in [self.__EXECUTION_IN_PROGRESS, self.__EXECUTION_IN_QUEUED]
372
+ or self.__CONNECTION_ABORTED in results_json['executionStatus']):
373
+ returned_result = results_json['executionStatus']
374
+ else:
375
+ returned_result = executionStatus + os.linesep + 'Outcome: ' + results_json['outcome'] + os.linesep
376
+ index = 0
377
+ while index < len(results_json['logs']):
378
+ returned_result = returned_result + 'Log' + str(index) + ': ' + results_json['logs'][index]['message'] + os.linesep
379
+ index += 1
380
+ else:
381
+ returned_result = self.__ERROR_OCCURRED + 'Error code: ' + \
382
+ str(response.status_code) + ' Reason: ' + str(response.reason)
383
+ if len(response.text) > 0:
384
+ returned_result = returned_result + os.linesep + 'Text: ' + response.text
385
+ except ValueError as err:
386
+ if len(response.text) > 0:
387
+ self.init_status = self._ERROR + os.linesep + str(response.text)
388
+ else:
389
+ self.init_status = self._ERROR + os.linesep + str(err)
390
+ finally:
391
+ return returned_result
392
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dotstat_io
3
- Version: 0.2.7
3
+ Version: 1.0.0
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
@@ -11,13 +11,15 @@ Classifier: Programming Language :: Python :: 3
11
11
  Classifier: Programming Language :: Python :: 3.10
12
12
  Classifier: Programming Language :: Python :: 3.11
13
13
  Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
14
15
  Requires-Dist: chardet (>=5.1.0,<6.0.0)
15
16
  Requires-Dist: msal (>=1.22.0,<2.0.0)
16
17
  Requires-Dist: requests (>=2.29.0,<3.0.0)
17
18
  Requires-Dist: requests-kerberos (>=0.14.0,<0.15.0)
19
+ Requires-Dist: setuptools (>=75.6.0,<76.0.0)
18
20
  Description-Content-Type: text/markdown
19
21
 
20
- ### DotStat_IO:
22
+ ### DotStat_IO package:
21
23
  A generic python package which could be integrated with other end-user applications and Gitlab runner to perform basic io with .Stat Suite.
22
24
  Its role is to mask the complexities of authentication to connect to .Stat Suite.
23
25
  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.
@@ -25,19 +27,19 @@ The user needs to provide a set of parameters and the package exposes a couple o
25
27
  ### This package contains three modules:
26
28
  - ADFSAuthentication module
27
29
  - KeycloakAuthentication module
28
- - Download_upload module
30
+ - Client module
29
31
 
30
32
  ### In ADFSAuthentication module, four methods are available:
31
33
  #### 1. To initialise the module for interactive use:
32
34
  ```python
33
35
  from dotstat_io.authentication import AdfsAuthentication
34
- with AdfsAuthentication.interactive(
36
+ interactive_obj = AdfsAuthentication.interactive(
35
37
  client_id=client_id,
36
38
  sdmx_resource_id=sdmx_resource_id,
37
39
  scopes=scopes,
38
40
  authority_url=authority_url,
39
- redirect_port=redirect_port) as Interactive_obj:
40
- access_token = Interactive_obj.get_token()
41
+ redirect_port=redirect_port)
42
+
41
43
  ```
42
44
  * `client_id:` The Application (client) ID that the ADFS assigned to your app
43
45
  * `sdmx_resource_id:` The ID of the application to be accessed such as .Stat Suite
@@ -48,13 +50,13 @@ with AdfsAuthentication.interactive(
48
50
  #### 2. To initialise the module for non-interactive use using a secret:
49
51
  ```python
50
52
  from dotstat_io.authentication import AdfsAuthentication
51
- with AdfsAuthentication.nointeractive_with_secret(
53
+ noninteractive_with_secret_obj = AdfsAuthentication.noninteractive_with_secret(
52
54
  client_id=client_id,
53
55
  sdmx_resource_id=sdmx_resource_id,
54
56
  scopes=scopes,
55
57
  authority_url=authority_url,
56
- client_secret=client_secret) as Nointeractive_with_secret_obj:
57
- access_token = Nointeractive_with_secret_obj.get_token()
58
+ client_secret=client_secret)
59
+
58
60
  ```
59
61
  * `client_id:` The Application (client) ID that the ADFS assigned to your app
60
62
  * `sdmx_resource_id:` The ID of the application to be accessed such as .Stat Suite
@@ -65,11 +67,11 @@ with AdfsAuthentication.nointeractive_with_secret(
65
67
  #### 3. To initialise the module for non-interactive use using windows client authentication:
66
68
  ```python
67
69
  from dotstat_io.authentication import AdfsAuthentication
68
- with AdfsAuthentication.nointeractive_with_adfs(
70
+ noninteractive_with_adfs_obj = AdfsAuthentication.noninteractive_with_adfs(
69
71
  client_id=client_id,
70
72
  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
+ token_url=token_url)
74
+
73
75
  ```
74
76
  * `client_id:` The Application (client) ID that the ADFS assigned to your app
75
77
  * `sdmx_resource_id:` The ID of the application to be accessed such as .Stat Suite
@@ -84,12 +86,12 @@ access_token = [Authentication Object Name].get_token()
84
86
  #### 1. To initialise the module for Keycloak authentication:
85
87
  ```python
86
88
  from dotstat_io.authentication import KeycloakAuthentication
87
- with KeycloakAuthentication.nointeractive_with_secret(
89
+ noninteractive_with_keycloak_obj = KeycloakAuthentication.noninteractive_with_secret(
88
90
  user=user,
89
91
  password=password,
90
92
  token_url=token_url,
91
- proxy=proxy) as Nointeractive_with_keycloak_obj:
92
- access_token = Nointeractive_with_keycloak_obj.get_token()
93
+ proxy=proxy)
94
+
93
95
  ```
94
96
  * `user:` User name for .Stat Suite authentication
95
97
  * `password:` User name's password
@@ -101,33 +103,47 @@ with KeycloakAuthentication.nointeractive_with_secret(
101
103
  access_token = [Authentication Object Name].get_token()
102
104
  ```
103
105
 
104
- ### In Download_upload module, four methods are available:
105
- #### 1. To download a file from .Stat Suite:
106
+ ### In Client module, six methods are available:
107
+ #### 1. To initialise the module using an Authentication object type AdfsAuthentication or KeycloakAuthentication:
108
+ ```python
109
+ from dotstat_io.client import Client
110
+ client_obj = Client.init_with_authentication_obj(Authentication_obj)
111
+ ```
112
+ * `Authentication_obj:` An initialized authentication object type AdfsAuthentication or KeycloakAuthentication
113
+
114
+ #### 2. To initialise the module using an access token:
115
+ ```python
116
+ from dotstat_io.client import Client
117
+ client_obj = Client.init_with_access_token(access_token)
118
+ ```
119
+ * `access_token:` An access token to make requests on .Stat Suite services (nsiws) using the authorisation service and underlying permission rules
120
+
121
+ #### 3. To download a file from .Stat Suite:
106
122
  ```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(
123
+ from dotstat_io.client import Client
124
+ client_obj = Client.init_with_authentication_obj(Authentication_obj)
125
+ returned_result = client_obj.download_data_file(
110
126
  dotstat_url, content_format, Path(file_path))
111
127
  ```
112
128
  * `dotstat_url:` URL of data to be downloaded from .Stat Suite
113
129
  * `content_format:` Format of the downloaded content
114
130
  * `file_path:` The full path where the file will downloaded
115
131
 
116
- #### 2. To download streamed content from .Stat Suite:
132
+ #### 4. To download streamed content from .Stat Suite:
117
133
  ```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(
134
+ from dotstat_io.client import Client
135
+ client_obj = Client.init_with_authentication_obj(Authentication_obj)
136
+ returned_result = client_obj.download_data_stream(
121
137
  dotstat_url, content_format)
122
138
  ```
123
139
  * `dotstat_url:` URL of data to be downloaded from .Stat Suite
124
140
  * `content_format:` Format of the downloaded content
125
141
 
126
- #### 3. To upload a data file to .Stat Suite:
142
+ #### 5. To upload a data file to .Stat Suite:
127
143
  ```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(
144
+ from dotstat_io.client import Client
145
+ client_obj = Client.init_with_authentication_obj(Authentication_obj)
146
+ returned_result = client_obj.upload_data_file(
131
147
  transfer_url, Path(file_path), space, validationType, use_filepath)
132
148
  ```
133
149
  * `transfer_url:` URL of the transfer service
@@ -136,15 +152,15 @@ with Download_upload(adfsAuthentication_obj, access_token) as Download_upload_ob
136
152
  * `validationType:` The type of validation to use during upload. Possible values: Basic Validation (0), Advanced Validation (1)
137
153
  * `use_filepath:` Use a file path of a shared folder accessible by the .Stat Suite data upload engine (for unlimited file sizes)
138
154
 
139
- #### 4. To upload a structure to .Stat Suite:
155
+ #### 6. To upload a structure to .Stat Suite:
140
156
  ```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(
157
+ from dotstat_io.client import Client
158
+ client_obj = Client.init_with_authentication_obj(Authentication_obj)
159
+ returned_result = client_obj.upload_structure(
144
160
  transfer_url, Path(file_path))
145
161
  ```
146
162
  * `transfer_url:` URL of the transfer service
147
163
  * `file_path:` The full path of the SDMX-ML file, which will be uploaded to .Stat Suite
148
164
 
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)
165
+ ### 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)
150
166
 
@@ -0,0 +1,6 @@
1
+ dotstat_io/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ dotstat_io/authentication.py,sha256=Ax8a2koJ5kjZ4jAvnN7S9U4GZfEfsurksI8bbw6JcqI,13230
3
+ dotstat_io/client.py,sha256=Y4gIVD8uuBTQkeZ6bWiIMH8IZ723RAqMM1S14II6pxQ,16018
4
+ dotstat_io-1.0.0.dist-info/METADATA,sha256=dC41wVc4qs94XP7520QGRWqN44pDJuE0atTWnddL474,7280
5
+ dotstat_io-1.0.0.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
6
+ dotstat_io-1.0.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.9.0
2
+ Generator: poetry-core 1.9.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any