tinybird 0.0.1.dev291__py3-none-any.whl → 1.0.5__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.
Files changed (76) hide show
  1. tinybird/ch_utils/constants.py +5 -0
  2. tinybird/connectors.py +1 -7
  3. tinybird/context.py +3 -3
  4. tinybird/datafile/common.py +10 -8
  5. tinybird/datafile/parse_pipe.py +2 -2
  6. tinybird/feedback_manager.py +3 -0
  7. tinybird/prompts.py +1 -0
  8. tinybird/service_datasources.py +223 -0
  9. tinybird/sql_template.py +26 -11
  10. tinybird/sql_template_fmt.py +14 -4
  11. tinybird/tb/__cli__.py +2 -2
  12. tinybird/tb/cli.py +1 -0
  13. tinybird/tb/client.py +104 -26
  14. tinybird/tb/config.py +24 -0
  15. tinybird/tb/modules/agent/agent.py +103 -67
  16. tinybird/tb/modules/agent/banner.py +15 -15
  17. tinybird/tb/modules/agent/explore_agent.py +5 -0
  18. tinybird/tb/modules/agent/mock_agent.py +5 -1
  19. tinybird/tb/modules/agent/models.py +6 -2
  20. tinybird/tb/modules/agent/prompts.py +49 -2
  21. tinybird/tb/modules/agent/tools/deploy.py +1 -1
  22. tinybird/tb/modules/agent/tools/execute_query.py +15 -18
  23. tinybird/tb/modules/agent/tools/request_endpoint.py +1 -1
  24. tinybird/tb/modules/agent/tools/run_command.py +9 -0
  25. tinybird/tb/modules/agent/utils.py +38 -48
  26. tinybird/tb/modules/branch.py +150 -0
  27. tinybird/tb/modules/build.py +58 -13
  28. tinybird/tb/modules/build_common.py +209 -25
  29. tinybird/tb/modules/cli.py +129 -16
  30. tinybird/tb/modules/common.py +172 -146
  31. tinybird/tb/modules/connection.py +125 -194
  32. tinybird/tb/modules/connection_kafka.py +382 -0
  33. tinybird/tb/modules/copy.py +3 -1
  34. tinybird/tb/modules/create.py +83 -150
  35. tinybird/tb/modules/datafile/build.py +27 -38
  36. tinybird/tb/modules/datafile/build_datasource.py +21 -25
  37. tinybird/tb/modules/datafile/diff.py +1 -1
  38. tinybird/tb/modules/datafile/format_pipe.py +46 -7
  39. tinybird/tb/modules/datafile/playground.py +59 -68
  40. tinybird/tb/modules/datafile/pull.py +2 -3
  41. tinybird/tb/modules/datasource.py +477 -308
  42. tinybird/tb/modules/deployment.py +2 -0
  43. tinybird/tb/modules/deployment_common.py +84 -44
  44. tinybird/tb/modules/deprecations.py +4 -4
  45. tinybird/tb/modules/dev_server.py +33 -12
  46. tinybird/tb/modules/exceptions.py +14 -0
  47. tinybird/tb/modules/feedback_manager.py +1 -1
  48. tinybird/tb/modules/info.py +69 -12
  49. tinybird/tb/modules/infra.py +4 -5
  50. tinybird/tb/modules/job_common.py +15 -0
  51. tinybird/tb/modules/local.py +143 -23
  52. tinybird/tb/modules/local_common.py +347 -19
  53. tinybird/tb/modules/local_logs.py +209 -0
  54. tinybird/tb/modules/login.py +21 -2
  55. tinybird/tb/modules/login_common.py +254 -12
  56. tinybird/tb/modules/mock.py +5 -54
  57. tinybird/tb/modules/mock_common.py +0 -54
  58. tinybird/tb/modules/open.py +10 -5
  59. tinybird/tb/modules/project.py +14 -5
  60. tinybird/tb/modules/shell.py +15 -7
  61. tinybird/tb/modules/sink.py +3 -1
  62. tinybird/tb/modules/telemetry.py +11 -3
  63. tinybird/tb/modules/test.py +13 -9
  64. tinybird/tb/modules/test_common.py +13 -87
  65. tinybird/tb/modules/tinyunit/tinyunit.py +0 -14
  66. tinybird/tb/modules/tinyunit/tinyunit_lib.py +0 -6
  67. tinybird/tb/modules/watch.py +5 -3
  68. tinybird/tb_cli_modules/common.py +2 -2
  69. tinybird/tb_cli_modules/telemetry.py +1 -1
  70. tinybird/tornado_template.py +6 -7
  71. {tinybird-0.0.1.dev291.dist-info → tinybird-1.0.5.dist-info}/METADATA +32 -6
  72. tinybird-1.0.5.dist-info/RECORD +132 -0
  73. {tinybird-0.0.1.dev291.dist-info → tinybird-1.0.5.dist-info}/WHEEL +1 -1
  74. tinybird-0.0.1.dev291.dist-info/RECORD +0 -128
  75. {tinybird-0.0.1.dev291.dist-info → tinybird-1.0.5.dist-info}/entry_points.txt +0 -0
  76. {tinybird-0.0.1.dev291.dist-info → tinybird-1.0.5.dist-info}/top_level.txt +0 -0
