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.
Files changed (48) hide show
  1. {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/PKG-INFO +6 -1
  2. {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/__cli__.py +2 -2
  3. {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/ch_utils/engine.py +14 -10
  4. {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/check_pypi.py +1 -1
  5. {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/client.py +135 -27
  6. {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/config.py +31 -3
  7. {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/datafile.py +2 -0
  8. {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/datatypes.py +29 -24
  9. {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/feedback_manager.py +3 -0
  10. {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/sql.py +46 -3
  11. {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/sql_template.py +103 -0
  12. {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/tb_cli_modules/common.py +1 -1
  13. {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/tb_cli_modules/datasource.py +7 -0
  14. {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird_cli.egg-info/PKG-INFO +6 -1
  15. {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird_cli.egg-info/top_level.txt +1 -0
  16. {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/setup.cfg +0 -0
  17. {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/ch_utils/constants.py +0 -0
  18. {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/connectors.py +0 -0
  19. {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/context.py +0 -0
  20. {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/git_settings.py +0 -0
  21. {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/sql_template_fmt.py +0 -0
  22. {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/sql_toolset.py +0 -0
  23. {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/syncasync.py +0 -0
  24. {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/tb_cli.py +0 -0
  25. {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/tb_cli_modules/auth.py +0 -0
  26. {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/tb_cli_modules/branch.py +0 -0
  27. {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/tb_cli_modules/cicd.py +0 -0
  28. {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/tb_cli_modules/cli.py +0 -0
  29. {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/tb_cli_modules/config.py +0 -0
  30. {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/tb_cli_modules/connection.py +0 -0
  31. {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/tb_cli_modules/exceptions.py +0 -0
  32. {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/tb_cli_modules/fmt.py +0 -0
  33. {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/tb_cli_modules/job.py +0 -0
  34. {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/tb_cli_modules/pipe.py +0 -0
  35. {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/tb_cli_modules/regions.py +0 -0
  36. {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/tb_cli_modules/tag.py +0 -0
  37. {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/tb_cli_modules/telemetry.py +0 -0
  38. {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/tb_cli_modules/test.py +0 -0
  39. {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/tb_cli_modules/tinyunit/tinyunit.py +0 -0
  40. {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/tb_cli_modules/tinyunit/tinyunit_lib.py +0 -0
  41. {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/tb_cli_modules/token.py +0 -0
  42. {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/tb_cli_modules/workspace.py +0 -0
  43. {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/tb_cli_modules/workspace_members.py +0 -0
  44. {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird/tornado_template.py +0 -0
  45. {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird_cli.egg-info/SOURCES.txt +0 -0
  46. {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird_cli.egg-info/dependency_links.txt +0 -0
  47. {tinybird_cli-5.18.1.dev0 → tinybird_cli-5.18.1.dev2}/tinybird_cli.egg-info/entry_points.txt +0 -0
  48. {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.dev0
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.dev0'
8
- __revision__ = '18f745e'
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=REPLACINGMERGETREE_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
- Traceback (most recent call last):
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
- ttl_array = engine_full.split(" TTL ")
814
- if len(ttl_array) <= 1:
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-cli/json"
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 _req(
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" or "text/csv" in response.headers["Content-Type"]
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
- "attrs": "used_by" if used_by else "",
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"/v0/pipes/{pipe}.pipe")
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
- return await self._req("/v0/user/workspaces/?with_environments=false")
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("/v0/user/workspaces/?with_environments=true")
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
- "/v0/user/workspaces/?with_environments=false&with_organization=true&with_members_and_owner=false"
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("/v0/user/workspaces/?with_environments=true&only_environments=true")
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(self, name: str, template: Optional[str], organization_id: Optional[str]):
685
- url = f"/v0/workspaces?name={name}"
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 organization_id:
689
- url += f"&assign_to_organization_id={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"/v0/workspaces/{id}", data, method="DELETE")
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("/v0/workspace")
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
- async def get_config(host: str, token: Optional[str], semver: Optional[str] = None) -> Dict[str, Any]:
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 = [r"\d\d\d\d.\d\d.\d\d(T|\s)\d\d:\d\d:\d\d", r"\d\d.\d\d.\d\d\d\d.\d{1,2}:\d{1,2}:\d{1,2}"]
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
- datetime64_type_pattern = r"^DateTime64(\([1-9](, ?'.+')?\))?$"
32
- datetime_type_pattern = r"^DateTime(\(('.+')?)?\)?$"
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 re.match(datetime64_type_pattern, type_to_check) is not None
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 re.match(datetime_type_pattern, type_to_check) is not None
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 re.match(r"\d\d\d\d-\d\d-\d\d$", x) is not None
103
+ return date_pattern.match(x) is not None
99
104
 
100
105
 
101
106
  def datetime64_test(x: str) -> bool:
102
- return any([re.match(p, x) for p in datetime64_patterns])
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([re.match(p, x) for p in datetime_patterns])
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 re.match(intx_re, x) is not None and -int_8_max <= int(x) < int_8_max
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 re.match(intx_re, x) is not None and -int16_max <= int(x) < int16_max
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 re.match(intx_re, x) is not None and -int32_max <= int(x) < int32_max
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 re.match(intx_re, x) is not None and -int64_max <= int(x) < int64_max
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 re.match(intx_re, x) is not None and -int128_max <= int(x) < int128_max
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 re.match(intx_re, x) is not None and -int256_max <= int(x) < int256_max
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 re.match(uintx_re, x) is not None and 0 <= int(x) < uint_8_max
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 re.match(uintx_re, x) is not None and 0 <= int(x) < uint16_max
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 re.match(uintx_re, x) is not None and 0 <= int(x) < uint32_max
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 re.match(uintx_re, x) is not None and 0 <= int(x) < uint64_max
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 re.match(intx_re, x) is not None and 0 <= int(x) < uint128_max
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 re.match(intx_re, x) is not None and 0 <= int(x) < uint256_max
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 validate_allowed(self):
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. Please, be sure you are using the "user token" instead of the "admin your@email" 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.dev0
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
 
@@ -1,4 +1,5 @@
1
1
  tests
2
2
  tests_e2e
3
3
  tests_e2e_ingestion
4
+ tests_tb
4
5
  tinybird