tukan-python 0.2.1__tar.gz → 0.3.1__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.
- {tukan_python-0.2.1/tukan_python.egg-info → tukan_python-0.3.1}/PKG-INFO +38 -1
- {tukan_python-0.2.1 → tukan_python-0.3.1}/README.md +37 -0
- {tukan_python-0.2.1 → tukan_python-0.3.1}/pyproject.toml +1 -1
- tukan_python-0.3.1/tests/test_sql_query.py +204 -0
- tukan_python-0.3.1/tukan_python/__init__.py +5 -0
- {tukan_python-0.2.1 → tukan_python-0.3.1}/tukan_python/query.py +118 -0
- {tukan_python-0.2.1 → tukan_python-0.3.1}/tukan_python/tukan.py +70 -0
- {tukan_python-0.2.1 → tukan_python-0.3.1/tukan_python.egg-info}/PKG-INFO +38 -1
- {tukan_python-0.2.1 → tukan_python-0.3.1}/tukan_python.egg-info/SOURCES.txt +1 -0
- tukan_python-0.2.1/tukan_python/__init__.py +0 -5
- {tukan_python-0.2.1 → tukan_python-0.3.1}/LICENSE +0 -0
- {tukan_python-0.2.1 → tukan_python-0.3.1}/setup.cfg +0 -0
- {tukan_python-0.2.1 → tukan_python-0.3.1}/tests/test_query.py +0 -0
- {tukan_python-0.2.1 → tukan_python-0.3.1}/tukan_python.egg-info/dependency_links.txt +0 -0
- {tukan_python-0.2.1 → tukan_python-0.3.1}/tukan_python.egg-info/requires.txt +0 -0
- {tukan_python-0.2.1 → tukan_python-0.3.1}/tukan_python.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tukan_python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: SDK de Python para acceder a datos oficiales de México a través de la API de Tukan.
|
|
5
5
|
Author-email: TukanMx <contacto@tukanmx.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -300,6 +300,43 @@ q.set_table_name("mex_cnbv_cb_orig_by_gender_monthly")
|
|
|
300
300
|
resultado = q.execute_query()
|
|
301
301
|
```
|
|
302
302
|
|
|
303
|
+
### 7. Consultas SQL con `SQLQuery`
|
|
304
|
+
|
|
305
|
+
Si prefieres escribir SQL directamente, puedes usar la clase `SQLQuery`. Las consultas se ejecutan con el motor Blizzard y la paginación se maneja automáticamente:
|
|
306
|
+
|
|
307
|
+
```python
|
|
308
|
+
from tukan_python import SQLQuery
|
|
309
|
+
|
|
310
|
+
sq = SQLQuery(sql="""
|
|
311
|
+
SELECT
|
|
312
|
+
END_DATE AS end_date,
|
|
313
|
+
INSTITUTIONS_REF AS institutions,
|
|
314
|
+
INSTITUTIONS_NAME AS institutions__name,
|
|
315
|
+
INDICATOR_REF AS indicator,
|
|
316
|
+
INDICATOR_NAME AS indicator__name,
|
|
317
|
+
VALUE as value
|
|
318
|
+
FROM tukan_db.source_of_truth_full.mex_tukan_retail_sales_by_company
|
|
319
|
+
WHERE END_DATE = '2022-12-31'
|
|
320
|
+
LIMIT 100000 OFFSET 0
|
|
321
|
+
""")
|
|
322
|
+
|
|
323
|
+
resultado = sq.execute()
|
|
324
|
+
print(resultado["df"])
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
Las tablas disponibles en SQL se encuentran en el esquema `tukan_db.source_of_truth_full`.
|
|
328
|
+
|
|
329
|
+
#### Guardar una consulta SQL en tu perfil
|
|
330
|
+
|
|
331
|
+
Puedes guardar tus consultas SQL para acceder a ellas desde la [aplicación web](https://app.tukanmx.com):
|
|
332
|
+
|
|
333
|
+
```python
|
|
334
|
+
sq = SQLQuery(sql="SELECT * FROM tukan_db.source_of_truth_full.mex_banxico_cf102 LIMIT 100")
|
|
335
|
+
sq.save_sql_query(name="Tipo de cambio FIX", language="es")
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
Parámetros opcionales: `description`, `tags` (lista de strings).
|
|
339
|
+
|
|
303
340
|
## Conceptos clave
|
|
304
341
|
|
|
305
342
|
### Tablas
|
|
@@ -272,6 +272,43 @@ q.set_table_name("mex_cnbv_cb_orig_by_gender_monthly")
|
|
|
272
272
|
resultado = q.execute_query()
|
|
273
273
|
```
|
|
274
274
|
|
|
275
|
+
### 7. Consultas SQL con `SQLQuery`
|
|
276
|
+
|
|
277
|
+
Si prefieres escribir SQL directamente, puedes usar la clase `SQLQuery`. Las consultas se ejecutan con el motor Blizzard y la paginación se maneja automáticamente:
|
|
278
|
+
|
|
279
|
+
```python
|
|
280
|
+
from tukan_python import SQLQuery
|
|
281
|
+
|
|
282
|
+
sq = SQLQuery(sql="""
|
|
283
|
+
SELECT
|
|
284
|
+
END_DATE AS end_date,
|
|
285
|
+
INSTITUTIONS_REF AS institutions,
|
|
286
|
+
INSTITUTIONS_NAME AS institutions__name,
|
|
287
|
+
INDICATOR_REF AS indicator,
|
|
288
|
+
INDICATOR_NAME AS indicator__name,
|
|
289
|
+
VALUE as value
|
|
290
|
+
FROM tukan_db.source_of_truth_full.mex_tukan_retail_sales_by_company
|
|
291
|
+
WHERE END_DATE = '2022-12-31'
|
|
292
|
+
LIMIT 100000 OFFSET 0
|
|
293
|
+
""")
|
|
294
|
+
|
|
295
|
+
resultado = sq.execute()
|
|
296
|
+
print(resultado["df"])
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
Las tablas disponibles en SQL se encuentran en el esquema `tukan_db.source_of_truth_full`.
|
|
300
|
+
|
|
301
|
+
#### Guardar una consulta SQL en tu perfil
|
|
302
|
+
|
|
303
|
+
Puedes guardar tus consultas SQL para acceder a ellas desde la [aplicación web](https://app.tukanmx.com):
|
|
304
|
+
|
|
305
|
+
```python
|
|
306
|
+
sq = SQLQuery(sql="SELECT * FROM tukan_db.source_of_truth_full.mex_banxico_cf102 LIMIT 100")
|
|
307
|
+
sq.save_sql_query(name="Tipo de cambio FIX", language="es")
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Parámetros opcionales: `description`, `tags` (lista de strings).
|
|
311
|
+
|
|
275
312
|
## Conceptos clave
|
|
276
313
|
|
|
277
314
|
### Tablas
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "tukan_python"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.1"
|
|
8
8
|
description = "SDK de Python para acceder a datos oficiales de México a través de la API de Tukan."
|
|
9
9
|
authors = [
|
|
10
10
|
{ name = "TukanMx", email = "contacto@tukanmx.com" }
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
|
|
3
|
+
import pandas as pd
|
|
4
|
+
import pytest
|
|
5
|
+
from unittest.mock import patch, MagicMock
|
|
6
|
+
|
|
7
|
+
from tukan_python.query import SQLQuery
|
|
8
|
+
from tukan_python.tukan import Tukan
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# --- Initialization ---
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@patch.object(Tukan, "__init__", return_value=None)
|
|
15
|
+
def test_sql_query_init_with_sql(mock_init):
|
|
16
|
+
sq = SQLQuery(sql="SELECT 1")
|
|
17
|
+
assert sq.sql == "SELECT 1"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@patch.object(Tukan, "__init__", return_value=None)
|
|
21
|
+
def test_sql_query_init_without_sql(mock_init):
|
|
22
|
+
sq = SQLQuery()
|
|
23
|
+
assert sq.sql is None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@patch.object(Tukan, "__init__", return_value=None)
|
|
27
|
+
def test_set_sql(mock_init):
|
|
28
|
+
sq = SQLQuery()
|
|
29
|
+
sq.set_sql("SELECT * FROM table")
|
|
30
|
+
assert sq.sql == "SELECT * FROM table"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# --- Base64 encoding ---
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@patch.object(Tukan, "__init__", return_value=None)
|
|
37
|
+
def test_encode_sql(mock_init):
|
|
38
|
+
sq = SQLQuery(sql="SELECT 1")
|
|
39
|
+
encoded = sq._encode_sql()
|
|
40
|
+
decoded = base64.b64decode(encoded).decode("utf-8")
|
|
41
|
+
assert decoded == "SELECT 1"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@patch.object(Tukan, "__init__", return_value=None)
|
|
45
|
+
def test_encode_sql_unicode(mock_init):
|
|
46
|
+
sq = SQLQuery(sql="SELECT * FROM tabla WHERE nombre = 'México'")
|
|
47
|
+
encoded = sq._encode_sql()
|
|
48
|
+
decoded = base64.b64decode(encoded).decode("utf-8")
|
|
49
|
+
assert "México" in decoded
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@patch.object(Tukan, "__init__", return_value=None)
|
|
53
|
+
def test_encode_sql_raises_when_no_sql(mock_init):
|
|
54
|
+
sq = SQLQuery()
|
|
55
|
+
with pytest.raises(ValueError, match="SQL query not set"):
|
|
56
|
+
sq._encode_sql()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# --- Payload ---
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@patch.object(Tukan, "__init__", return_value=None)
|
|
63
|
+
def test_build_payload_no_offset(mock_init):
|
|
64
|
+
sq = SQLQuery(sql="SELECT 1")
|
|
65
|
+
payload = sq._build_payload()
|
|
66
|
+
assert "raw_sql" in payload
|
|
67
|
+
assert "offset" not in payload
|
|
68
|
+
assert base64.b64decode(payload["raw_sql"]).decode("utf-8") == "SELECT 1"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@patch.object(Tukan, "__init__", return_value=None)
|
|
72
|
+
def test_build_payload_with_offset(mock_init):
|
|
73
|
+
sq = SQLQuery(sql="SELECT 1")
|
|
74
|
+
payload = sq._build_payload(offset=100)
|
|
75
|
+
assert payload["offset"] == 100
|
|
76
|
+
assert "raw_sql" in payload
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# --- Execution (mocked) ---
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@patch.object(Tukan, "__init__", return_value=None)
|
|
83
|
+
@patch.object(Tukan, "execute_post_operation")
|
|
84
|
+
def test_execute_single_page(mock_post, mock_init):
|
|
85
|
+
mock_post.return_value = {
|
|
86
|
+
"data": [{"col1": "a", "col2": 1}, {"col1": "b", "col2": 2}],
|
|
87
|
+
"has_more_data": False,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
sq = SQLQuery(sql="SELECT col1, col2 FROM table")
|
|
91
|
+
result = sq.execute()
|
|
92
|
+
|
|
93
|
+
assert isinstance(result["df"], pd.DataFrame)
|
|
94
|
+
assert len(result["df"]) == 2
|
|
95
|
+
assert len(result["data"]) == 2
|
|
96
|
+
assert result["data"][0]["col1"] == "a"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@patch.object(Tukan, "__init__", return_value=None)
|
|
100
|
+
@patch.object(Tukan, "execute_post_operation")
|
|
101
|
+
def test_execute_with_pagination(mock_post, mock_init):
|
|
102
|
+
mock_post.side_effect = [
|
|
103
|
+
{"data": [{"id": 1}, {"id": 2}], "has_more_data": True},
|
|
104
|
+
{"data": [{"id": 3}], "has_more_data": False},
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
sq = SQLQuery(sql="SELECT id FROM table")
|
|
108
|
+
result = sq.execute()
|
|
109
|
+
|
|
110
|
+
assert len(result["df"]) == 3
|
|
111
|
+
assert len(result["data"]) == 3
|
|
112
|
+
assert mock_post.call_count == 2
|
|
113
|
+
# Verify offset was passed in the second call
|
|
114
|
+
second_call_payload = mock_post.call_args_list[1][0][0]
|
|
115
|
+
assert second_call_payload["offset"] == 2
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@patch.object(Tukan, "__init__", return_value=None)
|
|
119
|
+
@patch.object(Tukan, "execute_post_operation")
|
|
120
|
+
def test_execute_empty_result(mock_post, mock_init):
|
|
121
|
+
mock_post.return_value = {"data": [], "has_more_data": False}
|
|
122
|
+
|
|
123
|
+
sq = SQLQuery(sql="SELECT 1 WHERE 1=0")
|
|
124
|
+
result = sq.execute()
|
|
125
|
+
|
|
126
|
+
assert len(result["df"]) == 0
|
|
127
|
+
assert result["data"] == []
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# --- API validation errors ---
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@patch.object(Tukan, "__init__", return_value=None)
|
|
134
|
+
@patch.object(Tukan, "execute_post_operation")
|
|
135
|
+
def test_execute_raises_on_validation_error(mock_post, mock_init):
|
|
136
|
+
mock_post.return_value = {
|
|
137
|
+
"detail": "Raw SQL validation failed: Query must start with SELECT or WITH (for CTEs)"
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
sq = SQLQuery(sql="DROP TABLE users")
|
|
141
|
+
with pytest.raises(ValueError, match="Raw SQL validation failed"):
|
|
142
|
+
sq.execute()
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@patch.object(Tukan, "__init__", return_value=None)
|
|
146
|
+
@patch.object(Tukan, "execute_post_operation")
|
|
147
|
+
def test_execute_raises_on_pagination_error(mock_post, mock_init):
|
|
148
|
+
mock_post.side_effect = [
|
|
149
|
+
{"data": [{"id": 1}], "has_more_data": True},
|
|
150
|
+
{"detail": "Some server error"},
|
|
151
|
+
]
|
|
152
|
+
|
|
153
|
+
sq = SQLQuery(sql="SELECT id FROM table")
|
|
154
|
+
with pytest.raises(ValueError, match="Some server error"):
|
|
155
|
+
sq.execute()
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
# --- String representation ---
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@patch.object(Tukan, "__init__", return_value=None)
|
|
162
|
+
def test_str(mock_init):
|
|
163
|
+
sq = SQLQuery(sql="SELECT 1")
|
|
164
|
+
assert "SELECT 1" in str(sq)
|
|
165
|
+
assert "SELECT 1" in repr(sq)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
# --- Tukan.sql() convenience ---
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@patch.object(Tukan, "execute_post_operation")
|
|
172
|
+
def test_tukan_sql_convenience(mock_post):
|
|
173
|
+
mock_post.return_value = {"data": [{"x": 1}], "has_more_data": False}
|
|
174
|
+
|
|
175
|
+
t = Tukan.__new__(Tukan)
|
|
176
|
+
t.token = "test_token"
|
|
177
|
+
t.env = "https://client.tukanmx.com/"
|
|
178
|
+
result = t.sql("SELECT x FROM table")
|
|
179
|
+
|
|
180
|
+
assert isinstance(result["df"], pd.DataFrame)
|
|
181
|
+
assert len(result["data"]) == 1
|
|
182
|
+
# Verify the endpoint used
|
|
183
|
+
call_args = mock_post.call_args
|
|
184
|
+
assert call_args[0][1] == "data/retrieve/?engine=blizzard"
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
# --- Endpoint ---
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def test_endpoint_includes_engine_param():
|
|
191
|
+
assert SQLQuery.ENDPOINT == "data/retrieve/?engine=blizzard"
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# --- _from_tukan classmethod ---
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def test_from_tukan():
|
|
198
|
+
t = Tukan.__new__(Tukan)
|
|
199
|
+
t.token = "test"
|
|
200
|
+
t.env = "https://client.tukanmx.com/"
|
|
201
|
+
|
|
202
|
+
sq = SQLQuery._from_tukan(t, "SELECT 1")
|
|
203
|
+
assert sq.tukan is t
|
|
204
|
+
assert sq.sql == "SELECT 1"
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import base64
|
|
1
2
|
from typing import Literal, Optional
|
|
2
3
|
|
|
3
4
|
import pandas as pd
|
|
@@ -207,3 +208,120 @@ class Query:
|
|
|
207
208
|
if "indicators" in response:
|
|
208
209
|
result["indicators"] = response["indicators"]
|
|
209
210
|
return result
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class SQLQuery:
|
|
214
|
+
"""Execute raw SQL queries against the Tukan API (blizzard engine).
|
|
215
|
+
|
|
216
|
+
The SQL string is base64-encoded and sent to the blizzard engine endpoint.
|
|
217
|
+
Pagination is handled automatically when the result set exceeds a single page.
|
|
218
|
+
|
|
219
|
+
Usage:
|
|
220
|
+
sq = SQLQuery(sql="SELECT * FROM tukan_db.source_of_truth_full.table_name LIMIT 1000")
|
|
221
|
+
result = sq.execute()
|
|
222
|
+
df = result["df"]
|
|
223
|
+
"""
|
|
224
|
+
|
|
225
|
+
ENDPOINT = "data/retrieve/?engine=blizzard"
|
|
226
|
+
|
|
227
|
+
def __init__(self, token: Optional[str] = None, sql: Optional[str] = None):
|
|
228
|
+
self.tukan = Tukan(token)
|
|
229
|
+
self._sql = sql
|
|
230
|
+
|
|
231
|
+
def set_sql(self, sql: str) -> None:
|
|
232
|
+
self._sql = sql
|
|
233
|
+
|
|
234
|
+
@property
|
|
235
|
+
def sql(self) -> Optional[str]:
|
|
236
|
+
return self._sql
|
|
237
|
+
|
|
238
|
+
def _encode_sql(self) -> str:
|
|
239
|
+
if self._sql is None:
|
|
240
|
+
raise ValueError(
|
|
241
|
+
"SQL query not set. Call set_sql() or pass sql= to the constructor."
|
|
242
|
+
)
|
|
243
|
+
return base64.b64encode(self._sql.encode("utf-8")).decode("utf-8")
|
|
244
|
+
|
|
245
|
+
def _build_payload(self, offset: int = 0) -> dict:
|
|
246
|
+
payload = {"raw_sql": self._encode_sql()}
|
|
247
|
+
if offset > 0:
|
|
248
|
+
payload["offset"] = offset
|
|
249
|
+
return payload
|
|
250
|
+
|
|
251
|
+
def execute(self) -> dict:
|
|
252
|
+
"""Execute the SQL query and return results with pagination.
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
dict with keys:
|
|
256
|
+
- "df": pandas DataFrame with all result rows
|
|
257
|
+
- "data": raw list of dicts
|
|
258
|
+
|
|
259
|
+
Raises:
|
|
260
|
+
ValueError: If the API rejects the SQL (e.g. validation failure).
|
|
261
|
+
"""
|
|
262
|
+
payload = self._build_payload()
|
|
263
|
+
response = self.tukan.execute_post_operation(payload, self.ENDPOINT)
|
|
264
|
+
|
|
265
|
+
if isinstance(response, dict) and "detail" in response:
|
|
266
|
+
raise ValueError(response["detail"])
|
|
267
|
+
|
|
268
|
+
all_data = response.get("data", [])
|
|
269
|
+
offset = len(all_data)
|
|
270
|
+
|
|
271
|
+
while response.get("has_more_data", False):
|
|
272
|
+
payload = self._build_payload(offset=offset)
|
|
273
|
+
response = self.tukan.execute_post_operation(payload, self.ENDPOINT)
|
|
274
|
+
if isinstance(response, dict) and "detail" in response:
|
|
275
|
+
raise ValueError(response["detail"])
|
|
276
|
+
page_data = response.get("data", [])
|
|
277
|
+
all_data.extend(page_data)
|
|
278
|
+
offset += len(page_data)
|
|
279
|
+
|
|
280
|
+
df = pd.DataFrame(all_data)
|
|
281
|
+
return {"df": df, "data": all_data}
|
|
282
|
+
|
|
283
|
+
@classmethod
|
|
284
|
+
def _from_tukan(cls, tukan_instance: "Tukan", sql: str) -> "SQLQuery":
|
|
285
|
+
instance = cls.__new__(cls)
|
|
286
|
+
instance.tukan = tukan_instance
|
|
287
|
+
instance._sql = sql
|
|
288
|
+
return instance
|
|
289
|
+
|
|
290
|
+
def save_sql_query(
|
|
291
|
+
self,
|
|
292
|
+
name: str,
|
|
293
|
+
language: str = "es",
|
|
294
|
+
description: str = "",
|
|
295
|
+
tags: Optional[list[str]] = None,
|
|
296
|
+
) -> dict:
|
|
297
|
+
"""Save this SQL query to the user's profile.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
name: Display name for the saved query.
|
|
301
|
+
language: Language code (default "es").
|
|
302
|
+
description: Optional description.
|
|
303
|
+
tags: Optional list of tag strings.
|
|
304
|
+
|
|
305
|
+
Returns:
|
|
306
|
+
API response dict.
|
|
307
|
+
"""
|
|
308
|
+
body = {
|
|
309
|
+
"name": name,
|
|
310
|
+
"description": description,
|
|
311
|
+
"language": language,
|
|
312
|
+
"query": {
|
|
313
|
+
"name": name,
|
|
314
|
+
"raw_sql": self._encode_sql(),
|
|
315
|
+
"language": language,
|
|
316
|
+
"retrieve_engine": "blizzard",
|
|
317
|
+
"use_vertical_mode": False,
|
|
318
|
+
},
|
|
319
|
+
"tags": tags if tags is not None else [],
|
|
320
|
+
}
|
|
321
|
+
return self.tukan.execute_post_operation(body, "visualizations/query/")
|
|
322
|
+
|
|
323
|
+
def __str__(self) -> str:
|
|
324
|
+
return f"SQLQuery(sql={self._sql!r})"
|
|
325
|
+
|
|
326
|
+
def __repr__(self) -> str:
|
|
327
|
+
return self.__str__()
|
|
@@ -198,6 +198,76 @@ class Tukan:
|
|
|
198
198
|
return df
|
|
199
199
|
|
|
200
200
|
|
|
201
|
+
def get_catalog_tables(
|
|
202
|
+
self, page: int = 1, page_size: int = 40, language: str = "es"
|
|
203
|
+
) -> list[dict]:
|
|
204
|
+
"""List available catalog tables.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
page: Page number (1-indexed).
|
|
208
|
+
page_size: Number of results per page.
|
|
209
|
+
language: Language code ('es' or 'en').
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
List of catalog table dicts.
|
|
213
|
+
"""
|
|
214
|
+
url = f"{self.env}data/catalogue_table/"
|
|
215
|
+
headers = {"Authorization": f"token {self.token}"}
|
|
216
|
+
params = {
|
|
217
|
+
"page_size": page_size,
|
|
218
|
+
"current": page,
|
|
219
|
+
"api": "data",
|
|
220
|
+
"resource": "catalogue_table",
|
|
221
|
+
"language": language,
|
|
222
|
+
}
|
|
223
|
+
request_partial = wrapped_partial(
|
|
224
|
+
requests.get, url=url, headers=headers, params=params, timeout=30
|
|
225
|
+
)
|
|
226
|
+
response = self.persistent_request(request_partial)
|
|
227
|
+
if response.status_code >= 300:
|
|
228
|
+
raise Exception(f"Failed to list catalog tables: {response.text}")
|
|
229
|
+
return response.json()
|
|
230
|
+
|
|
231
|
+
def export_catalog_table(self, catalogue_id: str) -> pd.DataFrame:
|
|
232
|
+
"""Export a catalog table as a DataFrame.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
catalogue_id: The catalog table ID to export.
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
DataFrame with the catalog table data.
|
|
239
|
+
"""
|
|
240
|
+
url = f"{self.env}data/retrieve_json_catalogue/{catalogue_id}/{self.token}/"
|
|
241
|
+
request_partial = wrapped_partial(requests.get, url=url, timeout=30)
|
|
242
|
+
response = self.persistent_request(request_partial)
|
|
243
|
+
if response.status_code >= 300:
|
|
244
|
+
raise Exception(f"Failed to export catalog table: {response.text}")
|
|
245
|
+
data = response.json()
|
|
246
|
+
return pd.DataFrame(data)
|
|
247
|
+
|
|
248
|
+
def sql(self, query: str) -> dict:
|
|
249
|
+
"""Execute a raw SQL query against the Tukan API.
|
|
250
|
+
|
|
251
|
+
The SQL string is base64-encoded and sent to the blizzard engine.
|
|
252
|
+
Pagination is handled automatically for large result sets.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
query: SQL query string
|
|
256
|
+
(e.g. "SELECT * FROM tukan_db.source_of_truth_full.table_name LIMIT 1000")
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
dict with keys:
|
|
260
|
+
- "df": pandas DataFrame with query results
|
|
261
|
+
- "data": raw list of dicts
|
|
262
|
+
|
|
263
|
+
Raises:
|
|
264
|
+
ValueError: If the API rejects the SQL (e.g. validation failure).
|
|
265
|
+
"""
|
|
266
|
+
from tukan_python.query import SQLQuery
|
|
267
|
+
|
|
268
|
+
return SQLQuery._from_tukan(self, query).execute()
|
|
269
|
+
|
|
270
|
+
|
|
201
271
|
def wrapped_partial(func, *args, **kwargs) -> Callable:
|
|
202
272
|
partial_func = partial(func, *args, **kwargs)
|
|
203
273
|
update_wrapper(partial_func, func)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tukan_python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: SDK de Python para acceder a datos oficiales de México a través de la API de Tukan.
|
|
5
5
|
Author-email: TukanMx <contacto@tukanmx.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -300,6 +300,43 @@ q.set_table_name("mex_cnbv_cb_orig_by_gender_monthly")
|
|
|
300
300
|
resultado = q.execute_query()
|
|
301
301
|
```
|
|
302
302
|
|
|
303
|
+
### 7. Consultas SQL con `SQLQuery`
|
|
304
|
+
|
|
305
|
+
Si prefieres escribir SQL directamente, puedes usar la clase `SQLQuery`. Las consultas se ejecutan con el motor Blizzard y la paginación se maneja automáticamente:
|
|
306
|
+
|
|
307
|
+
```python
|
|
308
|
+
from tukan_python import SQLQuery
|
|
309
|
+
|
|
310
|
+
sq = SQLQuery(sql="""
|
|
311
|
+
SELECT
|
|
312
|
+
END_DATE AS end_date,
|
|
313
|
+
INSTITUTIONS_REF AS institutions,
|
|
314
|
+
INSTITUTIONS_NAME AS institutions__name,
|
|
315
|
+
INDICATOR_REF AS indicator,
|
|
316
|
+
INDICATOR_NAME AS indicator__name,
|
|
317
|
+
VALUE as value
|
|
318
|
+
FROM tukan_db.source_of_truth_full.mex_tukan_retail_sales_by_company
|
|
319
|
+
WHERE END_DATE = '2022-12-31'
|
|
320
|
+
LIMIT 100000 OFFSET 0
|
|
321
|
+
""")
|
|
322
|
+
|
|
323
|
+
resultado = sq.execute()
|
|
324
|
+
print(resultado["df"])
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
Las tablas disponibles en SQL se encuentran en el esquema `tukan_db.source_of_truth_full`.
|
|
328
|
+
|
|
329
|
+
#### Guardar una consulta SQL en tu perfil
|
|
330
|
+
|
|
331
|
+
Puedes guardar tus consultas SQL para acceder a ellas desde la [aplicación web](https://app.tukanmx.com):
|
|
332
|
+
|
|
333
|
+
```python
|
|
334
|
+
sq = SQLQuery(sql="SELECT * FROM tukan_db.source_of_truth_full.mex_banxico_cf102 LIMIT 100")
|
|
335
|
+
sq.save_sql_query(name="Tipo de cambio FIX", language="es")
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
Parámetros opcionales: `description`, `tags` (lista de strings).
|
|
339
|
+
|
|
303
340
|
## Conceptos clave
|
|
304
341
|
|
|
305
342
|
### Tablas
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|