@@ -30,7 +30,6 @@ import requests
30
30
  from click import Context
31
31
  from click._termui_impl import ProgressBar
32
32
  from humanfriendly.tables import format_pretty_table
33
- from packaging.version import Version
34
33
  from thefuzz import process
35
34
 
36
35
  from tinybird.tb.client import (
@@ -61,7 +60,6 @@ from tinybird.tb.modules.exceptions import (
61
60
  CLIAuthException,
62
61
  CLIConnectionException,
63
62
  CLIException,
64
- CLIReleaseException,
65
63
  CLIWorkspaceException,
66
64
  )
67
65
  from tinybird.tb.modules.feedback_manager import FeedbackManager, warning_message
@@ -378,9 +376,33 @@ def getenv_bool(key: str, default: bool) -> bool:
378
376
  return v.lower() == "true" or v == "1"
379
377
 
380
378
 
381
- def _get_tb_client(token: str, host: str, staging: bool = False) -> TinyB:
379
+ def _get_tb_client(token: str, host: str, staging: bool = False, branch: Optional[str] = None) -> TinyB:
382
380
  disable_ssl: bool = getenv_bool("TB_DISABLE_SSL_CHECKS", False)
383
- return TinyB(token, host, version=VERSION, disable_ssl_checks=disable_ssl, send_telemetry=True, staging=staging)
381
+ cloud_client = TinyB(
382
+ token,
383
+ host,
384
+ version=VERSION,
385
+ disable_ssl_checks=disable_ssl,
386
+ send_telemetry=True,
387
+ staging=staging,
388
+ )
389
+
390
+ if not branch:
391
+ return cloud_client
392
+
393
+ workspaces = cloud_client.user_workspaces_and_branches(version="v1")
394
+ workspace = next((w for w in workspaces.get("workspaces", []) if w.get("name") == branch), None)
395
+ if not workspace:
396
+ raise CLIException(FeedbackManager.error_exception(error=f"Branch {branch} not found"))
397
+
398
+ return TinyB(
399
+ workspace.get("token", ""),
400
+ host,
401
+ version=VERSION,
402
+ disable_ssl_checks=disable_ssl,
403
+ send_telemetry=True,
404
+ staging=staging,
405
+ )
384
406
 
385
407
 
386
408
  def create_tb_client(ctx: Context) -> TinyB:
@@ -414,7 +436,16 @@ def analyze_file(filename: str, client: TinyB, format: str):
414
436
  meta, data = _analyze(filename, client, format)
415
437
  schema = meta["analysis"]["schema"]
416
438
  schema = schema.replace(", ", ",\n ")
417
- content = f"""DESCRIPTION >\n Generated from {filename}\n\nSCHEMA >\n {schema}"""
439
+ content = f"""DESCRIPTION >
440
+ Generated from {filename}
441
+
442
+ SCHEMA >
443
+ {schema}
444
+
445
+ ENGINE "MergeTree"
446
+ # ENGINE_SORTING_KEY "user_id, timestamp"
447
+ # ENGINE_TTL "timestamp + toIntervalDay(60)"
448
+ # Learn more at https://www.tinybird.co/docs/forward/dev-reference/datafiles/datasource-files"""
418
449
  return content
419
450
 
420
451
 
@@ -429,7 +460,16 @@ def _generate_datafile(
429
460
  meta, data = _analyze(filename, client, format, connector=connector)
430
461
  schema = meta["analysis"]["schema"]
431
462
  schema = schema.replace(", ", ",\n ")
432
- datafile = f"""DESCRIPTION >\n Generated from {filename}\n\nSCHEMA >\n {schema}"""
463
+ datafile = f"""DESCRIPTION >
464
+ Generated from {filename}
465
+
466
+ SCHEMA >
467
+ {schema}
468
+
469
+ ENGINE "MergeTree"
470
+ # ENGINE_SORTING_KEY "user_id, timestamp"
471
+ # ENGINE_TTL "timestamp + toIntervalDay(60)"
472
+ # Learn more at https://www.tinybird.co/docs/forward/dev-reference/datafiles/datasource-files"""
433
473
  return generate_datafile(datafile, filename, data, force, _format=format, folder=folder)
434
474
 
435
475
 
@@ -908,7 +948,7 @@ def get_format_from_filename_or_url(filename_or_url: str) -> str:
908
948
  'csv'
909
949
  """
910
950
  filename_or_url = filename_or_url.lower()
911
- if filename_or_url.endswith("json") or filename_or_url.endswith("ndjson"):
951
+ if filename_or_url.endswith(("json", "ndjson")):
912
952
  return "ndjson"
913
953
  if filename_or_url.endswith("parquet"):
914
954
  return "parquet"
@@ -916,7 +956,7 @@ def get_format_from_filename_or_url(filename_or_url: str) -> str:
916
956
  return "csv"
917
957
  try:
918
958
  parsed = urlparse(filename_or_url)
919
- if parsed.path.endswith("json") or parsed.path.endswith("ndjson"):
959
+ if parsed.path.endswith(("json", "ndjson")):
920
960
  return "ndjson"
921
961
  if parsed.path.endswith("parquet"):
922
962
  return "parquet"
@@ -965,7 +1005,10 @@ def push_data(
965
1005
  cb.prev_done = 0 # type: ignore[attr-defined]
966
1006
 
967
1007
  if not silent:
968
- click.echo(FeedbackManager.highlight(message=f"\n» Appending data to {datasource_name}..."))
1008
+ if mode == "replace":
1009
+ click.echo(FeedbackManager.highlight(message=f"\n» Replacing data in {datasource_name}..."))
1010
+ else:
1011
+ click.echo(FeedbackManager.highlight(message=f"\n» Appending data to {datasource_name}..."))
969
1012
 
970
1013
  if isinstance(url, list):
971
1014
  urls = url
@@ -1165,10 +1208,26 @@ def _get_setting_value(connection, setting, sensitive_settings):
1165
1208
  return connection.get(setting, "")
1166
1209
 
1167
1210
 
1168
- def switch_workspace(config: CLIConfig, workspace_name_or_id: str) -> None:
1211
+ def get_current_workspace_branches(config: CLIConfig) -> List[Dict[str, Any]]:
1212
+ current_main_workspace: Optional[Dict[str, Any]] = get_current_main_workspace(config)
1213
+ if not current_main_workspace:
1214
+ raise CLIException(FeedbackManager.error_unable_to_identify_main_workspace())
1215
+
1216
+ client = config.get_client()
1217
+ user_branches: List[Dict[str, Any]] = (client.user_workspace_branches("v1")).get("workspaces", [])
1218
+ all_branches: List[Dict[str, Any]] = (client.branches()).get("environments", [])
1219
+ branches = user_branches + [branch for branch in all_branches if branch not in user_branches]
1220
+
1221
+ return [branch for branch in branches if branch.get("main") == current_main_workspace["id"]]
1222
+
1223
+
1224
+ def switch_workspace(config: CLIConfig, workspace_name_or_id: str, only_environments: bool = False) -> None:
1169
1225
  try:
1170
- response = config.get_client().user_workspaces(version="v1")
1171
- workspaces = response["workspaces"]
1226
+ if only_environments:
1227
+ workspaces = get_current_workspace_branches(config)
1228
+ else:
1229
+ response = config.get_client().user_workspaces(version="v1")
1230
+ workspaces = response["workspaces"]
1172
1231
 
1173
1232
  workspace = next(
1174
1233
  (
@@ -1563,130 +1622,6 @@ def _get_current_main_workspace_common(
1563
1622
  return current
1564
1623
 
1565
1624
 
1566
- def is_post_semver(new_version: Version, current_version: Version) -> bool:
1567
- """
1568
- Check if only the post part of the semantic version has changed.
1569
-
1570
- Args:
1571
- new_version (Version): The new version to check.
1572
- current_version (Version): The current version to compare with.
1573
-
1574
- Returns:
1575
- bool: True if only the post part of the version has changed, False otherwise.
1576
-
1577
- Examples:
1578
- >>> is_post_semver(Version("0.0.0-2"), Version("0.0.0-1"))
1579
- True
1580
- >>> is_post_semver(Version("0.0.0-1"), Version("0.0.0-1"))
1581
- False
1582
- >>> is_post_semver(Version("0.0.1-1"), Version("0.0.0-1"))
1583
- False
1584
- >>> is_post_semver(Version("0.1.0-1"), Version("0.0.0-1"))
1585
- False
1586
- >>> is_post_semver(Version("1.0.0-1"), Version("0.0.0-1"))
1587
- False
1588
- >>> is_post_semver(Version("0.0.1-1"), Version("0.0.0"))
1589
- False
1590
- """
1591
- if (
1592
- new_version.major == current_version.major
1593
- and new_version.minor == current_version.minor
1594
- and new_version.micro == current_version.micro
1595
- ):
1596
- return new_version.post is not None and new_version.post != current_version.post
1597
-
1598
- return False
1599
-
1600
-
1601
- def is_major_semver(new_version: Version, current_version: Version) -> bool:
1602
- """
1603
- Check if only the major part of the semantic version has changed.
1604
-
1605
- Args:
1606
- new_version (Version): The new version to check.
1607
- current_version (Version): The current version to compare with.
1608
-
1609
- Returns:
1610
- bool: True if only the major part of the version has changed, False otherwise.
1611
-
1612
- Examples:
1613
- >>> is_major_semver(Version("1.0.0"), Version("0.0.0"))
1614
- True
1615
- >>> is_major_semver(Version("0.0.0"), Version("0.0.0"))
1616
- False
1617
- >>> is_major_semver(Version("1.0.1"), Version("1.0.0"))
1618
- False
1619
- >>> is_major_semver(Version("2.0.0-1"), Version("1.0.1-2"))
1620
- True
1621
- """
1622
-
1623
- return new_version.major != current_version.major
1624
-
1625
-
1626
- def print_release_summary(config: CLIConfig, semver: Optional[str], info: bool = False, dry_run=False):
1627
- if not semver:
1628
- click.echo(FeedbackManager.info_release_no_rollback())
1629
- return
1630
- try:
1631
- client = config.get_client()
1632
- response = client.release_rm(config["id"], semver, confirmation=config["name"], dry_run=True)
1633
- except Exception as e:
1634
- raise CLIReleaseException(FeedbackManager.error_exception(error=str(e)))
1635
- else:
1636
- columns = ["name", "id", "notes"]
1637
- if not response:
1638
- click.echo(FeedbackManager.info_release_no_rollback())
1639
- return
1640
-
1641
- if len(response["datasources"]) or len(response["pipes"]):
1642
- semver = response.get("semver", semver)
1643
- if info:
1644
- if dry_run:
1645
- click.echo(FeedbackManager.info_release_rm_resources_dry_run(semver=semver))
1646
- else:
1647
- click.echo(FeedbackManager.info_release_rm_resources())
1648
- else:
1649
- click.echo(FeedbackManager.info_release_rollback(semver=semver))
1650
-
1651
- if len(response["datasources"]):
1652
- click.echo("\nDatasources:")
1653
- rows = [
1654
- [ds, response["datasources"][ds], response["notes"].get(response["datasources"][ds], "")]
1655
- for ds in response["datasources"]
1656
- ]
1657
- echo_safe_humanfriendly_tables_format_smart_table(rows, column_names=columns)
1658
-
1659
- if len(response["pipes"]):
1660
- click.echo("\nPipes:")
1661
- rows = [
1662
- [pipe, response["pipes"][pipe], response["notes"].get(response["pipes"][pipe], "")]
1663
- for pipe in response["pipes"]
1664
- ]
1665
- echo_safe_humanfriendly_tables_format_smart_table(rows, column_names=columns)
1666
-
1667
-
1668
- def get_oldest_rollback(config: CLIConfig, client: TinyB) -> Optional[str]:
1669
- oldest_rollback_response = client.release_oldest_rollback(config["id"])
1670
- return oldest_rollback_response.get("semver") if oldest_rollback_response else None
1671
-
1672
-
1673
- def remove_release(
1674
- dry_run: bool, config: CLIConfig, semver: Optional[str], client: TinyB, force: bool, show_print=True
1675
- ):
1676
- if semver == OLDEST_ROLLBACK:
1677
- semver = get_oldest_rollback(config, client)
1678
- if show_print:
1679
- print_release_summary(config, semver, info=True, dry_run=True)
1680
- if not dry_run:
1681
- if semver:
1682
- response = client.release_rm(
1683
- config["id"], semver, confirmation=config["name"], dry_run=dry_run, force=force
1684
- )
1685
- click.echo(FeedbackManager.success_release_delete(semver=response.get("semver")))
1686
- else:
1687
- click.echo(FeedbackManager.info_no_release_deleted())
1688
-
1689
-
1690
1625
  def run_aws_iamrole_connection_flow(
1691
1626
  client: TinyB,
1692
1627
  service: str,
@@ -2197,18 +2132,17 @@ def ask_for_organization(
2197
2132
  user_token = get_user_token(config, user_token)
2198
2133
  organization = create_organization_and_add_workspaces(config, organization_name, user_token)
2199
2134
  organization_id = organization.get("id")
2135
+ elif len(organizations) == 1:
2136
+ organization_name = organizations[0]["name"]
2137
+ organization_id = organizations[0]["id"]
2200
2138
  else:
2201
- if len(organizations) == 1:
2202
- organization_name = organizations[0]["name"]
2203
- organization_id = organizations[0]["id"]
2139
+ sorted_organizations = sort_organizations_by_user(organizations, user_email=user_email)
2140
+ current_organization = ask_for_organization_interactively(sorted_organizations)
2141
+ if current_organization:
2142
+ organization_id = current_organization.get("id")
2143
+ organization_name = current_organization.get("name")
2204
2144
  else:
2205
- sorted_organizations = sort_organizations_by_user(organizations, user_email=user_email)
2206
- current_organization = ask_for_organization_interactively(sorted_organizations)
2207
- if current_organization:
2208
- organization_id = current_organization.get("id")
2209
- organization_name = current_organization.get("name")
2210
- else:
2211
- return None, None
2145
+ return None, None
2212
2146
  return organization_id, organization_name
2213
2147
 
2214
2148
 
@@ -2262,3 +2196,95 @@ def update_cli() -> None:
2262
2196
  click.echo(FeedbackManager.success(message="✓ Tinybird CLI updated"))
2263
2197
  else:
2264
2198
  click.echo(FeedbackManager.info(message="✓ Tinybird CLI is already up-to-date"))
2199
+
2200
+
2201
+ def create_workspace_branch(
2202
+ branch_name: Optional[str],
2203
+ last_partition: bool,
2204
+ all: bool,
2205
+ ignore_datasources: Optional[List[str]],
2206
+ wait: Optional[bool],
2207
+ ) -> None:
2208
+ """
2209
+ Creates a workspace branch
2210
+ """
2211
+ config = CLIConfig.get_project_config()
2212
+ _ = try_update_config_with_remote(config)
2213
+
2214
+ try:
2215
+ workspace = get_current_workspace(config)
2216
+ if not workspace:
2217
+ raise CLIWorkspaceException(FeedbackManager.error_workspace())
2218
+
2219
+ if not branch_name:
2220
+ click.echo(FeedbackManager.info_workspace_branch_create_greeting())
2221
+ default_name = f"{workspace['name']}_{uuid.uuid4().hex[0:4]}"
2222
+ branch_name = click.prompt("\Branch name", default=default_name, err=True, type=str)
2223
+ assert isinstance(branch_name, str)
2224
+
2225
+ response = config.get_client().create_workspace_branch(
2226
+ branch_name,
2227
+ last_partition,
2228
+ all,
2229
+ ignore_datasources,
2230
+ )
2231
+ assert isinstance(response, dict)
2232
+
2233
+ is_job: bool = "job" in response
2234
+ is_summary: bool = "partitions" in response
2235
+
2236
+ if not is_job and not is_summary:
2237
+ raise CLIException(str(response))
2238
+
2239
+ if all and not is_job:
2240
+ raise CLIException(str(response))
2241
+
2242
+ click.echo(
2243
+ FeedbackManager.success_workspace_branch_created(workspace_name=workspace["name"], branch_name=branch_name)
2244
+ )
2245
+
2246
+ job_id: Optional[str] = None
2247
+
2248
+ if is_job:
2249
+ job_id = response["job"]["job_id"]
2250
+ job_url = response["job"]["job_url"]
2251
+ click.echo(FeedbackManager.info_data_branch_job_url(url=job_url))
2252
+
2253
+ if wait and is_job:
2254
+ assert isinstance(job_id, str)
2255
+
2256
+ # Await the job to finish and get the result dict
2257
+ job_response = wait_job(config.get_client(), job_id, job_url, "Environment creation")
2258
+ if job_response is None:
2259
+ raise CLIException(f"Empty job API response (job_id: {job_id}, job_url: {job_url})")
2260
+ else:
2261
+ response = job_response.get("result", {})
2262
+ is_summary = "partitions" in response
2263
+
2264
+ except Exception as e:
2265
+ raise CLIException(FeedbackManager.error_exception(error=str(e)))
2266
+
2267
+
2268
+ async def print_current_branch(config: CLIConfig) -> None:
2269
+ _ = try_update_config_with_remote(config, only_if_needed=True)
2270
+
2271
+ response = config.get_client().user_workspaces_and_branches("v1")
2272
+
2273
+ columns = ["name", "id", "workspace"]
2274
+ table = []
2275
+
2276
+ for workspace in response["workspaces"]:
2277
+ if config["id"] == workspace["id"]:
2278
+ click.echo(FeedbackManager.info_current_branch())
2279
+ if workspace.get("is_branch"):
2280
+ name = workspace["name"]
2281
+ main_workspace = get_current_main_workspace(config)
2282
+ assert isinstance(main_workspace, dict)
2283
+ main_name = main_workspace["name"]
2284
+ else:
2285
+ name = MAIN_BRANCH
2286
+ main_name = workspace["name"]
2287
+ table.append([name, workspace["id"], main_name])
2288
+ break
2289
+
2290
+ echo_safe_humanfriendly_tables_format_smart_table(table, column_names=columns)