sws-api-client 1.3.0__tar.gz → 1.3.2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sws_api_client
3
- Version: 1.3.0
3
+ Version: 1.3.2
4
4
  Summary:
5
5
  Author: Matteo
6
6
  Author-email: matteo.terrinoni@fao.org
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "sws_api_client"
3
- version = "1.3.0"
3
+ version = "1.3.2"
4
4
  description = ""
5
5
  authors = ["Matteo <matteo.terrinoni@fao.org>", "Daniele <daniele.mansillo@fao.org>"]
6
6
  readme = "README.md"
@@ -0,0 +1,69 @@
1
+ """Codelists Module for SWS API.
2
+
3
+ This module provides functionality for managing codelists and retrieving
4
+ codelist information through the SWS API client.
5
+ """
6
+
7
+ import logging
8
+ from time import sleep
9
+ import os
10
+ from pydantic import BaseModel, Extra
11
+ from typing import List, Optional, Dict
12
+ from sws_api_client.generic_models import Code, Multilanguage
13
+ from sws_api_client.sws_api_client import SwsApiClient
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ class CodelistModel(BaseModel, extra="allow"):
18
+ """Model representing a codelist's metadata.
19
+
20
+ Attributes:
21
+ id (str): Unique identifier of the codelist
22
+ label (Multilanguage): Multilanguage labels for the codelist
23
+ type (str): Type of the codelist
24
+ """
25
+ id: str
26
+ label: Multilanguage
27
+ type: str
28
+
29
+ class Codelist(BaseModel, extra="allow"):
30
+ """Model representing a complete codelist with its codes.
31
+
32
+ Attributes:
33
+ model (CodelistModel): Metadata of the codelist
34
+ codes (List[Code]): List of codes belonging to this codelist
35
+ """
36
+ model: CodelistModel
37
+ codes: List[Code]
38
+
39
+ class Codelists:
40
+ """Class for managing codelist operations through the SWS API.
41
+
42
+ This class provides methods for retrieving codelist information
43
+ and managing codelist data.
44
+
45
+ Args:
46
+ sws_client (SwsApiClient): An instance of the SWS API client
47
+ """
48
+
49
+ def __init__(self, sws_client: SwsApiClient) -> None:
50
+ """Initialize the Codelists manager with SWS client."""
51
+ self.sws_client = sws_client
52
+
53
+ def get_codelist(self, codelist_id: str, nocache: bool = False) -> Codelist:
54
+ """Retrieve a codelist by its ID.
55
+
56
+ Args:
57
+ codelist_id (str): The identifier of the codelist
58
+ nocache (bool, optional): Whether to bypass cache. Defaults to False.
59
+
60
+ Returns:
61
+ Codelist: The requested codelist with its codes
62
+
63
+ Raises:
64
+ HTTPError: If the codelist cannot be retrieved
65
+ """
66
+ url = f"/admin/reference/codelist/{codelist_id}?nocache={'true' if nocache else 'false'}"
67
+
68
+ response = self.sws_client.discoverable.get('is_api', url)
69
+ return Codelist(**response)
@@ -1,3 +1,9 @@
1
+ """Datasets Module for SWS API.
2
+
3
+ This module provides functionality for managing datasets, including creation,
4
+ cloning, data import/export, and dimension management through the SWS API client.
5
+ """
6
+
1
7
  import logging
2
8
  from time import sleep
3
9
  import os
@@ -14,6 +20,16 @@ from concurrent.futures import ThreadPoolExecutor, as_completed
14
20
  logger = logging.getLogger(__name__)
15
21
 
16
22
  class Lifecycle(BaseModel):
23
+ """Model representing the lifecycle state of a dataset.
24
+
25
+ Attributes:
26
+ state (str): Current state of the dataset
27
+ type (str): Type of lifecycle
28
+ previousState (Optional[str]): Previous state if applicable
29
+ created (int): Creation timestamp
30
+ lastModified (int): Last modification timestamp
31
+ lastModifiedBy (str): User who last modified the dataset
32
+ """
17
33
  state: str
