tinybird 0.0.1.dev0__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 (45) hide show
  1. tinybird/__cli__.py +8 -0
  2. tinybird/ch_utils/constants.py +244 -0
  3. tinybird/ch_utils/engine.py +855 -0
  4. tinybird/check_pypi.py +25 -0
  5. tinybird/client.py +1281 -0
  6. tinybird/config.py +117 -0
  7. tinybird/connectors.py +428 -0
  8. tinybird/context.py +23 -0
  9. tinybird/datafile.py +5589 -0
  10. tinybird/datatypes.py +434 -0
  11. tinybird/feedback_manager.py +1022 -0
  12. tinybird/git_settings.py +145 -0
  13. tinybird/sql.py +865 -0
  14. tinybird/sql_template.py +2343 -0
  15. tinybird/sql_template_fmt.py +281 -0
  16. tinybird/sql_toolset.py +350 -0
  17. tinybird/syncasync.py +682 -0
  18. tinybird/tb_cli.py +25 -0
  19. tinybird/tb_cli_modules/auth.py +252 -0
  20. tinybird/tb_cli_modules/branch.py +1043 -0
  21. tinybird/tb_cli_modules/cicd.py +434 -0
  22. tinybird/tb_cli_modules/cli.py +1571 -0
  23. tinybird/tb_cli_modules/common.py +2082 -0
  24. tinybird/tb_cli_modules/config.py +344 -0
  25. tinybird/tb_cli_modules/connection.py +803 -0
  26. tinybird/tb_cli_modules/datasource.py +900 -0
  27. tinybird/tb_cli_modules/exceptions.py +91 -0
  28. tinybird/tb_cli_modules/fmt.py +91 -0
  29. tinybird/tb_cli_modules/job.py +85 -0
  30. tinybird/tb_cli_modules/pipe.py +858 -0
  31. tinybird/tb_cli_modules/regions.py +9 -0
  32. tinybird/tb_cli_modules/tag.py +100 -0
  33. tinybird/tb_cli_modules/telemetry.py +310 -0
  34. tinybird/tb_cli_modules/test.py +107 -0
  35. tinybird/tb_cli_modules/tinyunit/tinyunit.py +340 -0
  36. tinybird/tb_cli_modules/tinyunit/tinyunit_lib.py +71 -0
  37. tinybird/tb_cli_modules/token.py +349 -0
  38. tinybird/tb_cli_modules/workspace.py +269 -0
  39. tinybird/tb_cli_modules/workspace_members.py +212 -0
  40. tinybird/tornado_template.py +1194 -0
  41. tinybird-0.0.1.dev0.dist-info/METADATA +2815 -0
  42. tinybird-0.0.1.dev0.dist-info/RECORD +45 -0
  43. tinybird-0.0.1.dev0.dist-info/WHEEL +5 -0
  44. tinybird-0.0.1.dev0.dist-info/entry_points.txt +2 -0
  45. tinybird-0.0.1.dev0.dist-info/top_level.txt +4 -0
@@ -0,0 +1,344 @@
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 reset() -> None:
343
+ CLIConfig._global = None
344
+ CLIConfig._projects.clear()