datasourcelib 0.1.5__py3-none-any.whl → 0.1.6__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.
@@ -10,6 +10,7 @@ from ..datasources.sql_source import SQLDataSource
10
10
  from ..datasources.azure_devops_source import AzureDevOpsSource
11
11
  from ..datasources.sharepoint_source import SharePointSource
12
12
  from ..datasources.blob_source import BlobStorageSource
13
+ from ..datasources.dataverse_source import DataverseSource
13
14
 
14
15
  # concrete strategies
15
16
  from datasourcelib.strategies.full_load import FullLoadStrategy
@@ -35,7 +36,8 @@ class SyncManager:
35
36
  DataSourceType.SQL: SQLDataSource,
36
37
  DataSourceType.AZURE_DEVOPS: AzureDevOpsSource,
37
38
  DataSourceType.SHAREPOINT: SharePointSource,
38
- DataSourceType.BLOB_STORAGE: BlobStorageSource
39
+ DataSourceType.BLOB_STORAGE: BlobStorageSource,
40
+ DataSourceType.Dataverse: DataverseSource
39
41
  }
40
42
 
41
43
  def execute_sync(self, sync_type: SyncType,
@@ -4,4 +4,5 @@ class DataSourceType(str, Enum):
4
4
  SQL = "sql"
5
5
  AZURE_DEVOPS = "azure_devops"
6
6
  SHAREPOINT = "sharepoint"
7
- BLOB_STORAGE = "blob_storage"
7
+ BLOB_STORAGE = "blob_storage"
8
+ Dataverse = "Dataverse"
@@ -0,0 +1,291 @@
1
+ from typing import Any, Dict, List, Optional, Tuple
2
+ from datasourcelib.datasources.datasource_base import DataSourceBase
3
+ from datasourcelib.utils.logger import get_logger
4
+ from datasourcelib.utils.validators import require_keys
5
+ import pyodbc
6
+ import time
7
+ import pandas as pd
8
+
9
+ # optional requests import (webapi mode)
10
+ try:
11
+ import requests # type: ignore
12
+ except Exception:
13
+ requests = None # lazy import
14
+
15
+ logger = get_logger(__name__)
16
+
17
+ class DataverseSource(DataSourceBase):
18
+
19
+ def __init__(self, config: Dict[str, Any]):
20
+ super().__init__(config)
21
+ self._conn = None
22
+ self._mode = (self.config.get("dv_mode") or "tds").lower() # "tds" or "webapi"
23
+ self._access_token: Optional[str] = None
24
+ self._headers: Dict[str, str] = {}
25
+ self._max_retries = int(self.config.get("dv_max_retries", 3))
26
+
27
+ def validate_config(self) -> bool:
28
+ """
29
+ Validate required keys depending on selected dv_mode.
30
+ - tds: requires either 'tds_connection_string' OR ('dataverse_server' and 'dataverse_database')
31
+ - webapi: requires 'webapi_url','client_id','client_secret','tenant_id' (or 'resource')
32
+ """
33
+ try:
34
+ if self._mode == "webapi":
35
+ require_keys(self.config, ["dv_webapi_url", "dv_webapi_client_id", "dv_webapi_client_secret", "dv_webapi_tenant_id"])
36
+ else:
37
+ # TDS mode (ODBC)
38
+ if "dv_tds_connection_string" in self.config:
39
+ return True
40
+ # otherwise require components
41
+ require_keys(self.config, ["dv_tds_server", "dv_tds_database"])
42
+ # if not using integrated auth require creds
43
+ if not bool(self.config.get("dv_tds_windows_auth", False)):
44
+ require_keys(self.config, ["dv_tds_username", "dv_tds_password"])
45
+ return True
46
+ except Exception as ex:
47
+ logger.error("DataverseSource.validate_config failed: %s", ex)
48
+ return False
49
+
50
+ # -------------------------
51
+ # Connection helpers
52
+ # -------------------------
53
+ def _get_available_driver(self) -> str:
54
+ """Return first suitable ODBC driver for SQL/Dataverse TDS access."""
55
+ preferred_drivers = [
56
+ "ODBC Driver 18 for SQL Server",
57
+ "ODBC Driver 17 for SQL Server",
58
+ "SQL Server Native Client 11.0",
59
+ "SQL Server"
60
+ ]
61
+ try:
62
+ drivers = pyodbc.drivers()
63
+ logger.info("Available ODBC drivers: %s", drivers)
64
+
65
+ for d in preferred_drivers:
66
+ if d in drivers:
67
+ logger.info("Using ODBC driver: %s", d)
68
+ return d
69
+
70
+ # fallback to first available
71
+ if drivers:
72
+ logger.warning("No preferred driver found. Using: %s", drivers[0])
73
+ return drivers[0]
74
+ raise RuntimeError("No ODBC drivers available")
75
+ except Exception as ex:
76
+ logger.error("DataverseSource._get_available_driver failed: %s", ex)
77
+ raise
78
+
79
+ def _build_tds_conn_str(self) -> str:
80
+ """Build valid connection string with proper parameter names."""
81
+ if "dv_tds_connection_string" in self.config:
82
+ return self.config["dv_tds_connection_string"]
83
+
84
+ driver = self._get_available_driver()
85
+ # Fix: use correct config key names (dv_tds_server, not dv_tds_dataverse_server)
86
+ server = self.config.get("dv_tds_server", "").strip()
87
+ database = self.config.get("dv_tds_database", "").strip()
88
+
89
+ if not server:
90
+ raise ValueError("dv_tds_server are required")
91
+
92
+ logger.info("Building TDS connection (driver=%s, server=%s, database=%s)", driver, server, database)
93
+
94
+ # Use curly braces for driver name (handles spaces in driver names)
95
+ parts = [f"DRIVER={{{driver}}}"]
96
+ parts.append(f"Server={server}")
97
+ parts.append(f"Database={database}")
98
+ password = None
99
+ if bool(self.config.get("dv_tds_windows_auth", False)):
100
+ parts.append("Trusted_Connection=yes")
101
+ logger.info("Using Windows authentication")
102
+ else:
103
+ username = self.config.get("dv_tds_username", "").strip()
104
+ password = self.config.get("dv_tds_password", "").strip()
105
+
106
+ if not username or not password:
107
+ raise ValueError("dv_tds_username and dv_tds_password required when Windows auth disabled")
108
+
109
+ parts.append(f"UID={username}")
110
+ parts.append(f"PWD={password}")
111
+ parts.append("Authentication=ActiveDirectoryInteractive")
112
+ # Encryption settings
113
+ if not bool(self.config.get("dv_tds_is_onprem", False)):
114
+ parts.append("Encrypt=yes")
115
+ parts.append("TrustServerCertificate=no")
116
+ else:
117
+ parts.append("Encrypt=optional")
118
+ parts.append("TrustServerCertificate=yes")
119
+
120
+ conn_str = ";".join(parts)
121
+ logger.debug("Connection string: %s", conn_str.replace(password or "", "***") if password else conn_str)
122
+ return conn_str
123
+
124
+ def _obtain_webapi_token(self) -> Tuple[str, Dict[str, str]]:
125
+ """
126
+ Acquire OAuth2 token using client credentials flow.
127
+ Returns (access_token, headers)
128
+ Config expected keys: tenant_id, client_id, client_secret, optional resource
129
+ """
130
+ if requests is None:
131
+ raise RuntimeError("requests package required for Dataverse Web API mode")
132
+ tenant = self.config["dv_webapi_tenant_id"]
133
+ client_id = self.config["dv_webapi_client_id"]
134
+ client_secret = self.config["dv_webapi_client_secret"]
135
+ # resource or scope: prefer explicit resource, else fallback to webapi_url host
136
+ resource = self.config.get("dv_webapi_resource")
137
+ if not resource:
138
+ # infer resource from webapi_url e.g. https://<org>.crm.dynamics.com
139
+ webapi_url = self.config["dv_webapi_url"].rstrip("/")
140
+ resource = webapi_url.split("://")[-1]
141
+ resource = f"https://{resource}" # as resource
142
+ token_url = f"https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token"
143
+ data = {
144
+ "grant_type": "client_credentials",
145
+ "client_id": client_id,
146
+ "client_secret": client_secret,
147
+ "scope": f"{resource}/.default"
148
+ }
149
+ resp = requests.post(token_url, data=data, timeout=30)
150
+ resp.raise_for_status()
151
+ j = resp.json()
152
+ token = j.get("access_token")
153
+ if not token:
154
+ raise RuntimeError("Failed to obtain access token for Dataverse webapi")
155
+ headers = {"Authorization": f"Bearer {token}", "Accept": "application/json", "OData-MaxVersion": "4.0", "OData-Version": "4.0"}
156
+ return token, headers
157
+
158
+ # -------------------------
159
+ # Public connection API
160
+ # -------------------------
161
+ def connect(self) -> bool:
162
+ try:
163
+ if self._mode == "webapi":
164
+ token, headers = self._obtain_webapi_token()
165
+ self._access_token = token
166
+ self._headers = headers
167
+ self._connected = True
168
+ logger.info("DataverseSource connected (webapi mode) to %s", self.config.get("dv_webapi_url"))
169
+ return True
170
+ # else TDS mode
171
+ conn_str = self._build_tds_conn_str()
172
+ self._conn = pyodbc.connect(conn_str, timeout=int(self.config.get("dv_tds_timeout", 30)))
173
+ self._connected = True
174
+ logger.info("DataverseSource connected (dv_tds mode) to %s/%s", self.config.get("dv_server"), self.config.get("dv_database"))
175
+ return True
176
+ except pyodbc.Error as ex:
177
+ logger.error("DataverseSource.connect failed - ODBC Error: %s", ex)
178
+ self._connected = False
179
+ return False
180
+ except requests.RequestException as ex:
181
+ logger.error("DataverseSource.connect failed - HTTP Error: %s", ex)
182
+ self._connected = False
183
+ return False
184
+ except Exception as ex:
185
+ logger.exception("DataverseSource.connect failed")
186
+ self._connected = False
187
+ return False
188
+
189
+ def disconnect(self) -> None:
190
+ try:
191
+ if self._conn:
192
+ try:
193
+ self._conn.close()
194
+ except Exception:
195
+ pass
196
+ self._conn = None
197
+ self._access_token = None
198
+ self._headers = {}
199
+ finally:
200
+ self._connected = False
201
+ logger.info("DataverseSource disconnected")
202
+
203
+ # -------------------------
204
+ # Data fetch
205
+ # -------------------------
206
+ def fetch_data(self, query: Optional[str] = None, **kwargs) -> List[Dict[str, Any]]:
207
+ """
208
+ Fetch rows from Dataverse.
209
+ - TDS mode: executes SQL query (config key 'tds_query' or provided 'query')
210
+ - WebAPI mode: calls Dataverse Web API path fragment (e.g. 'accounts?$select=name') or uses 'entity_set' + query params
211
+ Returns list[dict].
212
+ """
213
+ attempt = 0
214
+ while attempt < self._max_retries:
215
+ try:
216
+ if not getattr(self, "_connected", False):
217
+ ok = self.connect()
218
+ if not ok:
219
+ raise RuntimeError("DataverseSource: cannot connect")
220
+
221
+ if self._mode == "webapi":
222
+ if requests is None:
223
+ raise RuntimeError("requests package required for webapi mode")
224
+ webapi_url = self.config["dv_webapi_url"].rstrip("/")
225
+ # if query provided, treat it as path fragment; else use entity_set from config
226
+ path_fragment = query or self.config.get("dv_webapi_entity_set")
227
+ if not path_fragment:
228
+ raise ValueError("DataverseSource.fetch_data requires a webapi 'query' or 'entity_set' in config")
229
+ url = f"{webapi_url}/api/data/v9.1/{path_fragment.lstrip('/')}"
230
+ params = kwargs.get("params")
231
+ resp = requests.get(url, headers=self._headers, params=params, timeout=60)
232
+ resp.raise_for_status()
233
+ j = resp.json()
234
+ items: Any = []
235
+ # Dataverse OData responses typically use 'value' for collections
236
+ if isinstance(j, dict) and "value" in j:
237
+ items = j["value"]
238
+ # otherwise return the raw json wrapped in a list or as-is
239
+ elif isinstance(j, list):
240
+ items= j
241
+ else:
242
+ items= [j]
243
+
244
+ df = pd.DataFrame(items)
245
+ # filter columns if configured
246
+ keep = self.config.get("dv_webapi_columns_to_keep")
247
+ if isinstance(keep, list) and keep:
248
+ cols_to_keep = [c for c in df.columns if c in keep]
249
+ else:
250
+ # exclude SharePoint metadata columns (start with '__' or prefixed with '@')
251
+ cols_to_keep = [c for c in df.columns if not str(c).startswith("__") and not str(c).startswith("@")]
252
+ df = df[cols_to_keep]
253
+ results = df.to_dict("records")
254
+ return results
255
+ # else TDS mode
256
+ sql = query or self.config.get("dv_tds_query") or self.config.get("dv_sql_query")
257
+ if not sql:
258
+ raise ValueError("DataverseSource.fetch_data requires a SQL query (tds mode)")
259
+
260
+ cur = self._conn.cursor()
261
+ try:
262
+ cur.execute(sql)
263
+ cols = [c[0] for c in (cur.description or [])]
264
+ rows = cur.fetchall()
265
+ results: List[Dict[str, Any]] = []
266
+ for r in rows:
267
+ results.append({cols[i]: r[i] for i in range(len(cols))})
268
+ return results
269
+ finally:
270
+ try:
271
+ cur.close()
272
+ except Exception:
273
+ pass
274
+
275
+ except Exception as ex:
276
+ attempt += 1
277
+ logger.warning("DataverseSource.fetch_data attempt %d/%d failed: %s", attempt, self._max_retries, ex)
278
+ # transient retry for network/connection errors
279
+ if attempt >= self._max_retries:
280
+ logger.exception("DataverseSource.fetch_data final failure")
281
+ raise
282
+ # backoff
283
+ time.sleep(min(2 ** attempt, 10))
284
+ # try reconnect for next attempt
285
+ try:
286
+ self.disconnect()
287
+ except Exception:
288
+ pass
289
+
290
+ # unreachable; defensive
291
+ return []
@@ -0,0 +1,159 @@
1
+ from typing import Any, Dict, List, Optional
2
+ from datasourcelib.datasources.datasource_base import DataSourceBase
3
+ from datasourcelib.utils.logger import get_logger
4
+ from datasourcelib.utils.validators import require_keys
5
+ import os
6
+ import pyodbc
7
+
8
+
9
+ logger = get_logger(__name__)
10
+
11
+ class SQLDataSource(DataSourceBase):
12
+
13
+ def __init__(self, config: Dict[str, Any]):
14
+ super().__init__(config)
15
+ self._conn = None
16
+ self._is_sqlite = False
17
+
18
+ def validate_config(self) -> bool:
19
+ """
20
+ Validate config. If sql_windows_auth is True then sql_username/sql_password are optional.
21
+ Otherwise require sql_username and sql_password.
22
+ """
23
+ try:
24
+ # Always require server/database at minimum
25
+ require_keys(self.config, ["sql_server", "sql_database"])
26
+ # If not using Windows authentication, require credentials
27
+ if not bool(self.config.get("sql_windows_auth", False)):
28
+ require_keys(self.config, ["sql_username", "sql_password"])
29
+ return True
30
+ except Exception as ex:
31
+ logger.error("SQLDataSource.validate_config: %s", ex)
32
+ return False
33
+
34
+ def connect(self) -> bool:
35
+ try:
36
+ sql_server = self.config.get("sql_server", "")
37
+ sql_database = self.config.get("sql_database", "")
38
+ sql_is_onprem = self.config.get("sql_is_onprem", False)
39
+
40
+ # Determine auth mode: sql_windows_auth (Trusted Connection) overrides username/password
41
+ sql_windows_auth = bool(self.config.get("sql_windows_auth", False))
42
+
43
+ # Get available driver
44
+ sql_driver = self._get_available_driver()
45
+
46
+ # Build connection string
47
+ conn_params = [
48
+ f'DRIVER={sql_driver}',
49
+ f'SERVER={sql_server}',
50
+ f'DATABASE={sql_database}',
51
+ ]
52
+
53
+ if sql_windows_auth:
54
+ # Use integrated Windows authentication (Trusted Connection)
55
+ # This will use the current process credentials / kerberos ticket.
56
+ conn_params.append('Trusted_Connection=yes')
57
+ logger.info("SQLDataSource using Windows (integrated) authentication")
58
+ else:
59
+ sql_username = self.config.get("sql_username", "")
60
+ sql_password = self.config.get("sql_password", "")
61
+ conn_params.extend([f'UID={sql_username}', f'PWD={sql_password}'])
62
+
63
+ # Add encryption settings based on environment
64
+ if not sql_is_onprem:
65
+ conn_params.extend([
66
+ 'Encrypt=yes',
67
+ 'TrustServerCertificate=no'
68
+ ])
69
+ else:
70
+ conn_params.extend([
71
+ 'Encrypt=optional',
72
+ 'TrustServerCertificate=yes'
73
+ ])
74
+
75
+ conn_str = ';'.join(conn_params)
76
+
77
+ # Attempt connection with timeout
78
+ self._conn = pyodbc.connect(conn_str, timeout=30)
79
+ self._connected = True
80
+ logger.info("SQLDataSource connected to %s using driver %s (sql_windows_auth=%s)", sql_server, sql_driver, sql_windows_auth)
81
+ return True
82
+
83
+ except pyodbc.Error as ex:
84
+ logger.error("SQLDataSource.connect failed - ODBC Error: %s", ex)
85
+ self._connected = False
86
+ return False
87
+ except Exception as ex:
88
+ logger.error("SQLDataSource.connect failed - Unexpected Error: %s", ex)
89
+ self._connected = False
90
+ return False
91
+
92
+ def disconnect(self) -> None:
93
+ try:
94
+ if self._conn:
95
+ self._conn.close()
96
+ finally:
97
+ self._conn = None
98
+ self._connected = False
99
+ logger.info("SQLDataSource disconnected")
100
+
101
+ def fetch_data(self, query: Optional[str] = None, **kwargs) -> List[Dict[str, Any]]:
102
+ max_retries = 3
103
+ retry_count = 0
104
+
105
+ while retry_count < max_retries:
106
+ try:
107
+ if not self._connected:
108
+ ok = self.connect()
109
+ if not ok:
110
+ raise RuntimeError("SQLDataSource: not connected and cannot connect")
111
+
112
+ query = self.config.get("sql_query")
113
+ if not query:
114
+ raise ValueError("SQLDataSource.fetch_data requires a query")
115
+
116
+ cur = self._conn.cursor()
117
+ try:
118
+ cur.execute(query)
119
+ cols = [d[0] if hasattr(d, "__len__") else d[0] for d in (cur.description or [])]
120
+ rows = cur.fetchall()
121
+ results: List[Dict[str, Any]] = []
122
+ for r in rows:
123
+ results.append({cols[i]: r[i] for i in range(len(cols))})
124
+ return results
125
+ finally:
126
+ try:
127
+ cur.close()
128
+ except Exception:
129
+ pass
130
+
131
+ except pyodbc.OperationalError as ex:
132
+ # Handle connection lost
133
+ retry_count += 1
134
+ logger.warning("Connection lost, attempt %d of %d: %s", retry_count, max_retries, ex)
135
+ self.disconnect()
136
+ if retry_count >= max_retries:
137
+ raise
138
+ except Exception as ex:
139
+ logger.error("Query execution failed: %s", ex)
140
+ raise
141
+
142
+ def _get_available_driver(self) -> str:
143
+ """Get first available SQL Server driver from preferred list."""
144
+ preferred_drivers = [
145
+ 'ODBC Driver 18 for SQL Server',
146
+ 'ODBC Driver 17 for SQL Server',
147
+ 'SQL Server Native Client 11.0',
148
+ 'SQL Server'
149
+ ]
150
+
151
+ try:
152
+ available_drivers = pyodbc.drivers()
153
+ for driver in preferred_drivers:
154
+ if driver in available_drivers:
155
+ return driver
156
+ raise RuntimeError(f"No suitable SQL Server driver found. Available drivers: {available_drivers}")
157
+ except Exception as ex:
158
+ logger.error("Failed to get SQL drivers: %s", ex)
159
+ raise
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datasourcelib
3
- Version: 0.1.5
3
+ Version: 0.1.6
4
4
  Summary: Data source sync strategies for vector DBs
5
5
  Home-page: https://github.com/akashmaurya0217/datasourcelib
6
6
  Author: Akash Kumar Maurya
@@ -1,17 +1,19 @@
1
1
  datasourcelib/__init__.py,sha256=I7JTSZ1J6ULg_TfdMEgFcd1regkCHuyKdZT4DcPtoyQ,78
2
2
  datasourcelib/core/__init__.py,sha256=nsXojDd97T7eMqqtCsZr1qSYLBitvKydSZRb9Dg7hqU,462
3
3
  datasourcelib/core/sync_base.py,sha256=AfwwaV3rJOFKVmKKpSj-BwznnCDCaeuT4LLNDfA3NAY,716
4
- datasourcelib/core/sync_manager.py,sha256=lj070S3PwSNcB0UL_ZDzDAm6uJ9G38TY491vQZ1dL3o,3849
4
+ datasourcelib/core/sync_manager.py,sha256=pep3lS9GINzhOnwrMSPnOh5rfIsMbu8a0TEkTyq4yRk,3961
5
5
  datasourcelib/core/sync_types.py,sha256=KVZB7PkfkFTzghoe--U8jLeAU8XAfba9qMRIVcUjuMc,297
6
6
  datasourcelib/datasources/__init__.py,sha256=lZtgs0vT-2gub5UZo8BUnREZl3K_-_xYqUP8mjf8vhM,436
7
7
  datasourcelib/datasources/azure_devops_source copy.py,sha256=g-IOCq5vGwwteU21jZPWW_GggMu1_myVJkP0_BmSdGY,7282
8
8
  datasourcelib/datasources/azure_devops_source.py,sha256=3hyZIrUdgwZEQNjb2iZGDMJcAw3Z6r7oV0hWAq_zMsg,8005
9
9
  datasourcelib/datasources/blob_source.py,sha256=Qk61_ulqUSPYDaiMzqgvJAu43c4AjTlDRdfFg4VwgDU,3574
10
10
  datasourcelib/datasources/datasource_base.py,sha256=N8fOGvTl8oWWAiydLI0Joz66luq73a5yovO0XA9Q3jk,1068
11
- datasourcelib/datasources/datasource_types.py,sha256=eEiWymYS05X_TxwuB7P3MpphPG1En67h3kRiSGeHjQ0,176
11
+ datasourcelib/datasources/datasource_types.py,sha256=jpm4f9n1l7X9aBD58Pbr9evXiCHHEhRCLojGwchUD7A,205
12
+ datasourcelib/datasources/dataverse_source.py,sha256=8qScGvTvMOVeDc_ODYtBmx97L9AIlokz3wkzioT_ovw,13296
12
13
  datasourcelib/datasources/sharepoint_source - Copy.py,sha256=7V1c-zyvTo4IuPN_YMrKwLZFgbtipbP-mtunmXjOLJQ,17664
13
14
  datasourcelib/datasources/sharepoint_source.py,sha256=t3rly2mVEI2qEDuUVqstck5ktkZW0BnF16Bke_NjPLI,23126
14
15
  datasourcelib/datasources/sql_source.py,sha256=ntZjiFXpa7V797x7mAATJV0LH-g878VHuRw-QTxEe28,6372
16
+ datasourcelib/datasources/sql_source_bkup.py,sha256=ntZjiFXpa7V797x7mAATJV0LH-g878VHuRw-QTxEe28,6372
15
17
  datasourcelib/indexes/__init__.py,sha256=S8dz-lyxy1BTuDuLGRJNLrZD_1ku_FIUnDEm6HhMyT0,94
16
18
  datasourcelib/indexes/azure_search_index.py,sha256=kznAz06UXgyT1Clqj6gRhnBQ5HFw40ZQHJElRFIcbRo,22115
17
19
  datasourcelib/strategies/__init__.py,sha256=kot3u62KIAqYBg9M-KRE4mkMII_zwrDBZNf8Dj1vmX8,399
@@ -26,8 +28,8 @@ datasourcelib/utils/exceptions.py,sha256=mgcDaW1k3VndgpMOwSm7NqgyRTvvE2a5ehn3x4f
26
28
  datasourcelib/utils/file_reader.py,sha256=Zr0rwNTRWE6KeVJEXgTOPS1_JI74LiUSiX5-6qojmN0,7301
27
29
  datasourcelib/utils/logger.py,sha256=Sl6lNlvubxtK9ztzyq7vjGVyA8_-pZ_ixpk5jfVsh6U,424
28
30
  datasourcelib/utils/validators.py,sha256=fLgmRAb5OZSdMVlHu_n0RKJUDl-G8dI8JsRSfxIquh8,205
29
- datasourcelib-0.1.5.dist-info/licenses/LICENSE,sha256=9S0AcKETmp9XOcC73jEjN7WSkuSWGFGreiBat6ONClo,1087
30
- datasourcelib-0.1.5.dist-info/METADATA,sha256=jDGgTdya-zt_go_TpEOJNfTQUI7CsbjM4m-Fg51XdqU,1199
31
- datasourcelib-0.1.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
32
- datasourcelib-0.1.5.dist-info/top_level.txt,sha256=wIwiwdIj8T9pAvE2TkGLUvT2oIi43C2vkkTKibUlv3U,14
33
- datasourcelib-0.1.5.dist-info/RECORD,,
31
+ datasourcelib-0.1.6.dist-info/licenses/LICENSE,sha256=9S0AcKETmp9XOcC73jEjN7WSkuSWGFGreiBat6ONClo,1087
32
+ datasourcelib-0.1.6.dist-info/METADATA,sha256=5lpuBdVreQu7PHsMoD9RWsnSx2cZjpKLEjFhclwO5oA,1199
33
+ datasourcelib-0.1.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
34
+ datasourcelib-0.1.6.dist-info/top_level.txt,sha256=wIwiwdIj8T9pAvE2TkGLUvT2oIi43C2vkkTKibUlv3U,14
35
+ datasourcelib-0.1.6.dist-info/RECORD,,