18
34
  type: str
19
35
  previousState: Optional[str]
@@ -22,14 +38,40 @@ class Lifecycle(BaseModel):
22
38
  lastModifiedBy: str
23
39
 
24
40
  class Domain(BaseModel):
41
+ """Model representing a domain.
42
+
43
+ Attributes:
44
+ id (str): Domain identifier
45
+ label (Multilanguage): Multilanguage labels
46
+ description (Dict): Domain description in multiple languages
47
+ """
25
48
  id: str
26
49
  label: Multilanguage
27
50
  description: Dict
28
51
 
29
52
  class Binding(BaseModel):
53
+ """Model representing column binding information.
54
+
55
+ Attributes:
56
+ joinColumn (str): Column used for joining
57
+ """
30
58
  joinColumn: str
31
59
 
32
60
  class Dimension(BaseModel):
61
+ """Model representing a dataset dimension.
62
+
63
+ Attributes:
64
+ id (str): Dimension identifier
65
+ label (Multilanguage): Multilanguage labels
66
+ description (Dict): Dimension description
67
+ sdmxName (str): SDMX name
68
+ codelist (str): Associated codelist
69
+ roots (List[str]): Root codes
70
+ binding (Binding): Column binding information
71
+ checkValidityPeriod (bool): Whether to check validity period
72
+ formulas (List): Associated formulas
73
+ type (str): Dimension type
74
+ """
33
75
  id: str
34
76
  label: Multilanguage
35
77
  description: Dict
@@ -98,13 +140,29 @@ class MappedCode(BaseModel):
98
140
  code: Code
99
141
  include: bool
100
142
  class Datasets:
143
+ """Class for managing dataset operations through the SWS API.
144
+
145
+ This class provides methods for creating, cloning, importing data,
146
+ and managing dataset dimensions.
147
+
148
+ Args:
149
+ sws_client (SwsApiClient): An instance of the SWS API client
150
+ """
101
151
 
102
152
  def __init__(self, sws_client: SwsApiClient) -> None:
153
+ """Initialize the Datasets manager with SWS client."""
103
154
  self.sws_client = sws_client
104
155
  self.codelists = Codelists(sws_client)
105
156
 
106
157
  def get_dataset_export_details(self, dataset_id: str) -> dict:
158
+ """Get export details for a dataset.
159
+
160
+ Args:
161
+ dataset_id (str): Dataset identifier
107
162
 
163
+ Returns:
164
+ dict: Dataset export information
165
+ """
108
166
  url = f"/dataset/{dataset_id}/info"
109
167
  params = {"extended": "true"}
110
168
 
@@ -113,14 +171,28 @@ class Datasets:
113
171
  return response
114
172
 
115
173
  def get_dataset_info(self, dataset_id: str) -> DataModel:
174
+ """Retrieve dataset information.
116
175
 
176
+ Args:
177
+ dataset_id (str): Dataset identifier
178
+
179
+ Returns:
180
+ DataModel: Complete dataset information model
181
+ """
117
182
  url = f"/admin/dataset/{dataset_id}"
118
183
 
119
184
  response = self.sws_client.discoverable.get('is_api', url)
120
185
  return DataModel(**response)
121
186
 
122
187
  def create_dataset(self, dataset: Dataset) -> DataModel:
188
+ """Create a new dataset.
189
+
190
+ Args:
191
+ dataset (Dataset): Dataset configuration
123
192
 
193
+ Returns:
194
+ DataModel: Created dataset model
195
+ """
124
196
  url = "/admin/dataset"
125
197
 
126
198
  response = self.sws_client.discoverable.post('is_api', url, data=dataset.model_dump())
@@ -128,14 +200,29 @@ class Datasets:
128
200
  return response
