tinybird 0.0.1.dev5__py3-none-any.whl → 0.0.1.dev7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of tinybird might be problematic. Click here for more details.

Files changed (55) hide show
  1. tinybird/__cli__.py +7 -8
  2. tinybird/tb/cli.py +28 -0
  3. tinybird/{tb_cli_modules → tb/modules}/auth.py +5 -5
  4. tinybird/{tb_cli_modules → tb/modules}/branch.py +5 -25
  5. tinybird/{tb_cli_modules → tb/modules}/build.py +10 -21
  6. tinybird/tb/modules/cicd.py +271 -0
  7. tinybird/{tb_cli_modules → tb/modules}/cli.py +20 -140
  8. tinybird/tb/modules/common.py +2110 -0
  9. tinybird/tb/modules/config.py +352 -0
  10. tinybird/{tb_cli_modules → tb/modules}/connection.py +4 -4
  11. tinybird/{tb_cli_modules → tb/modules}/create.py +20 -20
  12. tinybird/tb/modules/datafile/build.py +2103 -0
  13. tinybird/tb/modules/datafile/build_common.py +118 -0
  14. tinybird/tb/modules/datafile/build_datasource.py +403 -0
  15. tinybird/tb/modules/datafile/build_pipe.py +648 -0
  16. tinybird/tb/modules/datafile/common.py +897 -0
  17. tinybird/tb/modules/datafile/diff.py +197 -0
  18. tinybird/tb/modules/datafile/exceptions.py +23 -0
  19. tinybird/tb/modules/datafile/format_common.py +66 -0
  20. tinybird/tb/modules/datafile/format_datasource.py +160 -0
  21. tinybird/tb/modules/datafile/format_pipe.py +195 -0
  22. tinybird/tb/modules/datafile/parse_datasource.py +41 -0
  23. tinybird/tb/modules/datafile/parse_pipe.py +69 -0
  24. tinybird/tb/modules/datafile/pipe_checker.py +560 -0
  25. tinybird/tb/modules/datafile/pull.py +157 -0
  26. tinybird/{tb_cli_modules → tb/modules}/datasource.py +7 -6
  27. tinybird/tb/modules/exceptions.py +91 -0
  28. tinybird/{tb_cli_modules → tb/modules}/fmt.py +6 -3
  29. tinybird/{tb_cli_modules → tb/modules}/job.py +3 -3
  30. tinybird/{tb_cli_modules → tb/modules}/llm.py +1 -1
  31. tinybird/{tb_cli_modules → tb/modules}/local.py +9 -5
  32. tinybird/{tb_cli_modules → tb/modules}/mock.py +5 -5
  33. tinybird/{tb_cli_modules → tb/modules}/pipe.py +11 -5
  34. tinybird/{tb_cli_modules → tb/modules}/prompts.py +1 -1
  35. tinybird/tb/modules/regions.py +9 -0
  36. tinybird/{tb_cli_modules → tb/modules}/tag.py +2 -2
  37. tinybird/tb/modules/telemetry.py +310 -0
  38. tinybird/{tb_cli_modules → tb/modules}/test.py +5 -5
  39. tinybird/{tb_cli_modules → tb/modules}/tinyunit/tinyunit.py +1 -1
  40. tinybird/{tb_cli_modules → tb/modules}/token.py +3 -3
  41. tinybird/{tb_cli_modules → tb/modules}/workspace.py +5 -5
  42. tinybird/{tb_cli_modules → tb/modules}/workspace_members.py +4 -4
  43. tinybird/tb_cli_modules/common.py +9 -25
  44. tinybird/tb_cli_modules/config.py +0 -8
  45. {tinybird-0.0.1.dev5.dist-info → tinybird-0.0.1.dev7.dist-info}/METADATA +1 -1
  46. tinybird-0.0.1.dev7.dist-info/RECORD +71 -0
  47. tinybird-0.0.1.dev7.dist-info/entry_points.txt +2 -0
  48. tinybird/datafile.py +0 -6123
  49. tinybird/tb_cli.py +0 -28
  50. tinybird-0.0.1.dev5.dist-info/RECORD +0 -52
  51. tinybird-0.0.1.dev5.dist-info/entry_points.txt +0 -2
  52. /tinybird/{tb_cli_modules → tb/modules}/table.py +0 -0
  53. /tinybird/{tb_cli_modules → tb/modules}/tinyunit/tinyunit_lib.py +0 -0
  54. {tinybird-0.0.1.dev5.dist-info → tinybird-0.0.1.dev7.dist-info}/WHEEL +0 -0
  55. {tinybird-0.0.1.dev5.dist-info → tinybird-0.0.1.dev7.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,352 @@
1
+ import json
2
+ import os
3
+ from copy import deepcopy
4
+ from dataclasses import dataclass
5
+ from enum import Enum
6
+ from typing import Any, Dict, List, Optional, Tuple, Union
7
+ from urllib.parse import urlparse
8
+
9
+ from packaging import version
10
+
11
+ import tinybird.client as tbc
12
+ from tinybird.config import CURRENT_VERSION, DEFAULT_API_HOST, DEFAULT_LOCALHOST
13
+
14
+ APP_CONFIG_NAME = "tinybird"
15
+
16
+
17
+ class FeatureFlags:
18
+ @classmethod
19
+ def ignore_sql_errors(cls) -> bool: # Context: #1155
20
+ return "TB_IGNORE_SQL_ERRORS" in os.environ
21
+
22
+ @classmethod
23
+ def is_localhost(cls) -> bool:
24
+ return "SET_LOCALHOST" in os.environ
25
+
26
+ @classmethod
27
+ def ignore_ssl_errors(cls) -> bool:
28
+ return os.environ.get("TB_DISABLE_SSL_CHECKS", "0") == "1"
29
+
30
+ @classmethod
31
+ def send_telemetry(cls) -> bool:
32
+ if os.environ.get("TB_CLI_TELEMETRY_OPTOUT", "0") == "1":
33
+ return False
34
+ if "x.y.z" in CURRENT_VERSION and os.environ.get("TB_CLI_TELEMETRY_SEND_IN_LOCAL", "0") == "0":
35
+ return False
36
+ return True
37
+
38
+
39
+ def compare_versions(a: str, b: str) -> int:
40
+ va = version.parse(a)
41
+ vb = version.parse(b)
42
+ return -1 if va < vb else (1 if va > vb else 0)
43
+
44
+
45
+ class ConfigValueOrigin(Enum):
46
+ # Sources for config values (environment variables, .tinyb file or default value)
47
+
48
+ ENVIRONMENT: str = "env"
49
+ CONFIG: str = "conf"
50
+ DEFAULT: str = "default"
51
+ NONE: str = ""
52
+
53
+
54
+ @dataclass
55
+ class ConfigValue:
56
+ name: str
57
+ value: Any
58
+ origin: ConfigValueOrigin
59
+
60
+ def as_tuple(self) -> Tuple[str, str]:
61
+ return (self.name, self.value)
62
+
63
+
64
+ def write_json_file(data: Dict[str, Any], path: str) -> None:
65
+ with open(path, "w") as file:
66
+ file.write(json.dumps(data, indent=4, sort_keys=True))
67
+
68
+
69
+ class CLIConfig:
70
+ # Mapping between environment variables and config values
71
+ ENV_KEYS: Dict[str, str] = {
72
+ "token": "TB_TOKEN",
73
+ "user_token": "TB_USER_TOKEN",
74
+ "host": "TB_HOST",
75
+ "semver": "TB_SEMVER",
76
+ }
77
+
78
+ DEFAULTS: Dict[str, str] = {"host": DEFAULT_API_HOST if not FeatureFlags.is_localhost() else DEFAULT_LOCALHOST}
79
+
80
+ _global: Optional["CLIConfig"] = None
81
+ _projects: Dict[str, "CLIConfig"] = {}
82
+
83
+ def __init__(self, path: Optional[str], parent: Optional["CLIConfig"] = None) -> None:
84
+ self._path = path
85
+ self._parent = parent
86
+ self._values: Dict[str, ConfigValue] = {}
87
+ self._values["version"] = ConfigValue("version", CURRENT_VERSION, ConfigValueOrigin.DEFAULT)
88
+ self._workspaces: List[Dict[str, Any]] = []
89
+
90
+ if self._path:
91
+ self.override_with_file(self._path)
92
+ self.override_with_environment()
93
+ self.override_with_defaults()
94
+
95
+ def __str__(self) -> str:
96
+ return str(self.to_dict())
97
+
98
+ def to_dict(self) -> Dict[str, Any]:
99
+ """Helper to ease"""
100
+ result: Dict[str, Any] = self._parent.to_dict() if self._parent else {}
101
+ result.update(dict((v.name, deepcopy(v.value)) for v in self._values.values()))
102
+ return result
103
+
104
+ def __getitem__(self, key) -> Any:
105
+ """Gets a config key value in this order:
106
+ - Environment
107
+ - Internal dict (by host)
108
+ - Parent dict (if has a parent)
109
+ - Default values
110
+ """
111
+ if key in self._values:
112
+ return self._values[key].value
113
+ if self._parent:
114
+ return self._parent[key]
115
+ raise KeyError(key)
116
+
117
+ def __setitem__(self, key: str, value: Any) -> None:
118
+ self._values[key] = ConfigValue(key, value, ConfigValueOrigin.CONFIG)
119
+
120
+ def __contains__(self, key: str) -> bool:
121
+ return self.get_value_origin(key) != ConfigValueOrigin.NONE
122
+
123
+ def get(self, key: str, default: Any = None) -> Any:
124
+ try:
125
+ return self[key]
126
+ except KeyError:
127
+ return default
128
+
129
+ def get_value_origin(self, key: str) -> ConfigValueOrigin:
130
+ if key in self._values:
131
+ return self._values[key].origin
132
+ if self._parent:
133
+ return self._parent.get_value_origin(key)
134
+ else:
135
+ return ConfigValueOrigin.NONE
136
+
137
+ def persist_to_file(self, override_with_values: Optional["CLIConfig"] = None) -> None:
138
+ if not self._path:
139
+ raise ValueError("Cannot persist configuration: `path` is None")
140
+
141
+ if override_with_values:
142
+ self.update(override_with_values)
143
+
144
+ os.makedirs(os.path.dirname(self._path), exist_ok=True)
145
+ values = dict(v.as_tuple() for v in self._values.values())
146
+ write_json_file(values, self._path)
147
+
148
+ def override_with_file(self, path: str) -> bool:
149
+ """Loads the contents of the passed file."""
150
+ try:
151
+ with open(path) as file:
152
+ values: Dict[str, Any] = json.loads(file.read())
153
+ for k, v in values.items():
154
+ self[k] = v
155
+ return True
156
+ except IOError:
157
+ return False
158
+
159
+ def override_with_environment(self) -> None:
160
+ """Loads environment variables."""
161
+ for config_key, env_key in CLIConfig.ENV_KEYS.items():
162
+ env_value = os.environ.get(env_key, None)
163
+ if env_value:
164
+ self._values[config_key] = ConfigValue(config_key, env_value, ConfigValueOrigin.ENVIRONMENT)
165
+
166
+ def override_with_defaults(self) -> None:
167
+ """Loads default values."""
168
+ for key, default_value in CLIConfig.DEFAULTS.items():
169
+ if key not in self._values:
170
+ self._values[key] = ConfigValue(key, default_value, ConfigValueOrigin.DEFAULT)
171
+
172
+ def set_token(self, token: Optional[str]) -> None:
173
+ self["token"] = token
174
+
175
+ def get_token(self) -> Optional[str]:
176
+ try:
177
+ return self["token"]
178
+ except KeyError:
179
+ return None
180
+
181
+ def set_semver(self, semver: Optional[str]) -> None:
182
+ self["semver"] = semver
183
+
184
+ def get_semver(self) -> Optional[str]:
185
+ try:
186
+ return self["semver"]
187
+ except KeyError:
188
+ return None
189
+
190
+ def set_token_for_host(self, token: Optional[str], host: Optional[str]) -> None:
191
+ """Sets the token for the specified host.
192
+
193
+ Here we ask for the host explicitly to avoid mistakenly setting the token
194
+ for the wrong host.
195
+ """
196
+ if not token and not host:
197
+ return
198
+
199
+ assert isinstance(host, str)
200
+
201
+ tokens: Dict[str, Optional[str]] = self.get("tokens", {})
202
+ tokens[host] = token
203
+ self["tokens"] = tokens
204
+
205
+ def get_token_for_host(self, host: str) -> Optional[str]:
206
+ try:
207
+ return self["tokens"][host]
208
+ except KeyError:
209
+ return None
210
+
211
+ def set_user_token(self, token: Optional[str]) -> None:
212
+ self["user_token"] = token
213
+
214
+ def get_user_token(self) -> Optional[str]:
215
+ try:
216
+ return self["user_token"]
217
+ except KeyError:
218
+ return None
219
+
220
+ def set_host(self, host: Optional[str]) -> None:
221
+ url_info = urlparse(host)
222
+ scheme: str = url_info.scheme.decode() if isinstance(url_info.scheme, bytes) else url_info.scheme
223
+ netloc: str = url_info.netloc.decode() if isinstance(url_info.netloc, bytes) else url_info.netloc
224
+ self["host"] = f"{scheme}://{netloc}"
225
+
226
+ def get_host(self, use_defaults_if_needed: bool = False) -> Optional[str]:
227
+ result: Optional[str] = self.get("host", None)
228
+ if result:
229
+ return result
230
+ if use_defaults_if_needed:
231
+ return CLIConfig.DEFAULTS["host"]
232
+ return None
233
+
234
+ def get_client(self, token: Optional[str] = None, host: Optional[str] = None) -> tbc.TinyB:
235
+ """Returns a new TinyB client configured with:
236
+
237
+ - token:
238
+ - passed token
239
+ - OR the user token
240
+ - OR the current admin token
241
+
242
+ - host:
243
+ - passed host
244
+ - OR the current host
245
+ """
246
+ host = host or self.get_host()
247
+ assert isinstance(host, str)
248
+ token = token or self.get_token() or self.get_token_for_host(host) or ""
249
+ assert isinstance(token, str)
250
+
251
+ return tbc.TinyB(
252
+ token,
253
+ host,
254
+ version=CURRENT_VERSION,
255
+ disable_ssl_checks=FeatureFlags.ignore_ssl_errors(),
256
+ send_telemetry=FeatureFlags.send_telemetry(),
257
+ )
258
+
259
+ def get_user_client(self, host: Optional[str] = None) -> tbc.TinyB:
260
+ return self.get_client(self.get_user_token(), host)
261
+
262
+ def set_workspace_token(self, workspace_id: str, token: str) -> None:
263
+ pass
264
+
265
+ async def get_workspace_token(
266
+ self, workspace_id: Optional[str] = None, host: Optional[str] = None
267
+ ) -> Optional[str]:
268
+ """Returns the token for the specific workspace on a host.
269
+ - If no workspace passed, it uses the current active workspace.
270
+ - If no host is passed, it uses the current one.
271
+ """
272
+
273
+ # First, try to get any saved token for the host
274
+ if host:
275
+ try:
276
+ return self["tokens"][host]
277
+ except KeyError:
278
+ pass
279
+
280
+ # If we don't have an user_token, can't continue
281
+ if not self.get_user_token():
282
+ return None
283
+
284
+ if not workspace_id:
285
+ workspace_id = self.get("id", None)
286
+ if not workspace_id:
287
+ return None
288
+
289
+ client: tbc.TinyB = self.get_client(token=self.get_user_token(), host=host)
290
+
291
+ info: Dict[str, Any] = await client.user_workspaces_and_branches()
292
+ workspaces: List[Dict[str, Any]] = info["workspaces"]
293
+
294
+ result: Optional[str] = next(
295
+ (w["token"] for w in workspaces if workspace_id in (w.get("id"), w.get("name"))), None
296
+ )
297
+ if host:
298
+ self.set_token_for_host(result, host)
299
+
300
+ return result
301
+
302
+ def spawn(self) -> "CLIConfig":
303
+ return CLIConfig(path=None, parent=self)
304
+
305
+ def update(self, other: Union["CLIConfig", Dict[str, Any]]):
306
+ values = other if isinstance(other, dict) else other._values
307
+ for k, v in values.items():
308
+ self[k] = v
309
+
310
+ @staticmethod
311
+ def get_global_config(_path: Optional[str] = None) -> "CLIConfig":
312
+ """Returns the user-specific config.
313
+
314
+ The data is cached between calls, so feel free to use it freely instead
315
+ of saving a reference.
316
+
317
+ Note: the `_path` argument is mainly intended to help during testing.
318
+ """
319
+ if not CLIConfig._global:
320
+ path: Optional[str] = _path or os.environ.get("XDG_CONFIG_HOME", None)
321
+ if not path:
322
+ path = os.path.join(os.environ.get("HOME", "~"), ".config")
323
+ path = os.path.join(path, APP_CONFIG_NAME, ".tinyb")
324
+ CLIConfig._global = CLIConfig(path, parent=None)
325
+ return CLIConfig._global
326
+
327
+ @staticmethod
328
+ def get_project_config(working_dir: Optional[str] = None) -> "CLIConfig":
329
+ """Returns the project-specific config located at `working_dir` (defaults to `os.getcwd()`)
330
+
331
+ The data is cached between calls, given the same `working_dir`.
332
+ """
333
+ working_dir = working_dir or os.getcwd()
334
+ result = CLIConfig._projects.get(working_dir)
335
+ if not result:
336
+ path: str = os.path.join(working_dir, ".tinyb")
337
+ result = CLIConfig(path, parent=CLIConfig.get_global_config())
338
+ CLIConfig._projects[working_dir] = result
339
+ return result
340
+
341
+ @staticmethod
342
+ def get_llm_config(working_dir: Optional[str] = None) -> Dict[str, Any]:
343
+ return (
344
+ CLIConfig.get_project_config(working_dir)
345
+ .get("llms", {})
346
+ .get("openai", {"model": "gpt-4o-mini", "api_key": None})
347
+ )
348
+
349
+ @staticmethod
350
+ def reset() -> None:
351
+ CLIConfig._global = None
352
+ CLIConfig._projects.clear()
@@ -14,8 +14,8 @@ from click import Context
14
14
 
15
15
  from tinybird.client import DoesNotExistException, TinyB
16
16
  from tinybird.feedback_manager import FeedbackManager
17
- from tinybird.tb_cli_modules.cli import cli
18
- from tinybird.tb_cli_modules.common import (
17
+ from tinybird.tb.modules.cli import cli
18
+ from tinybird.tb.modules.common import (
19
19
  ConnectionReplacements,
20
20
  DataConnectorType,
21
21
  _get_setting_value,
@@ -33,8 +33,8 @@ from tinybird.tb_cli_modules.common import (
33
33
  validate_kafka_secret,
34
34
  validate_string_connector_param,
35
35
  )
36
- from tinybird.tb_cli_modules.exceptions import CLIConnectionException
37
- from tinybird.tb_cli_modules.telemetry import is_ci_environment
36
+ from tinybird.tb.modules.exceptions import CLIConnectionException
37
+ from tinybird.tb.modules.telemetry import is_ci_environment
38
38
 
39
39
  DATA_CONNECTOR_SETTINGS: Dict[DataConnectorType, List[str]] = {
40
40
  DataConnectorType.KAFKA: [
@@ -1,20 +1,21 @@
1
1
  import os
2
2
  from os import getcwd
3
3
  from pathlib import Path
4
- from typing import Any, Dict, List, Optional
4
+ from typing import Optional
5
5
 
6
6
  import click
7
7
  from click import Context
8
8
 
9
9
  from tinybird.client import TinyB
10
- from tinybird.datafile import folder_build
11
10
  from tinybird.feedback_manager import FeedbackManager
12
- from tinybird.tb_cli_modules.cli import cli
13
- from tinybird.tb_cli_modules.common import _generate_datafile, coro, generate_datafile, push_data
14
- from tinybird.tb_cli_modules.config import CLIConfig
15
- from tinybird.tb_cli_modules.exceptions import CLIDatasourceException
16
- from tinybird.tb_cli_modules.llm import LLM
17
- from tinybird.tb_cli_modules.local import (
11
+ from tinybird.tb.modules.cicd import init_cicd
12
+ from tinybird.tb.modules.cli import cli
13
+ from tinybird.tb.modules.common import _generate_datafile, coro, generate_datafile, push_data
14
+ from tinybird.tb.modules.config import CLIConfig
15
+ from tinybird.tb.modules.datafile.build import folder_build
16
+ from tinybird.tb.modules.exceptions import CLIDatasourceException
17
+ from tinybird.tb.modules.llm import LLM
18
+ from tinybird.tb.modules.local import (
18
19
  get_tinybird_local_client,
19
20
  )
20
21
 
@@ -54,15 +55,10 @@ async def create(
54
55
  click.echo(FeedbackManager.gray(message="Creating new project structure..."))
55
56
  await project_create(tb_client, data, prompt, folder)
56
57
  click.echo(FeedbackManager.success(message="✓ Scaffolding completed!\n"))
57
- workspaces: List[Dict[str, Any]] = (await tb_client.user_workspaces()).get("workspaces", [])
58
- datasources = await tb_client.datasources()
59
- pipes = await tb_client.pipes(dependencies=True)
60
- await folder_build(
61
- tb_client,
62
- workspaces,
63
- datasources,
64
- pipes,
65
- )
58
+ await folder_build(tb_client, folder=folder)
59
+
60
+ await init_cicd(data_project_dir=os.path.relpath(folder))
61
+
66
62
  if data:
67
63
  ds_name = os.path.basename(data.split(".")[0])
68
64
  await append_datasource(ctx, tb_client, ds_name, data, None, None, False, 1)
@@ -98,8 +94,10 @@ async def project_create(
98
94
  except FileExistsError:
99
95
  click.echo(FeedbackManager.info_path_created(path=x))
100
96
 
101
- def generate_pipe_file(name: str, content: str):
97
+ def generate_pipe_file(name: str, content: str, parent_dir: Optional[str] = None):
102
98
  base = Path("endpoints")
99
+ if parent_dir:
100
+ base = Path(parent_dir) / base
103
101
  if not base.exists():
104
102
  base = Path()
105
103
  f = base / (f"{name}.pipe")
@@ -164,8 +162,10 @@ SQL >
164
162
  LIMIT 5
165
163
  TYPE ENDPOINT
166
164
  """
167
- generate_datafile(events_ds, filename="events.datasource", data=None, _format="ndjson", force=force)
168
- generate_pipe_file("top_airlines", top_airlines)
165
+ generate_datafile(
166
+ events_ds, filename="events.datasource", data=None, _format="ndjson", force=force, parent_dir=folder
167
+ )
168
+ generate_pipe_file("top_airlines", top_airlines, parent_dir=folder)
169
169
 
170
170
 
171
171
  async def append_datasource(