tinybird 4.1.1.dev0__tar.gz → 4.2.1__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 (126) hide show
  1. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/PKG-INFO +17 -1
  2. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/__cli__.py +2 -2
  3. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/cli.py +1 -0
  4. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/create.py +3 -2
  5. tinybird-4.2.1/tinybird/tb/modules/logs.py +676 -0
  6. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird.egg-info/PKG-INFO +17 -1
  7. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird.egg-info/SOURCES.txt +1 -0
  8. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/setup.cfg +0 -0
  9. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/__cli__.py +0 -0
  10. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/ch_utils/constants.py +0 -0
  11. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/ch_utils/engine.py +0 -0
  12. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/check_pypi.py +0 -0
  13. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/client.py +0 -0
  14. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/config.py +0 -0
  15. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/context.py +0 -0
  16. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/datafile/common.py +0 -0
  17. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/datafile/exceptions.py +0 -0
  18. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/datafile/parse_connection.py +0 -0
  19. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/datafile/parse_datasource.py +0 -0
  20. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/datafile/parse_pipe.py +0 -0
  21. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/datatypes.py +0 -0
  22. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/feedback_manager.py +0 -0
  23. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/git_settings.py +0 -0
  24. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/prompts.py +0 -0
  25. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/service_datasources.py +0 -0
  26. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/sql.py +0 -0
  27. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/sql_template.py +0 -0
  28. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/sql_template_fmt.py +0 -0
  29. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/sql_toolset.py +0 -0
  30. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/syncasync.py +0 -0
  31. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/check_pypi.py +0 -0
  32. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/client.py +0 -0
  33. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/config.py +0 -0
  34. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/branch.py +0 -0
  35. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/build.py +0 -0
  36. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/build_common.py +0 -0
  37. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/cicd.py +0 -0
  38. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/cli.py +0 -0
  39. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/common.py +0 -0
  40. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/config.py +0 -0
  41. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/connection.py +0 -0
  42. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/connection_kafka.py +0 -0
  43. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/connection_s3.py +0 -0
  44. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/copy.py +0 -0
  45. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/datafile/build.py +0 -0
  46. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/datafile/build_common.py +0 -0
  47. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/datafile/build_datasource.py +0 -0
  48. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/datafile/build_pipe.py +0 -0
  49. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/datafile/diff.py +0 -0
  50. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/datafile/fixture.py +0 -0
  51. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/datafile/format_common.py +0 -0
  52. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/datafile/format_connection.py +0 -0
  53. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/datafile/format_datasource.py +0 -0
  54. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/datafile/format_pipe.py +0 -0
  55. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/datafile/pipe_checker.py +0 -0
  56. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/datafile/playground.py +0 -0
  57. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/datafile/pull.py +0 -0
  58. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/datasource.py +0 -0
  59. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/deployment.py +0 -0
  60. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/deployment_common.py +0 -0
  61. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/deprecations.py +0 -0
  62. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/endpoint.py +0 -0
  63. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/exceptions.py +0 -0
  64. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/feedback_manager.py +0 -0
  65. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/fmt.py +0 -0
  66. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/info.py +0 -0
  67. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/infra.py +0 -0
  68. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/job.py +0 -0
  69. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/job_common.py +0 -0
  70. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/llm.py +0 -0
  71. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/llm_utils.py +0 -0
  72. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/local.py +0 -0
  73. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/local_common.py +0 -0
  74. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/local_logs.py +0 -0
  75. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/login.py +0 -0
  76. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/login_common.py +0 -0
  77. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/logout.py +0 -0
  78. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/materialization.py +0 -0
  79. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/open.py +0 -0
  80. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/pipe.py +0 -0
  81. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/preview.py +0 -0
  82. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/project.py +0 -0
  83. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/project_commands.py +0 -0
  84. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/py_project.py +0 -0
  85. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/query_output.py +0 -0
  86. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/regions.py +0 -0
  87. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/secret.py +0 -0
  88. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/secret_common.py +0 -0
  89. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/sink.py +0 -0
  90. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/table.py +0 -0
  91. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/telemetry.py +0 -0
  92. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/test.py +0 -0
  93. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/test_common.py +0 -0
  94. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/tinyunit/tinyunit.py +0 -0
  95. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/tinyunit/tinyunit_lib.py +0 -0
  96. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/token.py +0 -0
  97. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/ts_project.py +0 -0
  98. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/watch.py +0 -0
  99. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/workspace.py +0 -0
  100. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/workspace_members.py +0 -0
  101. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb_cli.py +0 -0
  102. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb_cli_modules/auth.py +0 -0
  103. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb_cli_modules/branch.py +0 -0
  104. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb_cli_modules/cicd.py +0 -0
  105. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb_cli_modules/cli.py +0 -0
  106. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb_cli_modules/common.py +0 -0
  107. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb_cli_modules/config.py +0 -0
  108. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb_cli_modules/connection.py +0 -0
  109. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb_cli_modules/datasource.py +0 -0
  110. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb_cli_modules/exceptions.py +0 -0
  111. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb_cli_modules/fmt.py +0 -0
  112. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb_cli_modules/job.py +0 -0
  113. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb_cli_modules/pipe.py +0 -0
  114. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb_cli_modules/regions.py +0 -0
  115. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb_cli_modules/tag.py +0 -0
  116. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb_cli_modules/telemetry.py +0 -0
  117. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb_cli_modules/test.py +0 -0
  118. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb_cli_modules/tinyunit/tinyunit.py +0 -0
  119. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb_cli_modules/tinyunit/tinyunit_lib.py +0 -0
  120. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb_cli_modules/workspace.py +0 -0
  121. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb_cli_modules/workspace_members.py +0 -0
  122. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tornado_template.py +0 -0
  123. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird.egg-info/dependency_links.txt +0 -0
  124. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird.egg-info/entry_points.txt +0 -0
  125. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird.egg-info/requires.txt +0 -0
  126. {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: tinybird
3
- Version: 4.1.1.dev0
3
+ Version: 4.2.1
4
4
  Summary: Tinybird Command Line Tool
5
5
  Home-page: https://www.tinybird.co/docs/forward/commands
6
6
  Author: Tinybird
@@ -52,6 +52,12 @@ The Tinybird command-line tool allows you to use all the Tinybird functionality
52
52
  Changelog
53
53
  ----------
54
54
 
55
+ 4.1.1
56
+ *******
57
+
58
+ - `Changed` `tb init` now installs `tinybird-python-sdk-guidelines` when selecting the Python SDK project type.
59
+ - `Changed` `tb init` now defaults `dev_mode` to `branch` and persists that value in `tinybird.config.json`.
60
+
55
61
  4.1.0
56
62
  *******
57
63
 
@@ -95,6 +101,16 @@ Changelog
95
101
 
96
102
  - `Changed` Prompt-based AI flows now print a deprecation warning (`tb --prompt`, `tb create --prompt`, `tb datasource create --prompt`, `tb test create`, `tb mock`) ahead of their removal in a future release.
97
103
 
104
+ 3.3.2
105
+ *******
106
+
107
+ - `Added` `tb logs` command to query service observability data sources from the CLI.
108
+ - `Added` support for `--start`, `--end`, `--source`, `--limit`, and `--expand` in `tb logs`.
109
+ - `Added` support for `--source '*'` in `tb logs` to query all supported sources.
110
+ - `Changed` default `tb logs` sources to `tinybird.datasources_ops_log`, `tinybird.pipe_stats_rt`, and `tinybird.jobs_log`.
111
+ - `Changed` `tb logs` table output to show full datetime (`YYYY-MM-DD HH:MM:SS`) in `TIME` and to hide temporal fields from `DETAILS`.
112
+ - `Fixed` `tb logs` to correctly parse SQL responses returned as JSON strings.
113
+
98
114
  3.3.1
99
115
  *******
100
116
 
@@ -4,5 +4,5 @@ __description__ = 'Tinybird Command Line Tool'
4
4
  __url__ = 'https://www.tinybird.co/docs/forward/commands'
5
5
  __author__ = 'Tinybird'
6
6
  __author_email__ = 'support@tinybird.co'
7
- __version__ = '4.1.1.dev'
8
- __revision__ = '8993c4e'
7
+ __version__ = '4.2.1'
8
+ __revision__ = '7e04532'
@@ -16,6 +16,7 @@ import tinybird.tb.modules.job
16
16
  import tinybird.tb.modules.local
17
17
  import tinybird.tb.modules.login
18
18
  import tinybird.tb.modules.logout
19
+ import tinybird.tb.modules.logs
19
20
  import tinybird.tb.modules.materialization
20
21
  import tinybird.tb.modules.open
21
22
  import tinybird.tb.modules.pipe
@@ -18,7 +18,7 @@ from tinybird.tb.modules.project import Project
18
18
 
19
19
  DEFAULT_FOLDER = "tinybird"
20
20
  DEFAULT_SDK = "cli"
21
- DEFAULT_MODE = "manual"
21
+ DEFAULT_MODE = "branch"
22
22
  DEFAULT_CICD = "skip"
23
23
  SDK_CHOICES = ("typescript", "python", "cli")
24
24
  MODE_CHOICES = ("branch", "local", "manual")
@@ -32,6 +32,7 @@ SKILLS_INSTALL_BASE_ARGS = [
32
32
  GLOBAL_AGENT_SKILL = "tinybird"
33
33
  PROJECT_TYPE_AGENT_SKILLS = {
34
34
  "cli": "tinybird-cli-guidelines",
35
+ "python": "tinybird-python-sdk-guidelines",
35
36
  "typescript": "tinybird-typescript-sdk-guidelines",
36
37
  }
37
38
  SKILLS_INSTALL_TIMEOUT_SECONDS = 120
@@ -315,7 +316,7 @@ def _prompt_mode(mode: Optional[str]) -> str:
315
316
  click.echo(" [1] branch - Cloud branches mapped to your git feature branch")
316
317
  click.echo(" [2] local - Run build/test against Tinybird Local")
317
318
  click.echo(" [3] manual - Choose environment manually with flags")
318
- choice = click.prompt("\nSelect option", default=3, type=int)
319
+ choice = click.prompt("\nSelect option", default=1, type=int)
319
320
  if choice == 1:
320
321
  return "branch"
321
322
  if choice == 2:
@@ -0,0 +1,676 @@
1
+ # This is a command file for our CLI. Please keep it clean.
2
+ #
3
+ # - If it makes sense and only when strictly necessary, you can create utility functions in this file.
4
+ # - But please, **do not** interleave utility functions and command definitions.
5
+ import json
6
+ import re
7
+ import time
8
+ from contextlib import suppress
9
+ from datetime import datetime, timedelta, timezone
10
+ from typing import Any, Dict, List, Optional, Sequence, Set, Tuple
11
+
12
+ import click
13
+ import humanfriendly.tables
14
+ from click import Context
15
+
16
+ from tinybird.tb.client import TinyB
17
+ from tinybird.tb.modules.cli import cli
18
+ from tinybird.tb.modules.common import echo_json, force_echo
19
+ from tinybird.tb.modules.exceptions import CLIException
20
+ from tinybird.tb.modules.feedback_manager import FeedbackManager
21
+
22
+ LOG_SOURCES: Tuple[str, ...] = (
23
+ "tinybird.pipe_stats_rt",
24
+ "tinybird.bi_stats_rt",
25
+ "tinybird.block_log",
26
+ "tinybird.datasources_ops_log",
27
+ "tinybird.endpoint_errors",
28
+ "tinybird.kafka_ops_log",
29
+ "tinybird.sinks_ops_log",
30
+ "tinybird.jobs_log",
31
+ "tinybird.llm_usage",
32
+ )
33
+
34
+ DEFAULT_LOG_SOURCES: Tuple[str, ...] = (
35
+ "tinybird.datasources_ops_log",
36
+ "tinybird.pipe_stats_rt",
37
+ "tinybird.jobs_log",
38
+ )
39
+
40
+ TIMESTAMP_COLUMNS: Dict[str, str] = {
41
+ "tinybird.pipe_stats_rt": "start_datetime",
42
+ "tinybird.bi_stats_rt": "start_datetime",
43
+ "tinybird.block_log": "timestamp",
44
+ "tinybird.datasources_ops_log": "timestamp",
45
+ "tinybird.endpoint_errors": "start_datetime",
46
+ "tinybird.kafka_ops_log": "timestamp",
47
+ "tinybird.sinks_ops_log": "timestamp",
48
+ "tinybird.jobs_log": "created_at",
49
+ "tinybird.llm_usage": "start_time",
50
+ }
51
+
52
+ RELEVANT_DETAIL_FIELDS: Dict[str, Tuple[str, ...]] = {
53
+ "tinybird.pipe_stats_rt": (
54
+ "pipe_name",
55
+ "method",
56
+ "status_code",
57
+ "Error",
58
+ "error",
59
+ "duration",
60
+ "read_rows",
61
+ "read_bytes",
62
+ "result_rows",
63
+ ),
64
+ "tinybird.bi_stats_rt": (
65
+ "query_normalized",
66
+ "error_code",
67
+ "error",
68
+ "duration",
69
+ "read_rows",
70
+ "read_bytes",
71
+ "result_rows",
72
+ "result_bytes",
73
+ ),
74
+ "tinybird.block_log": (
75
+ "datasource_name",
76
+ "status",
77
+ "source",
78
+ "rows",
79
+ "bytes",
80
+ "processing_time",
81
+ "processing_error",
82
+ "quarantine_lines",
83
+ ),
84
+ "tinybird.datasources_ops_log": (
85
+ "event_type",
86
+ "datasource_name",
87
+ "result",
88
+ "elapsed_time",
89
+ "rows",
90
+ "error",
91
+ "pipe_name",
92
+ ),
93
+ "tinybird.endpoint_errors": (
94
+ "pipe_name",
95
+ "status_code",
96
+ "error",
97
+ "url",
98
+ "params",
99
+ ),
100
+ "tinybird.kafka_ops_log": (
101
+ "topic",
102
+ "partition",
103
+ "msg_type",
104
+ "lag",
105
+ "processed_messages",
106
+ "committed_messages",
107
+ "msg",
108
+ ),
109
+ "tinybird.sinks_ops_log": (
110
+ "service",
111
+ "pipe_name",
112
+ "result",
113
+ "error",
114
+ "elapsed_time",
115
+ "read_rows",
116
+ "written_rows",
117
+ ),
118
+ "tinybird.jobs_log": (
119
+ "job_id",
120
+ "job_type",
121
+ "status",
122
+ "pipe_name",
123
+ "error",
124
+ ),
125
+ "tinybird.llm_usage": (
126
+ "feature",
127
+ "origin",
128
+ "user_email",
129
+ "prompt_tokens",
130
+ "completion_tokens",
131
+ "total_tokens",
132
+ "duration",
133
+ "cost",
134
+ ),
135
+ }
136
+
137
+ LOG_TABLE_COLUMNS: Tuple[str, ...] = ("Timestamp", "Source", "Type", "Resource", "Result", "Duration", "Payload")
138
+
139
+ PRIMARY_LOG_FIELDS: Dict[str, Dict[str, Tuple[str, ...]]] = {
140
+ "tinybird.pipe_stats_rt": {
141
+ "type": ("method",),
142
+ "resource": ("pipe_name",),
143
+ "result": ("status_code",),
144
+ "duration": ("duration",),
145
+ },
146
+ "tinybird.datasources_ops_log": {
147
+ "type": ("event_type",),
148
+ "resource": ("datasource_name",),
149
+ "result": ("result",),
150
+ "duration": ("elapsed_time",),
151
+ },
152
+ "tinybird.kafka_ops_log": {
153
+ "type": ("msg_type",),
154
+ "resource": ("topic",),
155
+ "result": (),
156
+ "duration": ("time_write",),
157
+ },
158
+ "tinybird.sinks_ops_log": {
159
+ "type": ("service",),
160
+ "resource": ("pipe_name",),
161
+ "result": ("result",),
162
+ "duration": ("elapsed_time",),
163
+ },
164
+ "tinybird.jobs_log": {
165
+ "type": ("job_type",),
166
+ "resource": ("pipe_name",),
167
+ "result": ("status",),
168
+ "duration": (),
169
+ },
170
+ }
171
+
172
+ FALLBACK_PRIMARY_LOG_FIELDS: Dict[str, Tuple[str, ...]] = {
173
+ "type": ("event_type", "msg_type", "method", "job_type", "type"),
174
+ "resource": ("pipe_name", "datasource_name", "topic", "service", "resource"),
175
+ "result": ("result", "status", "status_code", "error"),
176
+ "duration": ("elapsed_time", "duration", "processing_time", "time_write"),
177
+ }
178
+
179
+ _RELATIVE_TIME_RE = re.compile(r"^-?(\d+)([mhdw])$")
180
+ _DETAILS_EXCLUDED_FIELDS = {
181
+ "start_datetime",
182
+ "timestamp",
183
+ "created_at",
184
+ "start_time",
185
+ "end_datetime",
186
+ "end_time",
187
+ "date",
188
+ "source",
189
+ }
190
+ _TEMPORAL_DETAIL_FIELDS = {
191
+ "date",
192
+ "datetime",
193
+ "timestamp",
194
+ "start_datetime",
195
+ "end_datetime",
196
+ "created_at",
197
+ "updated_at",
198
+ "started_at",
199
+ "ended_at",
200
+ "finished_at",
201
+ "start_time",
202
+ "end_time",
203
+ "event_date",
204
+ "query_last_execution",
205
+ "run_validation",
206
+ }
207
+ _DURATION_DETAIL_FIELDS = {"duration", "elapsed_time", "processing_time"}
208
+ _ROW_COUNT_DETAIL_FIELDS = {"rows", "read_rows", "written_rows", "result_rows", "quarantine_lines"}
209
+ _BYTE_COUNT_DETAIL_FIELDS = {"bytes", "read_bytes", "result_bytes", "written_bytes"}
210
+ _MILLISECOND_DURATION_SOURCES = {"tinybird.bi_stats_rt"}
211
+
212
+
213
+ def _to_iso_utc(dt: datetime) -> str:
214
+ return dt.astimezone(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
215
+
216
+
217
+ def _parse_relative_time(value: str, now: Optional[datetime] = None) -> str:
218
+ now = now or datetime.now(timezone.utc)
219
+ cleaned_value = value.strip()
220
+ match = _RELATIVE_TIME_RE.match(cleaned_value)
221
+ if not match:
222
+ return cleaned_value
223
+
224
+ amount = int(match.group(1))
225
+ unit = match.group(2)
226
+ delta_by_unit = {
227
+ "m": timedelta(minutes=amount),
228
+ "h": timedelta(hours=amount),
229
+ "d": timedelta(days=amount),
230
+ "w": timedelta(weeks=amount),
231
+ }
232
+ return _to_iso_utc(now - delta_by_unit[unit])
233
+
234
+
235
+ def _escape_sql_literal(value: str) -> str:
236
+ return value.replace("\\", "\\\\").replace("'", "\\'")
237
+
238
+
239
+ def _build_source_query(source: str, start_time: str, end_time: str) -> str:
240
+ timestamp_column = TIMESTAMP_COLUMNS[source]
241
+ escaped_start = _escape_sql_literal(start_time)
242
+ escaped_end = _escape_sql_literal(end_time)
243
+ return f"""
244
+ SELECT
245
+ '{source}' AS source,
246
+ {timestamp_column} AS timestamp,
247
+ formatRowNoNewline('JSONEachRow', *) AS data
248
+ FROM {source}
249
+ WHERE {timestamp_column} >= parseDateTimeBestEffort('{escaped_start}')
250
+ AND {timestamp_column} < parseDateTimeBestEffort('{escaped_end}')"""
251
+
252
+
253
+ def _build_query(sources: Sequence[str], start_time: str, end_time: str, limit: int) -> str:
254
+ query_parts = [_build_source_query(source, start_time, end_time) for source in sources]
255
+ source_union = "\n UNION ALL\n".join(query_parts)
256
+ return f"""
257
+ SELECT *
258
+ FROM (
259
+ {source_union}
260
+ )
261
+ ORDER BY timestamp DESC
262
+ LIMIT {limit}
263
+ FORMAT JSON"""
264
+
265
+
266
+ def _parse_sources(raw_sources: Sequence[str]) -> List[str]:
267
+ if not raw_sources:
268
+ return list(DEFAULT_LOG_SOURCES)
269
+
270
+ selected_sources: List[str] = []
271
+ for raw_source in raw_sources:
272
+ for source in raw_source.split(","):
273
+ cleaned_source = source.strip()
274
+ if not cleaned_source:
275
+ continue
276
+ if cleaned_source == "*":
277
+ return list(LOG_SOURCES)
278
+ if cleaned_source not in LOG_SOURCES:
279
+ raise CLIException(
280
+ FeedbackManager.error(
281
+ message=f"Unknown source '{cleaned_source}'. Valid sources: {', '.join(LOG_SOURCES)}"
282
+ )
283
+ )
284
+ if cleaned_source not in selected_sources:
285
+ selected_sources.append(cleaned_source)
286
+
287
+ if not selected_sources:
288
+ raise CLIException(FeedbackManager.error(message="At least one source is required"))
289
+
290
+ return selected_sources
291
+
292
+
293
+ def _format_time(timestamp_value: Any) -> str:
294
+ timestamp = str(timestamp_value)
295
+ try:
296
+ parsed_dt = datetime.fromisoformat(timestamp.replace("Z", "+00:00"))
297
+ return parsed_dt.strftime("%Y-%m-%d %H:%M:%S")
298
+ except ValueError:
299
+ return timestamp
300
+
301
+
302
+ def _normalize_json_value(value: Any) -> Any:
303
+ if isinstance(value, dict):
304
+ return {key: _normalize_json_value(val) for key, val in value.items()}
305
+ if isinstance(value, list):
306
+ return [_normalize_json_value(item) for item in value]
307
+ if isinstance(value, str):
308
+ stripped = value.strip()
309
+ if stripped and stripped[0] in {"{", "["}:
310
+ with suppress(json.JSONDecodeError):
311
+ return _normalize_json_value(json.loads(stripped))
312
+ return value
313
+
314
+
315
+ def _normalize_json_row(row: Dict[str, Any]) -> Dict[str, Any]:
316
+ normalized_row = dict(row)
317
+ normalized_row["data"] = _normalize_json_value(row.get("data"))
318
+ return normalized_row
319
+
320
+
321
+ def _truncate(value: str, width: int) -> str:
322
+ if width <= 3:
323
+ return value[:width]
324
+ if len(value) <= width:
325
+ return value
326
+ return f"{value[: width - 3]}..."
327
+
328
+
329
+ def _truncate_details(value: str, width: int) -> str:
330
+ if len(value) <= width:
331
+ return value
332
+
333
+ items = value.split(", ")
334
+ if not items:
335
+ return value[:width]
336
+
337
+ # Keep only whole values that fit in the available width.
338
+ kept: List[str] = []
339
+ for item in items:
340
+ candidate = item if not kept else f"{', '.join(kept)}, {item}"
341
+ if len(candidate) <= width:
342
+ kept.append(item)
343
+ continue
344
+ break
345
+
346
+ if not kept:
347
+ return value[:width]
348
+
349
+ return ", ".join(kept)
350
+
351
+
352
+ def _format_detail_value(value: Any) -> str:
353
+ if isinstance(value, bool):
354
+ return str(value).lower()
355
+ if isinstance(value, (int, float)):
356
+ return str(value)
357
+ if isinstance(value, (list, dict)):
358
+ return json.dumps(value, separators=(",", ":"))
359
+ return str(value).strip()
360
+
361
+
362
+ def _format_numeric_value(value: Any) -> str:
363
+ if isinstance(value, bool):
364
+ return str(value).lower()
365
+ if isinstance(value, int):
366
+ return f"{value:,}"
367
+ if isinstance(value, float):
368
+ normalized = int(value) if value.is_integer() else value
369
+ if isinstance(normalized, int):
370
+ return f"{normalized:,}"
371
+ return f"{normalized:,.3f}".rstrip("0").rstrip(".")
372
+ return _format_detail_value(value)
373
+
374
+
375
+ def _as_float(value: Any) -> Optional[float]:
376
+ if isinstance(value, bool):
377
+ return None
378
+ if isinstance(value, (int, float)):
379
+ return float(value)
380
+ if isinstance(value, str):
381
+ stripped = value.strip()
382
+ if not stripped:
383
+ return None
384
+ try:
385
+ return float(stripped)
386
+ except ValueError:
387
+ return None
388
+ return None
389
+
390
+
391
+ def _format_pretty_detail_value(key: str, value: Any, source: str) -> str:
392
+ base_key = key.strip().lower().split(".")[-1]
393
+ numeric_value = _as_float(value)
394
+
395
+ if base_key in _DURATION_DETAIL_FIELDS and numeric_value is not None:
396
+ if base_key == "duration" and source in _MILLISECOND_DURATION_SOURCES:
397
+ milliseconds = numeric_value
398
+ else:
399
+ milliseconds = numeric_value * 1000
400
+ return f"{_format_numeric_value(milliseconds)} ms"
401
+
402
+ if base_key in _ROW_COUNT_DETAIL_FIELDS and numeric_value is not None:
403
+ row_count = int(numeric_value) if float(numeric_value).is_integer() else numeric_value
404
+ return f"{_format_numeric_value(row_count)} rows"
405
+
406
+ if base_key in _BYTE_COUNT_DETAIL_FIELDS and numeric_value is not None:
407
+ byte_count = int(numeric_value) if float(numeric_value).is_integer() else numeric_value
408
+ return f"{_format_numeric_value(byte_count)} bytes"
409
+
410
+ return _format_detail_value(value)
411
+
412
+
413
+ def _select_detail_keys(parsed_data: Dict[str, Any], source: str, verbose: bool) -> List[str]:
414
+ visible_keys: List[str] = []
415
+ for key, value in parsed_data.items():
416
+ if value is None or value == "":
417
+ continue
418
+ if not verbose and _is_temporal_detail_field(key):
419
+ continue
420
+ visible_keys.append(key)
421
+
422
+ if verbose:
423
+ return visible_keys
424
+
425
+ preferred_keys = RELEVANT_DETAIL_FIELDS.get(source, ())
426
+ selected_keys: List[str] = [
427
+ key for key in preferred_keys if key in parsed_data and parsed_data.get(key) not in ("", None)
428
+ ]
429
+ return selected_keys or visible_keys
430
+
431
+
432
+ def _parse_data_object(data: Any) -> Optional[Dict[str, Any]]:
433
+ if isinstance(data, dict):
434
+ return data
435
+
436
+ text = str(data)
437
+ try:
438
+ parsed_data = json.loads(text)
439
+ except (json.JSONDecodeError, TypeError):
440
+ return None
441
+
442
+ return parsed_data if isinstance(parsed_data, dict) else None
443
+
444
+
445
+ def _find_matching_key(parsed_data: Dict[str, Any], key: str) -> Optional[str]:
446
+ if key in parsed_data:
447
+ return key
448
+
449
+ key_lower = key.lower()
450
+ for parsed_key in parsed_data:
451
+ if parsed_key.lower() == key_lower:
452
+ return parsed_key
453
+ return None
454
+
455
+
456
+ def _extract_primary_column_value(parsed_data: Dict[str, Any], source: str, column: str, used_keys: Set[str]) -> str:
457
+ source_fields = PRIMARY_LOG_FIELDS.get(source, {})
458
+ candidate_keys = source_fields[column] if column in source_fields else FALLBACK_PRIMARY_LOG_FIELDS.get(column, ())
459
+
460
+ for key in candidate_keys:
461
+ matching_key = _find_matching_key(parsed_data, key)
462
+ if matching_key is None:
463
+ continue
464
+
465
+ value = parsed_data.get(matching_key)
466
+ if value in ("", None):
467
+ continue
468
+
469
+ used_keys.add(matching_key)
470
+ if column == "duration":
471
+ return _format_pretty_detail_value(matching_key, value, source=source)
472
+ return _format_detail_value(value)
473
+
474
+ return "-"
475
+
476
+
477
+ def _summarize_payload(
478
+ parsed_data: Dict[str, Any], source: str, used_keys: Set[str], expand: bool, verbose: bool
479
+ ) -> str:
480
+ selected_keys = _select_detail_keys(parsed_data, source=source, verbose=verbose)
481
+ used_lower = {key.lower() for key in used_keys}
482
+ payload_keys = [
483
+ key for key in selected_keys if key.lower() not in used_lower and parsed_data.get(key) not in ("", None)
484
+ ]
485
+ if not payload_keys:
486
+ return "-"
487
+
488
+ values = [f"{key}: {_format_pretty_detail_value(key, parsed_data.get(key), source=source)}" for key in payload_keys]
489
+ summary = ", ".join(values)
490
+ return summary if expand else _truncate_details(summary, 120)
491
+
492
+
493
+ def _summarize_details(data: Any, source: str, expand: bool, verbose: bool) -> str:
494
+ text = str(data)
495
+ try:
496
+ parsed_data = json.loads(text)
497
+ except (json.JSONDecodeError, TypeError):
498
+ return text if expand else _truncate(text, 120)
499
+
500
+ if not isinstance(parsed_data, dict):
501
+ normalized = str(parsed_data)
502
+ return normalized if expand else _truncate(normalized, 120)
503
+
504
+ values: List[str] = []
505
+ for key in _select_detail_keys(parsed_data, source=source, verbose=verbose):
506
+ value = parsed_data.get(key)
507
+ formatted_value = _format_pretty_detail_value(key, value, source=source)
508
+ values.append(f"{key}: {formatted_value}" if verbose else formatted_value)
509
+
510
+ summary = ", ".join(values) if values else text
511
+ return summary
512
+
513
+
514
+ def _render_logs_table(rows: Sequence[Dict[str, Any]], expand: bool, verbose: bool) -> str:
515
+ rendered_rows: List[Tuple[str, str, str, str, str, str, str]] = []
516
+ for row in rows:
517
+ source = str(row.get("source", ""))
518
+ timestamp = _format_time(row.get("timestamp", ""))
519
+ parsed_data = _parse_data_object(row.get("data", ""))
520
+
521
+ if parsed_data is None:
522
+ payload = _format_detail_value(row.get("data", ""))
523
+ payload = payload if expand else _truncate(payload, 120)
524
+ rendered_rows.append((timestamp, source, "-", "-", "-", "-", payload))
525
+ continue
526
+
527
+ used_keys: Set[str] = set()
528
+ row_type = _extract_primary_column_value(parsed_data, source, "type", used_keys)
529
+ resource = _extract_primary_column_value(parsed_data, source, "resource", used_keys)
530
+ result = _extract_primary_column_value(parsed_data, source, "result", used_keys)
531
+ duration = _extract_primary_column_value(parsed_data, source, "duration", used_keys)
532
+ payload = _summarize_payload(parsed_data, source, used_keys, expand=expand, verbose=verbose)
533
+
534
+ rendered_rows.append((timestamp, source, row_type, resource, result, duration, payload))
535
+
536
+ return humanfriendly.tables.format_pretty_table(rendered_rows, column_names=list(LOG_TABLE_COLUMNS))
537
+
538
+
539
+ def _resolve_environment_label(ctx: Context) -> str:
540
+ env = ctx.ensure_object(dict).get("env", "local")
541
+ branch = ctx.ensure_object(dict).get("branch")
542
+ if env == "cloud" and branch:
543
+ return f"branch:{branch}"
544
+ return str(env)
545
+
546
+
547
+ def _is_temporal_detail_field(key: str) -> bool:
548
+ normalized_key = key.strip().lower()
549
+ base_key = normalized_key.split(".")[-1]
550
+
551
+ if base_key in _DETAILS_EXCLUDED_FIELDS:
552
+ return True
553
+ if base_key in _TEMPORAL_DETAIL_FIELDS:
554
+ return True
555
+ if base_key.endswith("_at"):
556
+ return True
557
+ if base_key.endswith("_timestamp"):
558
+ return True
559
+ return bool(base_key.endswith("_date"))
560
+
561
+
562
+ @cli.command(name="logs")
563
+ @click.option(
564
+ "-s",
565
+ "--start",
566
+ "start_time",
567
+ default="-1h",
568
+ show_default=True,
569
+ help="Start time (relative: -1h, -30m, -1d, -7d or ISO 8601).",
570
+ )
571
+ @click.option(
572
+ "-e",
573
+ "--end",
574
+ "end_time",
575
+ default=None,
576
+ help="End time (relative or ISO 8601).",
577
+ )
578
+ @click.option(
579
+ "--source",
580
+ "sources",
581
+ multiple=True,
582
+ help=f"Comma-separated or repeated list of sources. Use '*' for all. Available: {', '.join(LOG_SOURCES)}",
583
+ )
584
+ @click.option(
585
+ "-n",
586
+ "--limit",
587
+ default=100,
588
+ show_default=True,
589
+ type=click.IntRange(1, 1000),
590
+ help="Maximum rows to return.",
591
+ )
592
+ @click.option("-x", "--expand", is_flag=True, default=False, help="Show full details without truncation.")
593
+ @click.option("-v", "--verbose", is_flag=True, default=False, help="Show all fields in details with property names.")
594
+ @click.pass_context
595
+ def logs(
596
+ ctx: Context,
597
+ start_time: str,
598
+ end_time: Optional[str],
599
+ sources: Tuple[str, ...],
600
+ limit: int,
601
+ expand: bool,
602
+ verbose: bool,
603
+ ) -> None:
604
+ """Query Tinybird real-time service logs."""
605
+ output = ctx.ensure_object(dict)["output"]
606
+ if output not in {"human", "json"}:
607
+ force_echo(FeedbackManager.error_invalid_output_format(formats=", ".join(["human", "json"])))
608
+ return
609
+
610
+ client: TinyB = ctx.ensure_object(dict)["client"]
611
+ now = datetime.now(timezone.utc)
612
+
613
+ resolved_start = _parse_relative_time(start_time, now)
614
+ resolved_end = _parse_relative_time(end_time, now) if end_time else _to_iso_utc(now)
615
+ resolved_sources = _parse_sources(sources)
616
+ environment = _resolve_environment_label(ctx)
617
+ query = _build_query(resolved_sources, resolved_start, resolved_end, limit)
618
+
619
+ started = time.monotonic()
620
+ try:
621
+ result = client.query(query)
622
+ except Exception as e:
623
+ raise CLIException(FeedbackManager.error_exception(error=str(e)))
624
+
625
+ if isinstance(result, str):
626
+ try:
627
+ result = json.loads(result)
628
+ except json.JSONDecodeError:
629
+ raise CLIException(FeedbackManager.error_exception(error=result))
630
+
631
+ if not isinstance(result, dict):
632
+ raise CLIException(FeedbackManager.error_exception(error="Unexpected response format while querying logs"))
633
+ if "error" in result:
634
+ raise CLIException(FeedbackManager.error_exception(error=str(result["error"])))
635
+
636
+ rows = [row for row in result.get("data", []) if isinstance(row, dict)]
637
+ reported_rows = result.get("rows")
638
+ rows_count = len(rows)
639
+ if isinstance(reported_rows, int) and reported_rows >= 0:
640
+ rows_count = reported_rows
641
+ elif isinstance(reported_rows, str) and reported_rows.isdigit():
642
+ rows_count = int(reported_rows)
643
+ if not rows:
644
+ rows_count = 0
645
+ statistics = result.get("statistics", {})
646
+ elapsed_seconds = statistics.get("elapsed") if isinstance(statistics, dict) else None
647
+ if not isinstance(elapsed_seconds, (float, int)):
648
+ elapsed_seconds = time.monotonic() - started
649
+
650
+ payload = {
651
+ "environment": environment,
652
+ "query": {
653
+ "start": resolved_start,
654
+ "end": resolved_end,
655
+ "sources": resolved_sources,
656
+ "limit": limit,
657
+ "verbose": verbose,
658
+ },
659
+ "statistics": statistics if isinstance(statistics, dict) else {},
660
+ "rows": rows_count,
661
+ "data": [_normalize_json_row(row) for row in rows],
662
+ }
663
+
664
+ if output == "json":
665
+ echo_json(payload, indent=8)
666
+ return
667
+
668
+ if not rows:
669
+ click.echo("No logs found for the specified time range.")
670
+ click.echo(f"\nQueried {len(resolved_sources)} source(s) from {environment} in {elapsed_seconds:.1f}s")
671
+ return
672
+
673
+ click.echo(_render_logs_table(rows, expand=expand, verbose=verbose))
674
+ click.echo(
675
+ f"\nFetched {rows_count} logs from {len(resolved_sources)} source(s) in {elapsed_seconds:.1f}s ({environment})"
676
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: tinybird
3
- Version: 4.1.1.dev0
3
+ Version: 4.2.1
4
4
  Summary: Tinybird Command Line Tool
5
5
  Home-page: https://www.tinybird.co/docs/forward/commands
6
6
  Author: Tinybird
@@ -52,6 +52,12 @@ The Tinybird command-line tool allows you to use all the Tinybird functionality
52
52
  Changelog
53
53
  ----------
54
54
 
55
+ 4.1.1
56
+ *******
57
+
58
+ - `Changed` `tb init` now installs `tinybird-python-sdk-guidelines` when selecting the Python SDK project type.
59
+ - `Changed` `tb init` now defaults `dev_mode` to `branch` and persists that value in `tinybird.config.json`.
60
+
55
61
  4.1.0
56
62
  *******
57
63
 
@@ -95,6 +101,16 @@ Changelog
95
101
 
96
102
  - `Changed` Prompt-based AI flows now print a deprecation warning (`tb --prompt`, `tb create --prompt`, `tb datasource create --prompt`, `tb test create`, `tb mock`) ahead of their removal in a future release.
97
103
 
104
+ 3.3.2
105
+ *******
106
+
107
+ - `Added` `tb logs` command to query service observability data sources from the CLI.
108
+ - `Added` support for `--start`, `--end`, `--source`, `--limit`, and `--expand` in `tb logs`.
109
+ - `Added` support for `--source '*'` in `tb logs` to query all supported sources.
110
+ - `Changed` default `tb logs` sources to `tinybird.datasources_ops_log`, `tinybird.pipe_stats_rt`, and `tinybird.jobs_log`.
111
+ - `Changed` `tb logs` table output to show full datetime (`YYYY-MM-DD HH:MM:SS`) in `TIME` and to hide temporal fields from `DETAILS`.
112
+ - `Fixed` `tb logs` to correctly parse SQL responses returned as JSON strings.
113
+
98
114
  3.3.1
99
115
  *******
100
116
 
@@ -65,6 +65,7 @@ tinybird/tb/modules/local_logs.py
65
65
  tinybird/tb/modules/login.py
66
66
  tinybird/tb/modules/login_common.py
67
67
  tinybird/tb/modules/logout.py
68
+ tinybird/tb/modules/logs.py
68
69
  tinybird/tb/modules/materialization.py
69
70
  tinybird/tb/modules/open.py
70
71
  tinybird/tb/modules/pipe.py
File without changes
File without changes