129
201
 
130
202
  def clone_dataset(self, dataset_id: str, new_id: str) -> DataModel:
203
+ """Clone an existing dataset with a new identifier.
204
+
205
+ Args:
206
+ dataset_id (str): Source dataset identifier
207
+ new_id (str): New dataset identifier
131
208
 
209
+ Returns:
210
+ DataModel: Cloned dataset model
211
+ """
132
212
  dataset = self.get_dataset_info(dataset_id)
133
213
  dataset.dataset.id = new_id
134
214
  new_dataset = self.create_dataset(dataset.dataset)
135
215
  return new_dataset
136
216
 
137
217
  def get_job_status(self, jobId: str) -> dict:
218
+ """Check status of a job.
138
219
 
220
+ Args:
221
+ jobId (str): Job identifier
222
+
223
+ Returns:
224
+ dict: Job status information with result and success flags
225
+ """
139
226
  url = f"/job/status/{jobId}"
140
227
 
141
228
  response = self.sws_client.discoverable.get('is_api', url)
@@ -145,10 +232,29 @@ class Datasets:
145
232
  return dict(result=result, success=success)
146
233
 
147
234
  def import_data(self, dataset_id: str, file_path, sessionId = None, zip=False ) -> bool:
235
+ """Import data into a dataset.
236
+
237
+ Args:
238
+ dataset_id (str): Target dataset identifier
239
+ file_path: Path to data file
240
+ sessionId (Optional[int]): Session identifier
241
+ zip (bool): Whether to zip the file
242
+
243
+ Returns:
244
+ bool: True if import successful
245
+ """
148
246
  return self.import_data_chunk(dataset_id, file_path, sessionId)
149
247
 
150
248
 
151
249
  def get_dataset_dimension_codes(self, dataset_id: str) -> Dict[str, List[Code]]:
250
+ """Get codes for all dimensions in a dataset.
251
+
252
+ Args:
253
+ dataset_id (str): Dataset identifier
254
+
255
+ Returns:
256
+ Dict[str, List[Code]]: Dictionary mapping dimension IDs to their codes
257
+ """
152
258
  dataset_info = self.get_dataset_info(dataset_id)
153
259
  dimensions = dataset_info.dataset.dimensions.dimensions
154
260
 
@@ -188,7 +294,19 @@ class Datasets:
188
294
  return dimensions_codes
189
295
 
190
296
  def import_data_chunk(self, dataset_id: str, file_path: str, sessionId: Optional[int] = None) -> dict:
191
- """Helper function to import a single data chunk, zipping files over 10MB automatically."""
297
+ """Import a single chunk of data, automatically handling file compression.
298
+
299
+ Args:
300
+ dataset_id (str): Target dataset identifier
301
+ file_path (str): Path to data file
302
+ sessionId (Optional[int]): Session identifier
303
+
304
+ Returns:
305
+ dict: Import job result
306
+
307
+ Raises:
308
+ OSError: If file operations fail
309
+ """
192
310
  # Check file size and zip if greater than 10MB
193
311
  zip_file_path = None
194
312
  if os.path.getsize(file_path) > 10 * 1024 * 1024: # 10MB in bytes
@@ -232,7 +350,14 @@ class Datasets:
232
350
  return self.get_job_result(job_id)
233
351
 
234
352
  def get_job_result(self, job_id: str) -> bool:
235
- """Check job status until it's completed."""
353
+ """Poll for job completion and return result.
354
+
355
+ Args:
356
+ job_id (str): Job identifier
357
+
358
+ Returns:
359
+ bool: True if job completed successfully
360
+ """
236
361
  while True:
237
362
  logger.debug(f"Checking job status for job ID {job_id}")
238
363
  job_status = self.get_job_status(job_id)
@@ -241,7 +366,19 @@ class Datasets:
241
366
  sleep(5)
242
367
 
