tinybird-cli 5.18.1.dev0__tar.gz → 5.18.1.dev2__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.
- {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/PKG-INFO +6 -1
- {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/__cli__.py +2 -2
- {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/ch_utils/engine.py +14 -10
- {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/check_pypi.py +1 -1
- {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/client.py +135 -27
- {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/config.py +31 -3
- {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/datafile.py +2 -0
- {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/datatypes.py +29 -24
- {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/feedback_manager.py +3 -0
- {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/sql.py +46 -3
- {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/sql_template.py +103 -0
- {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/tb_cli_modules/common.py +1 -1
- {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/tb_cli_modules/datasource.py +7 -0
- {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird_cli.egg-info/PKG-INFO +6 -1
- {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird_cli.egg-info/top_level.txt +1 -0
- {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/setup.cfg +0 -0
- {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/ch_utils/constants.py +0 -0
- {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/connectors.py +0 -0
- {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/context.py +0 -0
- {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/git_settings.py +0 -0
- {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/sql_template_fmt.py +0 -0
- {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/sql_toolset.py +0 -0
- {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/syncasync.py +0 -0
- {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/tb_cli.py +0 -0
- {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/tb_cli_modules/auth.py +0 -0
- {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/tb_cli_modules/branch.py +0 -0
- {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/tb_cli_modules/cicd.py +0 -0
- {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/tb_cli_modules/cli.py +0 -0
- {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/tb_cli_modules/config.py +0 -0
- {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/tb_cli_modules/connection.py +0 -0
- {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/tb_cli_modules/exceptions.py +0 -0
- {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/tb_cli_modules/fmt.py +0 -0
- {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/tb_cli_modules/job.py +0 -0
- {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/tb_cli_modules/pipe.py +0 -0
- {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/tb_cli_modules/regions.py +0 -0
- {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/tb_cli_modules/tag.py +0 -0
- {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/tb_cli_modules/telemetry.py +0 -0
- {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/tb_cli_modules/test.py +0 -0
- {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/tb_cli_modules/tinyunit/tinyunit.py +0 -0
- {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/tb_cli_modules/tinyunit/tinyunit_lib.py +0 -0
- {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/tb_cli_modules/token.py +0 -0
- {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/tb_cli_modules/workspace.py +0 -0
- {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/tb_cli_modules/workspace_members.py +0 -0
- {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/tornado_template.py +0 -0
- {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird_cli.egg-info/SOURCES.txt +0 -0
- {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird_cli.egg-info/dependency_links.txt +0 -0
- {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird_cli.egg-info/entry_points.txt +0 -0
- {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird_cli.egg-info/requires.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: tinybird_cli
|
|
3
|
-
Version: 5.18.1.
|
|
3
|
+
Version: 5.18.1.dev2
|
|
4
4
|
Summary: Tinybird Command Line Tool
|
|
5
5
|
Home-page: https://www.tinybird.co/docs/cli
|
|
6
6
|
Author: Tinybird
|
|
@@ -61,6 +61,11 @@ The Tinybird command-line tool allows you to use all the Tinybird functionality
|
|
|
61
61
|
Changelog
|
|
62
62
|
----------
|
|
63
63
|
|
|
64
|
+
5.18.1.dev1
|
|
65
|
+
***********
|
|
66
|
+
|
|
67
|
+
- `Added` support for changes in `/v0/datasources/(.+)/delete`.
|
|
68
|
+
|
|
64
69
|
5.18.0
|
|
65
70
|
***********
|
|
66
71
|
|
|
@@ -4,5 +4,5 @@ __description__ = 'Tinybird Command Line Tool'
|
|
|
4
4
|
__url__ = 'https://www.tinybird.co/docs/cli'
|
|
5
5
|
__author__ = 'Tinybird'
|
|
6
6
|
__author_email__ = 'support@tinybird.co'
|
|
7
|
-
__version__ = '5.18.1.
|
|
8
|
-
__revision__ = '
|
|
7
|
+
__version__ = '5.18.1.dev2'
|
|
8
|
+
__revision__ = '046558d'
|
|
@@ -456,7 +456,7 @@ ENABLED_ENGINES = [
|
|
|
456
456
|
MERGETREE_OPTIONS,
|
|
457
457
|
),
|
|
458
458
|
# AggregatingMergeTree()
|
|
459
|
-
engine_config("AggregatingMergeTree", options=
|
|
459
|
+
engine_config("AggregatingMergeTree", options=MERGETREE_OPTIONS),
|
|
460
460
|
# CollapsingMergeTree(sign)
|
|
461
461
|
engine_config(
|
|
462
462
|
"CollapsingMergeTree",
|
|
@@ -631,9 +631,7 @@ def engine_full_from_dict(
|
|
|
631
631
|
|
|
632
632
|
>>> schema = 'sign_column Int8'
|
|
633
633
|
>>> engine_full_from_dict('AggregatingMergeTree', {}, schema=schema)
|
|
634
|
-
|
|
635
|
-
...
|
|
636
|
-
ValueError: Missing required option 'sorting_key'
|
|
634
|
+
'AggregatingMergeTree() ORDER BY (tuple())'
|
|
637
635
|
|
|
638
636
|
>>> columns=[]
|
|
639
637
|
>>> columns.append({'name': 'key_column', 'type': 'Int8', 'codec': None, 'default_value': None, 'nullable': False, 'normalized_name': 'key_column'})
|
|
@@ -763,6 +761,16 @@ def engine_local_to_replicated(engine: str, database: str, name: str) -> str:
|
|
|
763
761
|
return re.sub(r"(.*)MergeTree(\(([^\)]*)\))*(.*)", _replace, engine.strip())
|
|
764
762
|
|
|
765
763
|
|
|
764
|
+
def ttl_from_engine(engine: str) -> Optional[str]:
|
|
765
|
+
ttl_array = engine.split(" TTL ")
|
|
766
|
+
if len(ttl_array) <= 1:
|
|
767
|
+
return None
|
|
768
|
+
settings_array = engine.split(" SETTINGS ")
|
|
769
|
+
settings = " SETTINGS " + settings_array[1] if len(settings_array) > 1 else None
|
|
770
|
+
ttl = ttl_array[1][: -(len(settings))] if settings else ttl_array[1]
|
|
771
|
+
return ttl
|
|
772
|
+
|
|
773
|
+
|
|
766
774
|
def ttl_condition_from_engine_full(engine_full: Optional[str]) -> Optional[str]:
|
|
767
775
|
"""
|
|
768
776
|
>>> ttl_condition_from_engine_full(None)
|
|
@@ -810,13 +818,9 @@ def ttl_condition_from_engine_full(engine_full: Optional[str]) -> Optional[str]:
|
|
|
810
818
|
return None
|
|
811
819
|
|
|
812
820
|
try:
|
|
813
|
-
|
|
814
|
-
if
|
|
821
|
+
ttl = ttl_from_engine(engine_full)
|
|
822
|
+
if not ttl:
|
|
815
823
|
return None
|
|
816
|
-
settings_array = engine_full.split(" SETTINGS ")
|
|
817
|
-
settings = " SETTINGS " + settings_array[1] if len(settings_array) > 1 else None
|
|
818
|
-
ttl = ttl_array[1][: -(len(settings))] if settings else ttl_array[1]
|
|
819
|
-
|
|
820
824
|
groups = SIMPLE_TTL_DEFINITION.search(ttl)
|
|
821
825
|
if not groups:
|
|
822
826
|
return None
|
|
@@ -6,7 +6,7 @@ from tinybird.feedback_manager import FeedbackManager
|
|
|
6
6
|
from tinybird.syncasync import sync_to_async
|
|
7
7
|
from tinybird.tb_cli_modules.common import CLIException, getenv_bool
|
|
8
8
|
|
|
9
|
-
PYPY_URL = "https://pypi.org/pypi/tinybird
|
|
9
|
+
PYPY_URL = "https://pypi.org/pypi/tinybird/json"
|
|
10
10
|
requests_get = sync_to_async(requests.get, thread_sensitive=False)
|
|
11
11
|
|
|
12
12
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import json
|
|
3
3
|
import logging
|
|
4
|
+
import os
|
|
4
5
|
import ssl
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
from typing import Any, Callable, Dict, List, Mapping, Optional, Set, Union
|
|
@@ -91,6 +92,8 @@ class TinyB:
|
|
|
91
92
|
disable_ssl_checks: bool = False,
|
|
92
93
|
send_telemetry: bool = False,
|
|
93
94
|
semver: Optional[str] = None,
|
|
95
|
+
env: Optional[str] = "production",
|
|
96
|
+
staging: bool = False,
|
|
94
97
|
):
|
|
95
98
|
ctx = ssl.create_default_context()
|
|
96
99
|
ctx.check_hostname = False
|
|
@@ -102,8 +105,10 @@ class TinyB:
|
|
|
102
105
|
self.disable_ssl_checks = disable_ssl_checks
|
|
103
106
|
self.send_telemetry = send_telemetry
|
|
104
107
|
self.semver = semver
|
|
108
|
+
self.env = env
|
|
109
|
+
self.staging = staging
|
|
105
110
|
|
|
106
|
-
async def
|
|
111
|
+
async def _req_raw(
|
|
107
112
|
self,
|
|
108
113
|
endpoint: str,
|
|
109
114
|
data=None,
|
|
@@ -112,7 +117,7 @@ class TinyB:
|
|
|
112
117
|
retries: int = LIMIT_RETRIES,
|
|
113
118
|
use_token: Optional[str] = None,
|
|
114
119
|
**kwargs,
|
|
115
|
-
):
|
|
120
|
+
) -> Response:
|
|
116
121
|
url = f"{self.host.strip('/')}/{endpoint.strip('/')}"
|
|
117
122
|
|
|
118
123
|
token_to_use = use_token if use_token else self.token
|
|
@@ -122,6 +127,8 @@ class TinyB:
|
|
|
122
127
|
url += ("&" if "?" in url else "?") + "cli_version=" + quote(self.version)
|
|
123
128
|
if self.semver:
|
|
124
129
|
url += ("&" if "?" in url else "?") + "__tb__semver=" + self.semver
|
|
130
|
+
if self.staging:
|
|
131
|
+
url += ("&" if "?" in url else "?") + "__tb__deployment=staging"
|
|
125
132
|
|
|
126
133
|
verify_ssl = not self.disable_ssl_checks
|
|
127
134
|
try:
|
|
@@ -155,10 +162,15 @@ class TinyB:
|
|
|
155
162
|
response = await sync_to_async(session.get, thread_sensitive=False)(
|
|
156
163
|
url, verify=verify_ssl, **kwargs
|
|
157
164
|
)
|
|
158
|
-
|
|
159
165
|
except Exception as e:
|
|
160
166
|
raise e
|
|
161
167
|
|
|
168
|
+
if self.send_telemetry:
|
|
169
|
+
try:
|
|
170
|
+
add_telemetry_event("api_request", endpoint=url, token=self.token, status_code=response.status_code)
|
|
171
|
+
except Exception as ex:
|
|
172
|
+
logging.exception(f"Can't send telemetry: {ex}")
|
|
173
|
+
|
|
162
174
|
logging.debug("== server response ==")
|
|
163
175
|
logging.debug(response.content)
|
|
164
176
|
logging.debug("== end ==")
|
|
@@ -169,6 +181,21 @@ class TinyB:
|
|
|
169
181
|
except Exception as ex:
|
|
170
182
|
logging.exception(f"Can't send telemetry: {ex}")
|
|
171
183
|
|
|
184
|
+
return response
|
|
185
|
+
|
|
186
|
+
async def _req(
|
|
187
|
+
self,
|
|
188
|
+
endpoint: str,
|
|
189
|
+
data=None,
|
|
190
|
+
files=None,
|
|
191
|
+
method: str = "GET",
|
|
192
|
+
retries: int = LIMIT_RETRIES,
|
|
193
|
+
use_token: Optional[str] = None,
|
|
194
|
+
**kwargs,
|
|
195
|
+
):
|
|
196
|
+
token_to_use = use_token if use_token else self.token
|
|
197
|
+
response = await self._req_raw(endpoint, data, files, method, retries, use_token, **kwargs)
|
|
198
|
+
|
|
172
199
|
if response.status_code == 403:
|
|
173
200
|
error = parse_error_response(response)
|
|
174
201
|
if not token_to_use:
|
|
@@ -188,7 +215,9 @@ class TinyB:
|
|
|
188
215
|
if response.status_code == 599:
|
|
189
216
|
raise TimeoutException("timeout")
|
|
190
217
|
if "Content-Type" in response.headers and (
|
|
191
|
-
response.headers["Content-Type"] == "text/plain"
|
|
218
|
+
response.headers["Content-Type"] == "text/plain"
|
|
219
|
+
or "text/csv" in response.headers["Content-Type"]
|
|
220
|
+
or "ndjson" in response.headers["Content-Type"]
|
|
192
221
|
):
|
|
193
222
|
return response.content.decode("utf-8")
|
|
194
223
|
if response.status_code >= 400 and response.status_code not in [400, 403, 404, 409, 429]:
|
|
@@ -248,10 +277,10 @@ class TinyB:
|
|
|
248
277
|
url = url + "?" + scopes_url
|
|
249
278
|
return await self._req(url, method="PUT", data="")
|
|
250
279
|
|
|
251
|
-
async def datasources(self, branch: Optional[str] = None, used_by: bool = False):
|
|
252
|
-
params = {
|
|
253
|
-
|
|
254
|
-
|
|
280
|
+
async def datasources(self, branch: Optional[str] = None, used_by: bool = False) -> List[Dict[str, Any]]:
|
|
281
|
+
params = {}
|
|
282
|
+
if used_by:
|
|
283
|
+
params["attrs"] = "used_by"
|
|
255
284
|
response = await self._req(f"/v0/datasources?{urlencode(params)}")
|
|
256
285
|
ds = response["datasources"]
|
|
257
286
|
|
|
@@ -259,6 +288,25 @@ class TinyB:
|
|
|
259
288
|
ds = [x for x in ds if x["name"].startswith(branch)]
|
|
260
289
|
return ds
|
|
261
290
|
|
|
291
|
+
async def secrets(self) -> List[Dict[str, Any]]:
|
|
292
|
+
response = await self._req("/v0/variables")
|
|
293
|
+
return response["variables"]
|
|
294
|
+
|
|
295
|
+
async def get_secret(self, name: str) -> Optional[Dict[str, Any]]:
|
|
296
|
+
return await self._req(f"/v0/variables/{name}")
|
|
297
|
+
|
|
298
|
+
async def create_secret(self, name: str, value: str):
|
|
299
|
+
response = await self._req("/v0/variables", method="POST", data={"name": name, "value": value})
|
|
300
|
+
return response
|
|
301
|
+
|
|
302
|
+
async def update_secret(self, name: str, value: str):
|
|
303
|
+
response = await self._req(f"/v0/variables/{name}", method="PUT", data={"value": value})
|
|
304
|
+
return response
|
|
305
|
+
|
|
306
|
+
async def delete_secret(self, name: str):
|
|
307
|
+
response = await self._req(f"/v0/variables/{name}", method="DELETE")
|
|
308
|
+
return response
|
|
309
|
+
|
|
262
310
|
async def get_connections(self, service: Optional[str] = None):
|
|
263
311
|
params = {}
|
|
264
312
|
|
|
@@ -356,7 +404,7 @@ class TinyB:
|
|
|
356
404
|
return res
|
|
357
405
|
|
|
358
406
|
async def pipe_file(self, pipe: str):
|
|
359
|
-
return await self._req(f"/
|
|
407
|
+
return await self._req(f"/v1/pipes/{pipe}.pipe")
|
|
360
408
|
|
|
361
409
|
async def datasource_file(self, datasource: str):
|
|
362
410
|
try:
|
|
@@ -498,6 +546,9 @@ class TinyB:
|
|
|
498
546
|
payload = {"datasource_a": datasource_a, "datasource_b": datasource_b}
|
|
499
547
|
return await self._req("/v0/datasources/exchange", method="POST", data=payload)
|
|
500
548
|
|
|
549
|
+
async def datasource_events(self, datasource_name: str, data: str):
|
|
550
|
+
return await self._req(f"/v0/events?name={datasource_name}", method="POST", data=data)
|
|
551
|
+
|
|
501
552
|
async def analyze_pipe_node(
|
|
502
553
|
self, pipe_name: str, node: Dict[str, Any], dry_run: str = "false", datasource_name: Optional[str] = None
|
|
503
554
|
):
|
|
@@ -532,7 +583,7 @@ class TinyB:
|
|
|
532
583
|
)
|
|
533
584
|
return response
|
|
534
585
|
|
|
535
|
-
async def pipes(self, branch=None, dependencies: bool = False, node_attrs=None, attrs=None):
|
|
586
|
+
async def pipes(self, branch=None, dependencies: bool = False, node_attrs=None, attrs=None) -> List[Dict[str, Any]]:
|
|
536
587
|
params = {
|
|
537
588
|
"dependencies": "true" if dependencies else "false",
|
|
538
589
|
"attrs": attrs if attrs else "",
|
|
@@ -637,10 +688,12 @@ class TinyB:
|
|
|
637
688
|
async def pipe_unlink_materialized(self, pipe_name: str, node_id: str):
|
|
638
689
|
return await self._req(f"/v0/pipes/{pipe_name}/nodes/{node_id}/materialization", method="DELETE")
|
|
639
690
|
|
|
640
|
-
async def query(self, sql: str, pipeline: Optional[str] = None):
|
|
691
|
+
async def query(self, sql: str, pipeline: Optional[str] = None, playground: Optional[str] = None):
|
|
641
692
|
params = {}
|
|
642
693
|
if pipeline:
|
|
643
694
|
params = {"pipeline": pipeline}
|
|
695
|
+
if playground:
|
|
696
|
+
params = {"playground": playground}
|
|
644
697
|
params.update({"release_replacements": "true"})
|
|
645
698
|
|
|
646
699
|
if len(sql) > TinyB.MAX_GET_LENGTH:
|
|
@@ -661,19 +714,27 @@ class TinyB:
|
|
|
661
714
|
async def job_cancel(self, job_id: str):
|
|
662
715
|
return await self._req(f"/v0/jobs/{job_id}/cancel", method="POST", data=b"")
|
|
663
716
|
|
|
664
|
-
async def user_workspaces(self):
|
|
665
|
-
|
|
717
|
+
async def user_workspaces(self, version: str = "v0"):
|
|
718
|
+
data = await self._req(f"/{version}/user/workspaces/?with_environments=false")
|
|
719
|
+
# TODO: this is repeated in local_common.py but I'm avoiding circular imports
|
|
720
|
+
local_port = int(os.getenv("TB_LOCAL_PORT", 80))
|
|
721
|
+
local_host = f"http://localhost:{local_port}"
|
|
722
|
+
if local_host != self.host:
|
|
723
|
+
return data
|
|
724
|
+
|
|
725
|
+
local_workspaces = [x for x in data["workspaces"] if not x["name"].startswith("Tinybird_Local_")]
|
|
726
|
+
return {**data, "workspaces": local_workspaces}
|
|
666
727
|
|
|
667
|
-
async def user_workspaces_and_branches(self):
|
|
668
|
-
return await self._req("/
|
|
728
|
+
async def user_workspaces_and_branches(self, version: str = "v0"):
|
|
729
|
+
return await self._req(f"/{version}/user/workspaces/?with_environments=true")
|
|
669
730
|
|
|
670
|
-
async def user_workspaces_with_organization(self):
|
|
731
|
+
async def user_workspaces_with_organization(self, version: str = "v0"):
|
|
671
732
|
return await self._req(
|
|
672
|
-
"/
|
|
733
|
+
f"/{version}/user/workspaces/?with_environments=false&with_organization=true&with_members_and_owner=false"
|
|
673
734
|
)
|
|
674
735
|
|
|
675
|
-
async def user_workspace_branches(self):
|
|
676
|
-
return await self._req("/
|
|
736
|
+
async def user_workspace_branches(self, version: str = "v0"):
|
|
737
|
+
return await self._req(f"/{version}/user/workspaces/?with_environments=true&only_environments=true")
|
|
677
738
|
|
|
678
739
|
async def branches(self):
|
|
679
740
|
return await self._req("/v0/environments")
|
|
@@ -681,12 +742,18 @@ class TinyB:
|
|
|
681
742
|
async def releases(self, workspace_id):
|
|
682
743
|
return await self._req(f"/v0/workspaces/{workspace_id}/releases")
|
|
683
744
|
|
|
684
|
-
async def create_workspace(
|
|
685
|
-
|
|
745
|
+
async def create_workspace(
|
|
746
|
+
self,
|
|
747
|
+
name: str,
|
|
748
|
+
template: Optional[str],
|
|
749
|
+
assign_to_organization_id: Optional[str] = None,
|
|
750
|
+
version: str = "v0",
|
|
751
|
+
):
|
|
752
|
+
url = f"/{version}/workspaces?name={name}"
|
|
686
753
|
if template:
|
|
687
754
|
url += f"&starter_kit={template}"
|
|
688
|
-
if
|
|
689
|
-
url += f"&assign_to_organization_id={
|
|
755
|
+
if assign_to_organization_id:
|
|
756
|
+
url += f"&assign_to_organization_id={assign_to_organization_id}"
|
|
690
757
|
return await self._req(url, method="POST", data=b"")
|
|
691
758
|
|
|
692
759
|
async def create_workspace_branch(
|
|
@@ -785,9 +852,9 @@ class TinyB:
|
|
|
785
852
|
url = f"/v0/environments/{branch_id}/regression"
|
|
786
853
|
return await self._req(url, method="POST", data=data, headers={"Content-Type": "application/json"})
|
|
787
854
|
|
|
788
|
-
async def delete_workspace(self, id: str, hard_delete_confirmation: Optional[str]):
|
|
855
|
+
async def delete_workspace(self, id: str, hard_delete_confirmation: Optional[str], version: str = "v0"):
|
|
789
856
|
data = {"confirmation": hard_delete_confirmation}
|
|
790
|
-
return await self._req(f"/
|
|
857
|
+
return await self._req(f"/{version}/workspaces/{id}", data, method="DELETE")
|
|
791
858
|
|
|
792
859
|
async def delete_branch(self, id: str):
|
|
793
860
|
return await self._req(f"/v0/environments/{id}", method="DELETE")
|
|
@@ -818,12 +885,53 @@ class TinyB:
|
|
|
818
885
|
params = {"with_token": "true" if with_token else "false"}
|
|
819
886
|
return await self._req(f"/v0/workspaces/{workspace_id}?{urlencode(params)}")
|
|
820
887
|
|
|
821
|
-
async def workspace_info(self):
|
|
822
|
-
return await self._req("/
|
|
888
|
+
async def workspace_info(self, version: str = "v0") -> Dict[str, Any]:
|
|
889
|
+
return await self._req(f"/{version}/workspace")
|
|
823
890
|
|
|
824
891
|
async def organization(self, organization_id: str):
|
|
825
892
|
return await self._req(f"/v0/organizations/{organization_id}")
|
|
826
893
|
|
|
894
|
+
async def create_organization(
|
|
895
|
+
self,
|
|
896
|
+
name: str,
|
|
897
|
+
):
|
|
898
|
+
url = f"/v0/organizations?name={name}"
|
|
899
|
+
return await self._req(url, method="POST", data=b"")
|
|
900
|
+
|
|
901
|
+
async def add_workspaces_to_organization(self, organization_id: str, workspace_ids: List[str]):
|
|
902
|
+
if not workspace_ids:
|
|
903
|
+
return
|
|
904
|
+
return await self._req(
|
|
905
|
+
f"/v0/organizations/{organization_id}/workspaces",
|
|
906
|
+
method="PUT",
|
|
907
|
+
data=json.dumps({"workspace_ids": ",".join(workspace_ids)}),
|
|
908
|
+
)
|
|
909
|
+
|
|
910
|
+
async def infra_create(self, organization_id: str, name: str, host: str) -> Dict[str, Any]:
|
|
911
|
+
params = {
|
|
912
|
+
"organization_id": organization_id,
|
|
913
|
+
"name": name,
|
|
914
|
+
"host": host,
|
|
915
|
+
}
|
|
916
|
+
return await self._req(f"/v1/infra?{urlencode(params)}", method="POST")
|
|
917
|
+
|
|
918
|
+
async def infra_update(self, infra_id: str, organization_id: str, name: str, host: str) -> Dict[str, Any]:
|
|
919
|
+
params = {
|
|
920
|
+
"organization_id": organization_id,
|
|
921
|
+
}
|
|
922
|
+
if name:
|
|
923
|
+
params["name"] = name
|
|
924
|
+
if host:
|
|
925
|
+
params["host"] = host
|
|
926
|
+
return await self._req(f"/v1/infra/{infra_id}?{urlencode(params)}", method="PUT")
|
|
927
|
+
|
|
928
|
+
async def infra_list(self, organization_id: str) -> List[Dict[str, Any]]:
|
|
929
|
+
data = await self._req(f"/v1/infra?organization_id={organization_id}")
|
|
930
|
+
return data.get("infras", [])
|
|
931
|
+
|
|
932
|
+
async def infra_delete(self, infra_id: str, organization_id: str) -> Dict[str, Any]:
|
|
933
|
+
return await self._req(f"/v1/infra/{infra_id}?organization_id={organization_id}", method="DELETE")
|
|
934
|
+
|
|
827
935
|
async def wait_for_job(
|
|
828
936
|
self,
|
|
829
937
|
job_id: str,
|
|
@@ -70,14 +70,33 @@ LEGACY_HOSTS = {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
|
|
73
|
-
|
|
73
|
+
CLOUD_HOSTS = {
|
|
74
|
+
"https://api.tinybird.co": "https://cloud.tinybird.co/gcp/europe-west3",
|
|
75
|
+
"https://api.us-east.tinybird.co": "https://cloud.tinybird.co/gcp/us-east4",
|
|
76
|
+
"https://api.us-east.aws.tinybird.co": "https://cloud.tinybird.co/aws/us-east-1",
|
|
77
|
+
"https://api.us-west-2.aws.tinybird.co": "https://cloud.tinybird.co/aws/us-west-2",
|
|
78
|
+
"https://api.eu-central-1.aws.tinybird.co": "https://cloud.tinybird.co/aws/eu-central-1",
|
|
79
|
+
"https://api.eu-west-1.aws.tinybird.co": "https://cloud.tinybird.co/aws/eu-west-1",
|
|
80
|
+
"https://api.europe-west2.gcp.tinybird.co": "https://cloud.tinybird.co/gcp/europe-west2",
|
|
81
|
+
"https://api.ap-east.aws.tinybird.co": "https://cloud.tinybird.co/aws/ap-east",
|
|
82
|
+
"https://ui.tinybird.co": "https://cloud.tinybird.co/gcp/europe-west3",
|
|
83
|
+
"https://ui.us-east.tinybird.co": "https://cloud.tinybird.co/gcp/us-east4",
|
|
84
|
+
"https://ui.us-east.aws.tinybird.co": "https://cloud.tinybird.co/aws/us-east-1",
|
|
85
|
+
"https://ui.us-west-2.aws.tinybird.co": "https://cloud.tinybird.co/aws/us-west-2",
|
|
86
|
+
"https://ui.eu-central-1.aws.tinybird.co": "https://cloud.tinybird.co/aws/eu-central-1",
|
|
87
|
+
"https://ui.europe-west2.gcp.tinybird.co": "https://cloud.tinybird.co/gcp/europe-west2",
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
async def get_config(
|
|
92
|
+
host: str, token: Optional[str], semver: Optional[str] = None, config_file: Optional[str] = None
|
|
93
|
+
) -> Dict[str, Any]:
|
|
74
94
|
if host:
|
|
75
95
|
host = host.rstrip("/")
|
|
76
96
|
|
|
77
|
-
config_file = Path(getcwd()) / ".tinyb"
|
|
78
97
|
config = {}
|
|
79
98
|
try:
|
|
80
|
-
async with aiofiles.open(config_file) as file:
|
|
99
|
+
async with aiofiles.open(config_file or Path(getcwd()) / ".tinyb") as file:
|
|
81
100
|
res = await file.read()
|
|
82
101
|
config = json.loads(res)
|
|
83
102
|
except OSError:
|
|
@@ -91,6 +110,7 @@ async def get_config(host: str, token: Optional[str], semver: Optional[str] = No
|
|
|
91
110
|
config["semver"] = semver or config.get("semver", None)
|
|
92
111
|
config["host"] = host or config.get("host", DEFAULT_API_HOST)
|
|
93
112
|
config["workspaces"] = config.get("workspaces", [])
|
|
113
|
+
config["cwd"] = config.get("cwd", getcwd())
|
|
94
114
|
return config
|
|
95
115
|
|
|
96
116
|
|
|
@@ -104,6 +124,14 @@ def get_display_host(ui_host: str):
|
|
|
104
124
|
return LEGACY_HOSTS.get(ui_host, ui_host)
|
|
105
125
|
|
|
106
126
|
|
|
127
|
+
def get_display_cloud_host(api_host: str) -> str:
|
|
128
|
+
is_local = "localhost" in api_host
|
|
129
|
+
if is_local:
|
|
130
|
+
port = api_host.split(":")[-1]
|
|
131
|
+
return f"http://cloud.tinybird.co/local/{port}"
|
|
132
|
+
return CLOUD_HOSTS.get(api_host, api_host)
|
|
133
|
+
|
|
134
|
+
|
|
107
135
|
class FeatureFlags:
|
|
108
136
|
@classmethod
|
|
109
137
|
def ignore_sql_errors(cls) -> bool: # Context: #1155
|
|
@@ -1174,6 +1174,8 @@ def parse(
|
|
|
1174
1174
|
else:
|
|
1175
1175
|
doc.filtering_tags += filtering_tags
|
|
1176
1176
|
|
|
1177
|
+
# WARNING! This code is duplicate for Tinybird Forward in tinybird/tb/modules/datafile/common.py
|
|
1178
|
+
# If you make any changes here, it's very likely that you need to also change the other module. If in doubt, ask
|
|
1177
1179
|
cmds = {
|
|
1178
1180
|
"from": assign("from"),
|
|
1179
1181
|
"source": sources,
|
|
@@ -5,11 +5,14 @@ from decimal import Decimal
|
|
|
5
5
|
from typing import Callable, Dict, List, Optional, Tuple
|
|
6
6
|
|
|
7
7
|
datetime64_patterns = [
|
|
8
|
-
r"\d\d\d\d.\d\d.\d\d(T|\s)\d\d:\d\d:\d\d.\d\d\d",
|
|
9
|
-
r"\d\d.\d\d.\d\d\d\d.\d{1,2}:\d{1,2}:\d{1,2}.\d{1,3}",
|
|
8
|
+
re.compile(r"\d\d\d\d.\d\d.\d\d(T|\s)\d\d:\d\d:\d\d.\d\d\d"),
|
|
9
|
+
re.compile(r"\d\d.\d\d.\d\d\d\d.\d{1,2}:\d{1,2}:\d{1,2}.\d{1,3}"),
|
|
10
10
|
]
|
|
11
11
|
|
|
12
|
-
datetime_patterns = [
|
|
12
|
+
datetime_patterns = [
|
|
13
|
+
re.compile(r"\d\d\d\d.\d\d.\d\d(T|\s)\d\d:\d\d:\d\d"),
|
|
14
|
+
re.compile(r"\d\d.\d\d.\d\d\d\d.\d{1,2}:\d{1,2}:\d{1,2}"),
|
|
15
|
+
]
|
|
13
16
|
|
|
14
17
|
int_8_max = 2**7
|
|
15
18
|
int16_max = 2**15
|
|
@@ -23,13 +26,15 @@ uint32_max = 2**32
|
|
|
23
26
|
uint64_max = 2**64
|
|
24
27
|
uint128_max = 2**128
|
|
25
28
|
uint256_max = 2**256
|
|
26
|
-
intx_re = r"^[+-]?\d+$"
|
|
27
|
-
uintx_re = r"^\d+$"
|
|
29
|
+
intx_re = re.compile(r"^[+-]?\d+$")
|
|
30
|
+
uintx_re = re.compile(r"^\d+$")
|
|
28
31
|
float32_max = 2**23 # 23 bits is the fractional part of float 32 ieee754
|
|
29
32
|
float64_max = 2**52 # 51 bits is the fractional part of float 64 ieee754
|
|
30
33
|
|
|
31
|
-
|
|
32
|
-
|
|
34
|
+
date_pattern = re.compile(r"\d\d\d\d-\d\d-\d\d$")
|
|
35
|
+
|
|
36
|
+
datetime64_type_pattern = re.compile(r"^DateTime64(\([1-9](, ?'.+')?\))?$")
|
|
37
|
+
datetime_type_pattern = re.compile(r"^DateTime(\(('.+')?)?\)?$")
|
|
33
38
|
|
|
34
39
|
# List from https://github.com/tinybirdco/ClickHousePrivate/blob/153473d9c1c871974688a1d72dcff7a13fc2076c/src/DataTypes/Serializations/SerializationBool.cpp#L216
|
|
35
40
|
bool_allowed_values = {
|
|
@@ -71,7 +76,7 @@ def is_type_datetime64(type_to_check: str) -> bool:
|
|
|
71
76
|
>>> is_type_datetime64("datetime64")
|
|
72
77
|
False
|
|
73
78
|
"""
|
|
74
|
-
return
|
|
79
|
+
return datetime64_type_pattern.match(type_to_check) is not None
|
|
75
80
|
|
|
76
81
|
|
|
77
82
|
def is_type_datetime(type_to_check: str) -> bool:
|
|
@@ -87,7 +92,7 @@ def is_type_datetime(type_to_check: str) -> bool:
|
|
|
87
92
|
>>> is_type_datetime("datetime")
|
|
88
93
|
False
|
|
89
94
|
"""
|
|
90
|
-
return
|
|
95
|
+
return datetime_type_pattern.match(type_to_check) is not None
|
|
91
96
|
|
|
92
97
|
|
|
93
98
|
def string_test(x: str) -> bool:
|
|
@@ -95,63 +100,63 @@ def string_test(x: str) -> bool:
|
|
|
95
100
|
|
|
96
101
|
|
|
97
102
|
def date_test(x: str) -> bool:
|
|
98
|
-
return
|
|
103
|
+
return date_pattern.match(x) is not None
|
|
99
104
|
|
|
100
105
|
|
|
101
106
|
def datetime64_test(x: str) -> bool:
|
|
102
|
-
return any([
|
|
107
|
+
return any([p.match(x) for p in datetime64_patterns])
|
|
103
108
|
|
|
104
109
|
|
|
105
110
|
def datetime_test(x: str) -> bool:
|
|
106
|
-
return any([
|
|
111
|
+
return any([p.match(x) for p in datetime_patterns])
|
|
107
112
|
|
|
108
113
|
|
|
109
114
|
def int_8_test(x: str) -> bool:
|
|
110
|
-
return
|
|
115
|
+
return intx_re.match(x) is not None and -int_8_max <= int(x) < int_8_max
|
|
111
116
|
|
|
112
117
|
|
|
113
118
|
def int16_test(x: str) -> bool:
|
|
114
|
-
return
|
|
119
|
+
return intx_re.match(x) is not None and -int16_max <= int(x) < int16_max
|
|
115
120
|
|
|
116
121
|
|
|
117
122
|
def int32_test(x: str) -> bool:
|
|
118
|
-
return
|
|
123
|
+
return intx_re.match(x) is not None and -int32_max <= int(x) < int32_max
|
|
119
124
|
|
|
120
125
|
|
|
121
126
|
def int64_test(x: str) -> bool:
|
|
122
|
-
return
|
|
127
|
+
return intx_re.match(x) is not None and -int64_max <= int(x) < int64_max
|
|
123
128
|
|
|
124
129
|
|
|
125
130
|
def int128_test(x: str) -> bool:
|
|
126
|
-
return
|
|
131
|
+
return intx_re.match(x) is not None and -int128_max <= int(x) < int128_max
|
|
127
132
|
|
|
128
133
|
|
|
129
134
|
def int256_test(x: str) -> bool:
|
|
130
|
-
return
|
|
135
|
+
return intx_re.match(x) is not None and -int256_max <= int(x) < int256_max
|
|
131
136
|
|
|
132
137
|
|
|
133
138
|
def uint_8_test(x: str) -> bool:
|
|
134
|
-
return
|
|
139
|
+
return uintx_re.match(x) is not None and 0 <= int(x) < uint_8_max
|
|
135
140
|
|
|
136
141
|
|
|
137
142
|
def uint16_test(x: str) -> bool:
|
|
138
|
-
return
|
|
143
|
+
return uintx_re.match(x) is not None and 0 <= int(x) < uint16_max
|
|
139
144
|
|
|
140
145
|
|
|
141
146
|
def uint32_test(x: str) -> bool:
|
|
142
|
-
return
|
|
147
|
+
return uintx_re.match(x) is not None and 0 <= int(x) < uint32_max
|
|
143
148
|
|
|
144
149
|
|
|
145
150
|
def uint64_test(x: str) -> bool:
|
|
146
|
-
return
|
|
151
|
+
return uintx_re.match(x) is not None and 0 <= int(x) < uint64_max
|
|
147
152
|
|
|
148
153
|
|
|
149
154
|
def uint128_test(x: str) -> bool:
|
|
150
|
-
return
|
|
155
|
+
return intx_re.match(x) is not None and 0 <= int(x) < uint128_max
|
|
151
156
|
|
|
152
157
|
|
|
153
158
|
def uint256_test(x: str) -> bool:
|
|
154
|
-
return
|
|
159
|
+
return intx_re.match(x) is not None and 0 <= int(x) < uint256_max
|
|
155
160
|
|
|
156
161
|
|
|
157
162
|
def float_test(x: str) -> bool:
|
|
@@ -910,6 +910,9 @@ Ready? """
|
|
|
910
910
|
success_delete_rows_datasource = success_message(
|
|
911
911
|
"** Data Source '{datasource}' rows deleted matching condition \"{delete_condition}\""
|
|
912
912
|
)
|
|
913
|
+
success_delete_rows_datasource_no_rows = success_message(
|
|
914
|
+
"** Data Source '{datasource}' no rows to delete matching condition \"{delete_condition}\""
|
|
915
|
+
)
|
|
913
916
|
success_dry_run_delete_rows_datasource = success_message(
|
|
914
917
|
"** [DRY RUN] Data Source '{datasource}' rows '{rows}' matching condition \"{delete_condition}\" to be deleted"
|
|
915
918
|
)
|
|
@@ -9,6 +9,24 @@ valid_chars_name: str = string.ascii_letters + string.digits + "._`*<>+-'"
|
|
|
9
9
|
valid_chars_fn: str = valid_chars_name + "[](),=!?:/ \n\t\r"
|
|
10
10
|
|
|
11
11
|
INDEX_WHITELIST = ["minmax", "set", "bloom_filter", "ngrambf_v1", "tokenbf_v1"]
|
|
12
|
+
INDEX_SUPPORTED_TYPES = {
|
|
13
|
+
"bloom_filter": [
|
|
14
|
+
"Int*",
|
|
15
|
+
"UInt*",
|
|
16
|
+
"Float*",
|
|
17
|
+
"Enum",
|
|
18
|
+
"Date",
|
|
19
|
+
"DateTime",
|
|
20
|
+
"String",
|
|
21
|
+
"FixedString",
|
|
22
|
+
"Array",
|
|
23
|
+
"LowCardinality",
|
|
24
|
+
"Nullable",
|
|
25
|
+
"UUID",
|
|
26
|
+
"Map",
|
|
27
|
+
],
|
|
28
|
+
"ngrambf_v1": ["String", "FixedString", "Map"],
|
|
29
|
+
}
|
|
12
30
|
|
|
13
31
|
|
|
14
32
|
@dataclass
|
|
@@ -39,12 +57,37 @@ class TableIndex:
|
|
|
39
57
|
def clear_index_sql(self):
|
|
40
58
|
return f"CLEAR INDEX IF EXISTS {self.name}"
|
|
41
59
|
|
|
42
|
-
def
|
|
60
|
+
def _validate_index_type(self, table_structure: List[Dict[str, Any]]):
|
|
61
|
+
for col in table_structure:
|
|
62
|
+
if col["normalized_name"] != self.expr:
|
|
63
|
+
continue
|
|
64
|
+
col_type = col["type"]
|
|
65
|
+
index_supported_types: Optional[str] = next((t for t in INDEX_SUPPORTED_TYPES if t in self.type_full), None)
|
|
66
|
+
|
|
67
|
+
if index_supported_types:
|
|
68
|
+
for supported_type in INDEX_SUPPORTED_TYPES.get(index_supported_types, []):
|
|
69
|
+
# Convert supported type to regex pattern
|
|
70
|
+
# Replace * with \d+ to match any number
|
|
71
|
+
pattern = supported_type.replace("*", r"\d+")
|
|
72
|
+
if re.match(f"^{pattern}$", col_type):
|
|
73
|
+
return
|
|
74
|
+
raise ValueError(
|
|
75
|
+
f"Not allowed data type '{col_type}' for index '{self.type_full}' for column '{self.expr}' "
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
def validate_allowed(self, table_structure: Optional[List[Dict[str, Any]]] = None):
|
|
43
79
|
"""
|
|
44
80
|
Validate at API level not to depend on CLI version
|
|
45
81
|
"""
|
|
46
82
|
if not any(index in self.type_full for index in INDEX_WHITELIST):
|
|
47
83
|
raise ValueError(f"Not allowed index '{self.type_full}'")
|
|
84
|
+
try:
|
|
85
|
+
if table_structure:
|
|
86
|
+
self._validate_index_type(table_structure)
|
|
87
|
+
except ValueError as e:
|
|
88
|
+
raise e
|
|
89
|
+
except Exception:
|
|
90
|
+
logging.exception(f"Error validating index '{self.type_full}' for column '{self.expr}'")
|
|
48
91
|
|
|
49
92
|
|
|
50
93
|
@dataclass
|
|
@@ -174,7 +217,7 @@ def try_to_fix_nullable_in_simple_aggregating_function(t: str) -> Optional[str]:
|
|
|
174
217
|
return result
|
|
175
218
|
|
|
176
219
|
|
|
177
|
-
def schema_to_sql_columns(schema: List[Dict[str, Any]]) -> List[str]:
|
|
220
|
+
def schema_to_sql_columns(schema: List[Dict[str, Any]], skip_jsonpaths: bool = False) -> List[str]:
|
|
178
221
|
"""return an array with each column in SQL
|
|
179
222
|
>>> schema_to_sql_columns([{'name': 'temperature', 'type': 'Float32', 'codec': None, 'default_value': None, 'nullable': False, 'normalized_name': 'temperature'}, {'name': 'temperature_delta', 'type': 'Float32', 'codec': 'CODEC(Delta(4), LZ4))', 'default_value': 'MATERIALIZED temperature', 'nullable': False, 'normalized_name': 'temperature_delta'}])
|
|
180
223
|
['`temperature` Float32', '`temperature_delta` Float32 MATERIALIZED temperature CODEC(Delta(4), LZ4))']
|
|
@@ -198,7 +241,7 @@ def schema_to_sql_columns(schema: List[Dict[str, Any]]) -> List[str]:
|
|
|
198
241
|
else:
|
|
199
242
|
_type = x["type"]
|
|
200
243
|
parts = [col_name(name, backquotes=True), _type]
|
|
201
|
-
if x.get("jsonpath", None):
|
|
244
|
+
if x.get("jsonpath", None) and not skip_jsonpaths:
|
|
202
245
|
parts.append(f"`json:{x['jsonpath']}`")
|
|
203
246
|
if "default_value" in x and x["default_value"] not in ("", None):
|
|
204
247
|
parts.append(x["default_value"])
|
|
@@ -2399,3 +2399,106 @@ def extract_variables_from_sql(sql: str, params: List[Dict[str, Any]]) -> Dict[s
|
|
|
2399
2399
|
return {}
|
|
2400
2400
|
|
|
2401
2401
|
return defaults
|
|
2402
|
+
|
|
2403
|
+
|
|
2404
|
+
def render_template_with_secrets(name: str, content: str, secrets: Optional[Dict[str, str]] = None) -> str:
|
|
2405
|
+
"""Renders a template with secrets, allowing for default values.
|
|
2406
|
+
|
|
2407
|
+
Args:
|
|
2408
|
+
name: The name of the template
|
|
2409
|
+
content: The template content
|
|
2410
|
+
secrets: A dictionary mapping secret names to their values
|
|
2411
|
+
|
|
2412
|
+
Returns:
|
|
2413
|
+
The rendered template
|
|
2414
|
+
|
|
2415
|
+
Examples:
|
|
2416
|
+
>>> render_template_with_secrets(
|
|
2417
|
+
... "my_kafka_connection",
|
|
2418
|
+
... "KAFKA_BOOTSTRAP_SERVERS {{ tb_secret('PRODUCTION_KAFKA_SERVERS', 'localhost:9092') }}",
|
|
2419
|
+
... secrets = {'PRODUCTION_KAFKA_SERVERS': 'server1:9092,server2:9092'}
|
|
2420
|
+
... )
|
|
2421
|
+
'KAFKA_BOOTSTRAP_SERVERS server1:9092,server2:9092'
|
|
2422
|
+
|
|
2423
|
+
>>> render_template_with_secrets(
|
|
2424
|
+
... "my_kafka_connection",
|
|
2425
|
+
... "KAFKA_BOOTSTRAP_SERVERS {{ tb_secret('MISSING_SECRET', 'localhost:9092') }}",
|
|
2426
|
+
... secrets = {}
|
|
2427
|
+
... )
|
|
2428
|
+
'KAFKA_BOOTSTRAP_SERVERS localhost:9092'
|
|
2429
|
+
|
|
2430
|
+
>>> render_template_with_secrets(
|
|
2431
|
+
... "my_kafka_connection",
|
|
2432
|
+
... "KAFKA_BOOTSTRAP_SERVERS {{ tb_secret('MISSING_SECRET', '') }}",
|
|
2433
|
+
... secrets = {}
|
|
2434
|
+
... )
|
|
2435
|
+
'KAFKA_BOOTSTRAP_SERVERS ""'
|
|
2436
|
+
|
|
2437
|
+
>>> render_template_with_secrets(
|
|
2438
|
+
... "my_kafka_connection",
|
|
2439
|
+
... "KAFKA_BOOTSTRAP_SERVERS {{ tb_secret('MISSING_SECRET', 0) }}",
|
|
2440
|
+
... secrets = {}
|
|
2441
|
+
... )
|
|
2442
|
+
'KAFKA_BOOTSTRAP_SERVERS 0'
|
|
2443
|
+
|
|
2444
|
+
>>> render_template_with_secrets(
|
|
2445
|
+
... "my_kafka_connection",
|
|
2446
|
+
... "KAFKA_BOOTSTRAP_SERVERS {{ tb_secret('MISSING_SECRET') }}",
|
|
2447
|
+
... secrets = {}
|
|
2448
|
+
... )
|
|
2449
|
+
Traceback (most recent call last):
|
|
2450
|
+
...
|
|
2451
|
+
tinybird.sql_template.SQLTemplateException: Template Syntax Error: Cannot access secret 'MISSING_SECRET'. Check the secret exists in the Workspace and the token has the required scope.
|
|
2452
|
+
"""
|
|
2453
|
+
if not secrets:
|
|
2454
|
+
secrets = {}
|
|
2455
|
+
|
|
2456
|
+
def tb_secret(secret_name: str, default: Optional[str] = None) -> str:
|
|
2457
|
+
"""Get a secret value with an optional default.
|
|
2458
|
+
|
|
2459
|
+
Args:
|
|
2460
|
+
secret_name: The name of the secret to retrieve
|
|
2461
|
+
default: The default value to use if the secret is not found
|
|
2462
|
+
|
|
2463
|
+
Returns:
|
|
2464
|
+
The secret value or default
|
|
2465
|
+
|
|
2466
|
+
Raises:
|
|
2467
|
+
SQLTemplateException: If the secret is not found and no default is provided
|
|
2468
|
+
"""
|
|
2469
|
+
if secret_name in secrets:
|
|
2470
|
+
value = secrets[secret_name]
|
|
2471
|
+
if isinstance(value, str) and len(value) == 0:
|
|
2472
|
+
return '""'
|
|
2473
|
+
return value
|
|
2474
|
+
elif default is not None:
|
|
2475
|
+
if isinstance(default, str) and len(default) == 0:
|
|
2476
|
+
return '""'
|
|
2477
|
+
return default
|
|
2478
|
+
else:
|
|
2479
|
+
raise SQLTemplateException(
|
|
2480
|
+
f"Cannot access secret '{secret_name}'. Check the secret exists in the Workspace and the token has the required scope."
|
|
2481
|
+
)
|
|
2482
|
+
|
|
2483
|
+
# Create the template
|
|
2484
|
+
t = Template(content, name=name, autoescape=None)
|
|
2485
|
+
|
|
2486
|
+
try:
|
|
2487
|
+
# Create namespace with our tb_secret function
|
|
2488
|
+
namespace = {"tb_secret": tb_secret}
|
|
2489
|
+
|
|
2490
|
+
# Generate the template without all the extra processing
|
|
2491
|
+
# This directly uses the underlying _generate method of the Template class
|
|
2492
|
+
result = t.generate(**namespace)
|
|
2493
|
+
|
|
2494
|
+
# Convert the result to string
|
|
2495
|
+
if isinstance(result, bytes):
|
|
2496
|
+
return result.decode("utf-8")
|
|
2497
|
+
|
|
2498
|
+
return str(result)
|
|
2499
|
+
except SQLTemplateCustomError as e:
|
|
2500
|
+
raise e
|
|
2501
|
+
except SQLTemplateException as e:
|
|
2502
|
+
raise e
|
|
2503
|
+
except Exception as e:
|
|
2504
|
+
raise SQLTemplateException(f"Error rendering template with secrets: {str(e)}")
|
|
@@ -637,7 +637,7 @@ async def check_user_token(ctx: Context, token: str):
|
|
|
637
637
|
if not is_authenticated.get("is_valid", False):
|
|
638
638
|
raise CLIWorkspaceException(
|
|
639
639
|
FeedbackManager.error_exception(
|
|
640
|
-
error='Invalid token.
|
|
640
|
+
error='Invalid token. Make sure you are using the "user token" instead of the "admin your@email" token.'
|
|
641
641
|
)
|
|
642
642
|
)
|
|
643
643
|
if is_authenticated.get("is_valid") and not is_authenticated.get("is_user", False):
|
|
@@ -466,6 +466,13 @@ async def datasource_delete_rows(ctx, datasource_name, sql_condition, yes, wait,
|
|
|
466
466
|
)
|
|
467
467
|
)
|
|
468
468
|
return
|
|
469
|
+
if res is None: # 204,205
|
|
470
|
+
click.echo(
|
|
471
|
+
FeedbackManager.success_delete_rows_datasource_no_rows(
|
|
472
|
+
datasource=datasource_name, delete_condition=sql_condition
|
|
473
|
+
)
|
|
474
|
+
)
|
|
475
|
+
return
|
|
469
476
|
job_id = res["job_id"]
|
|
470
477
|
job_url = res["job_url"]
|
|
471
478
|
click.echo(FeedbackManager.info_datasource_delete_rows_job_url(url=job_url))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: tinybird_cli
|
|
3
|
-
Version: 5.18.1.
|
|
3
|
+
Version: 5.18.1.dev2
|
|
4
4
|
Summary: Tinybird Command Line Tool
|
|
5
5
|
Home-page: https://www.tinybird.co/docs/cli
|
|
6
6
|
Author: Tinybird
|
|
@@ -61,6 +61,11 @@ The Tinybird command-line tool allows you to use all the Tinybird functionality
|
|
|
61
61
|
Changelog
|
|
62
62
|
----------
|
|
63
63
|
|
|
64
|
+
5.18.1.dev1
|
|
65
|
+
***********
|
|
66
|
+
|
|
67
|
+
- `Added` support for changes in `/v0/datasources/(.+)/delete`.
|
|
68
|
+
|
|
64
69
|
5.18.0
|
|
65
70
|
***********
|
|
66
71
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/tb_cli_modules/tinyunit/tinyunit.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/tb_cli_modules/workspace_members.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird_cli.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird_cli.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|