243
368
  def chunk_csv_file(self, file_path: str, chunk_size: int) -> List[str]:
244
- """Splits the CSV file into smaller chunks while preserving leading zeros for all columns except 'value'."""
369
+ """Split CSV file into smaller chunks while preserving data format.
370
+
371
+ Args:
372
+ file_path (str): Path to CSV file
373
+ chunk_size (int): Number of rows per chunk
374
+
375
+ Returns:
376
+ List[str]: Paths to chunk files
377
+
378
+ Raises:
379
+ OSError: If file operations fail
380
+ pd.errors.EmptyDataError: If CSV file is empty
381
+ """
245
382
  temp_files = []
246
383
 
247
384
  # Load the data in chunks and process each chunk
@@ -257,8 +394,19 @@ class Datasets:
257
394
  return temp_files
258
395
 
259
396
  def import_data_concurrent(self, dataset_id: str, file_path: str, sessionId: Optional[int] = None, chunk_size: int = 10000, max_workers: int = 5) -> None:
260
- """Splits the CSV and imports chunks concurrently with limited workers, showing progress in the logs."""
261
-
397
+ """Import data in parallel using multiple threads.
398
+
399
+ Args:
400
+ dataset_id (str): Target dataset identifier
401
+ file_path (str): Path to data file
402
+ sessionId (Optional[int]): Session identifier
403
+ chunk_size (int): Rows per chunk
404
+ max_workers (int): Maximum number of concurrent imports
405
+
406
+ Raises:
407
+ OSError: If file operations fail
408
+ Exception: If import operations fail
409
+ """
262
410
  # Step 1: Split the file into chunks
263
411
  chunk_files = self.chunk_csv_file(file_path, chunk_size)
264
412
  total_chunks = len(chunk_files) # Total number of chunks for progress tracking
@@ -297,4 +445,3 @@ class Datasets:
297
445
  logger.info("Importing chunks: completed")
298
446
 
299
447
 
300
-
@@ -0,0 +1,162 @@
1
+ """Datatables Module for SWS API.
2
+
3
+ This module provides functionality for managing datatables, including retrieving
4
+ information, exporting data, and handling CSV paths through the SWS API client.
5
+ """
6
+
7
+ import logging
8
+ from pydantic import BaseModel, Field, HttpUrl
9
+ from typing import List, Optional, Dict, Any
10
+ from sws_api_client.sws_api_client import SwsApiClient
11
+ logger = logging.getLogger(__name__)
12
+ from datetime import datetime, time
13
+ from time import sleep
14
+
15
+ class LabelModel(BaseModel):
16
+ """Model for multilingual labels.
17
+
18
+ Attributes:
19
+ en (str): English label text
20
+ """
21
+ en: str
22
+
23
+ class DescriptionModel(BaseModel):
24
+ """Model for multilingual descriptions.
25
+
26
+ Attributes:
27
+ en (str): English description text
28
+ """
29
+ en: str
30
+
31
+ class ColumnModel(BaseModel):
32
+ """Model representing a datatable column.
33
+
34
+ Attributes:
35
+ id (str): Column identifier
36
+ label (LabelModel): Column labels in different languages
37
+ description (DescriptionModel): Column descriptions in different languages
38
+ type (str): Data type of the column
39
+ constraints (List[Any]): List of column constraints
40
+ defaultValue (Optional[Any]): Default value for the column
41
+ facets (List[Any]): List of column facets
42
+ """
43
+ id: str
44
+ label: LabelModel
45
+ description: DescriptionModel
46
+ type: str
47
+ constraints: List[Any]
48
+ defaultValue: Optional[Any]
49
+ facets: List[Any]
50
+
51
+ class DataModel(BaseModel):
52
+ """Model representing datatable data status.
53
+
54
+ Attributes:
55
+ url (Optional[str]): URL to access the data
56
+ available (bool): Whether the data is available
57
+ uptodate (bool): Whether the data is up to date
58
+ """
59
+ url: Optional[str]
60
+ available: bool
61
+ uptodate: bool
62
+
63
+ class DatatableModel(BaseModel):
64
+ """Model representing a complete datatable.
65
+
66
+ Attributes:
67
+ id (str): Datatable identifier
68
+ name (str): Name of the datatable
69
+ label (LabelModel): Labels in different languages
70
+ description (DescriptionModel): Descriptions in different languages
71
+ schema_name (str): Schema name
72
+ domains (List[str]): List of associated domains
73
+ plugins (List[Any]): List of associated plugins
74
+ columns (List[ColumnModel]): List of table columns
75
+ facets (List[Any]): List of table facets
76
+ last_update (datetime): Last update timestamp
77
+ data (DataModel): Data status information
78
+ """
79
+ id: str
80
+ name: str
81
+ label: LabelModel
82
+ description: DescriptionModel
83
+ schema_name: str = Field(..., alias="schema")
84
+ domains: List[str]
85
+ plugins: List[Any]
86
+ columns: List[ColumnModel]
87
+ facets: List[Any]
88
+ last_update: datetime
89
+ data: DataModel
90
+
91
+ class Datatables:
92
+ """Class for managing datatable operations through the SWS API.
93
+
94
+ This class provides methods for retrieving datatable information
95
+ and managing data exports.
96
+
97
+ Args:
98
+ sws_client (SwsApiClient): An instance of the SWS API client
99
+ """
100
+
101
+ def __init__(self, sws_client: SwsApiClient) -> None:
102
+ """Initialize the Datatables manager with SWS client."""
103
+ self.sws_client = sws_client
104
+
105
+ def get_datatable_info(self, datatable_id: str) -> DatatableModel:
106
+ """Retrieve information about a specific datatable.
107
+
108
+ Args:
109
+ datatable_id (str): The identifier of the datatable
110
+
111
+ Returns:
112
+ DatatableModel: Datatable information if found, None otherwise
113
+ """
114
+ url = f"/datatables/{datatable_id}"
115
+
116
+ response = self.sws_client.discoverable.get('datatable_api', url)
117
+ if(response.get('id') is not None):
118
+ return DatatableModel(**response)
119
+ else:
120
+ return None
121
+
122
+ def invoke_export_datatable(self, datatable_id: str) -> Dict:
123
+ """Trigger an export operation for a datatable.
124
+
125
+ Args:
126
+ datatable_id (str): The identifier of the datatable
127
+
128
+ Returns:
129
+ Dict: Response containing export operation status
130
+ """
131
+ url = f"/datatables/{datatable_id}/invoke_export_2_s3"
132
+
133
+ response = self.sws_client.discoverable.post('datatable_api', url)
134
+
135
+ return response
136
+
137
+ def get_datatable_csv_path(self, datatable, timeout=60*15, interval=2) -> HttpUrl:
138
+ """Get the CSV file path for a datatable, waiting for export if necessary.
139
+
140
+ Args:
141
+ datatable: The datatable identifier
142
+ timeout (int): Maximum time to wait for export in seconds (default: 15 minutes)
143
+ interval (int): Time between checks in seconds (default: 2)
144
+
145
+ Returns:
146
+ HttpUrl: URL to the CSV file
147
+
148
+ Raises:
149
+ TimeoutError: If export doesn't complete within timeout period
150
+ """
151
+ info = self.get_datatable_info(datatable)
152
+ if info.data.available and info.data.uptodate:
153
+ return info.data.url
154
+ else:
155
+ self.invoke_export_datatable(datatable)
156
+ # get the updated info every interval seconds until the data are available to a maximum of timeout seconds
157
+ start = datetime.now()
158
+ while (datetime.now() - start).seconds < timeout:
159
+ info = self.get_datatable_info(datatable)
160
+ if info.data.available:
161
+ return info.data.url
162
+ sleep(interval)