tinybird 0.0.1.dev17__tar.gz → 0.0.1.dev19__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.
Potentially problematic release.
This version of tinybird might be problematic. Click here for more details.
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/PKG-INFO +1 -1
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/client.py +22 -2
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/datafile.py +2 -0
- tinybird-0.0.1.dev19/tinybird/tb/__cli__.py +8 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/build.py +32 -10
- tinybird-0.0.1.dev19/tinybird/tb/modules/build_shell.py +368 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/cicd.py +9 -89
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/cli.py +3 -1
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/common.py +1 -108
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/create.py +2 -6
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/datafile/common.py +224 -247
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/datafile/parse_datasource.py +8 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/datafile/parse_pipe.py +10 -1
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/llm.py +4 -3
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/local_common.py +1 -1
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/login.py +17 -10
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/mock.py +14 -12
- tinybird-0.0.1.dev19/tinybird/tb/modules/test.py +230 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird.egg-info/PKG-INFO +1 -1
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird.egg-info/SOURCES.txt +1 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird.egg-info/requires.txt +1 -0
- tinybird-0.0.1.dev17/tinybird/tb/modules/build_shell.py +0 -149
- tinybird-0.0.1.dev17/tinybird/tb/modules/test.py +0 -138
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/setup.cfg +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/__cli__.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/ch_utils/constants.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/ch_utils/engine.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/check_pypi.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/config.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/connectors.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/context.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/datatypes.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/feedback_manager.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/git_settings.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/prompts.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/sql.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/sql_template.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/sql_template_fmt.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/sql_toolset.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/syncasync.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/cli.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/auth.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/config.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/connection.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/datafile/build.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/datafile/build_common.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/datafile/build_datasource.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/datafile/build_pipe.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/datafile/diff.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/datafile/exceptions.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/datafile/fixture.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/datafile/format_common.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/datafile/format_datasource.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/datafile/format_pipe.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/datafile/pipe_checker.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/datafile/pull.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/datasource.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/exceptions.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/fmt.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/job.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/local.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/pipe.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/regions.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/table.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/tag.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/telemetry.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/tinyunit/tinyunit.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/tinyunit/tinyunit_lib.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/token.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/workspace.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/workspace_members.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb_cli.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb_cli_modules/auth.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb_cli_modules/branch.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb_cli_modules/cicd.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb_cli_modules/cli.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb_cli_modules/common.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb_cli_modules/config.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb_cli_modules/connection.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb_cli_modules/datasource.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb_cli_modules/exceptions.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb_cli_modules/fmt.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb_cli_modules/job.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb_cli_modules/pipe.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb_cli_modules/regions.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb_cli_modules/tag.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb_cli_modules/telemetry.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb_cli_modules/test.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb_cli_modules/tinyunit/tinyunit.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb_cli_modules/tinyunit/tinyunit_lib.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb_cli_modules/workspace.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb_cli_modules/workspace_members.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tornado_template.py +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird.egg-info/dependency_links.txt +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird.egg-info/entry_points.txt +0 -0
- {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird.egg-info/top_level.txt +0 -0
|
@@ -103,7 +103,7 @@ class TinyB:
|
|
|
103
103
|
self.send_telemetry = send_telemetry
|
|
104
104
|
self.semver = semver
|
|
105
105
|
|
|
106
|
-
async def
|
|
106
|
+
async def _req_raw(
|
|
107
107
|
self,
|
|
108
108
|
endpoint: str,
|
|
109
109
|
data=None,
|
|
@@ -155,10 +155,15 @@ class TinyB:
|
|
|
155
155
|
response = await sync_to_async(session.get, thread_sensitive=False)(
|
|
156
156
|
url, verify=verify_ssl, **kwargs
|
|
157
157
|
)
|
|
158
|
-
|
|
159
158
|
except Exception as e:
|
|
160
159
|
raise e
|
|
161
160
|
|
|
161
|
+
if self.send_telemetry:
|
|
162
|
+
try:
|
|
163
|
+
add_telemetry_event("api_request", endpoint=url, token=self.token, status_code=response.status_code)
|
|
164
|
+
except Exception as ex:
|
|
165
|
+
logging.exception(f"Can't send telemetry: {ex}")
|
|
166
|
+
|
|
162
167
|
logging.debug("== server response ==")
|
|
163
168
|
logging.debug(response.content)
|
|
164
169
|
logging.debug("== end ==")
|
|
@@ -169,6 +174,21 @@ class TinyB:
|
|
|
169
174
|
except Exception as ex:
|
|
170
175
|
logging.exception(f"Can't send telemetry: {ex}")
|
|
171
176
|
|
|
177
|
+
return response
|
|
178
|
+
|
|
179
|
+
async def _req(
|
|
180
|
+
self,
|
|
181
|
+
endpoint: str,
|
|
182
|
+
data=None,
|
|
183
|
+
files=None,
|
|
184
|
+
method: str = "GET",
|
|
185
|
+
retries: int = LIMIT_RETRIES,
|
|
186
|
+
use_token: Optional[str] = None,
|
|
187
|
+
**kwargs,
|
|
188
|
+
):
|
|
189
|
+
token_to_use = use_token if use_token else self.token
|
|
190
|
+
response = await self._req_raw(endpoint, data, files, method, retries, use_token, **kwargs)
|
|
191
|
+
|
|
172
192
|
if response.status_code == 403:
|
|
173
193
|
error = parse_error_response(response)
|
|
174
194
|
if not token_to_use:
|
|
@@ -212,6 +212,7 @@ class ExportReplacements:
|
|
|
212
212
|
("export_file_template", "file_template", None),
|
|
213
213
|
("export_format", "format", "csv"),
|
|
214
214
|
("export_compression", "compression", None),
|
|
215
|
+
("export_write_strategy", "write_strategy", None),
|
|
215
216
|
("export_strategy", "strategy", "@new"),
|
|
216
217
|
("export_kafka_topic", "kafka_topic", None),
|
|
217
218
|
("kafka_connection_name", "connection", None),
|
|
@@ -1239,6 +1240,7 @@ def parse(
|
|
|
1239
1240
|
"export_format": assign_var("export_format"),
|
|
1240
1241
|
"export_strategy": assign_var("export_strategy"),
|
|
1241
1242
|
"export_compression": assign_var("export_compression"),
|
|
1243
|
+
"export_write_strategy": assign_var("export_write_strategy"),
|
|
1242
1244
|
"export_kafka_topic": assign_var("export_kafka_topic"),
|
|
1243
1245
|
}
|
|
1244
1246
|
|
|
@@ -6,7 +6,6 @@ from pathlib import Path
|
|
|
6
6
|
from typing import Any, Awaitable, Callable, List, Union
|
|
7
7
|
|
|
8
8
|
import click
|
|
9
|
-
from click import Context
|
|
10
9
|
from watchdog.events import FileSystemEventHandler
|
|
11
10
|
from watchdog.observers import Observer
|
|
12
11
|
|
|
@@ -16,7 +15,7 @@ from tinybird.config import FeatureFlags
|
|
|
16
15
|
from tinybird.feedback_manager import FeedbackManager
|
|
17
16
|
from tinybird.tb.modules.build_shell import BuildShell, print_table_formatted
|
|
18
17
|
from tinybird.tb.modules.cli import cli
|
|
19
|
-
from tinybird.tb.modules.common import
|
|
18
|
+
from tinybird.tb.modules.common import push_data
|
|
20
19
|
from tinybird.tb.modules.datafile.build import folder_build
|
|
21
20
|
from tinybird.tb.modules.datafile.common import get_project_filenames, get_project_fixtures, has_internal_datafiles
|
|
22
21
|
from tinybird.tb.modules.datafile.exceptions import ParseException
|
|
@@ -101,10 +100,7 @@ def watch_files(
|
|
|
101
100
|
is_flag=True,
|
|
102
101
|
help="Watch for changes in the files and re-check them.",
|
|
103
102
|
)
|
|
104
|
-
|
|
105
|
-
@coro
|
|
106
|
-
async def build(
|
|
107
|
-
ctx: Context,
|
|
103
|
+
def build(
|
|
108
104
|
folder: str,
|
|
109
105
|
watch: bool,
|
|
110
106
|
) -> None:
|
|
@@ -115,7 +111,7 @@ async def build(
|
|
|
115
111
|
context.disable_template_security_validation.set(True)
|
|
116
112
|
is_internal = has_internal_datafiles(folder)
|
|
117
113
|
folder_path = os.path.abspath(folder)
|
|
118
|
-
tb_client =
|
|
114
|
+
tb_client = asyncio.run(get_tinybird_local_client(folder_path))
|
|
119
115
|
|
|
120
116
|
def check_filenames(filenames: List[str]):
|
|
121
117
|
parser_matrix = {".pipe": parse_pipe, ".datasource": parse_datasource}
|
|
@@ -191,16 +187,42 @@ async def build(
|
|
|
191
187
|
ok = False
|
|
192
188
|
return ok
|
|
193
189
|
|
|
194
|
-
build_ok =
|
|
190
|
+
build_ok = asyncio.run(build_once(filenames))
|
|
195
191
|
|
|
196
192
|
if watch:
|
|
197
|
-
|
|
193
|
+
paths = [Path(f) for f in get_project_filenames(folder, with_vendor=True)]
|
|
194
|
+
|
|
195
|
+
def is_vendor(f: Path) -> bool:
|
|
196
|
+
return f.parts[0] == "vendor"
|
|
197
|
+
|
|
198
|
+
def get_vendor_workspace(f: Path) -> str:
|
|
199
|
+
return f.parts[1]
|
|
200
|
+
|
|
201
|
+
def is_endpoint(f: Path) -> bool:
|
|
202
|
+
return f.suffix == ".pipe" and not is_vendor(f) and f.parts[0] == "endpoints"
|
|
203
|
+
|
|
204
|
+
def is_pipe(f: Path) -> bool:
|
|
205
|
+
return f.suffix == ".pipe" and not is_vendor(f)
|
|
206
|
+
|
|
207
|
+
datasource_paths = [f for f in paths if f.suffix == ".datasource"]
|
|
208
|
+
datasources = [f.stem for f in datasource_paths if not is_vendor(f)]
|
|
209
|
+
shared_datasources = [f"{get_vendor_workspace(f)}.{f.stem}" for f in datasource_paths if is_vendor(f)]
|
|
210
|
+
pipes = [f.stem for f in paths if is_pipe(f) and not is_endpoint(f)]
|
|
211
|
+
endpoints = [f.stem for f in paths if is_endpoint(f)]
|
|
212
|
+
shell = BuildShell(
|
|
213
|
+
folder=folder,
|
|
214
|
+
client=tb_client,
|
|
215
|
+
datasources=datasources,
|
|
216
|
+
shared_datasources=shared_datasources,
|
|
217
|
+
pipes=pipes,
|
|
218
|
+
endpoints=endpoints,
|
|
219
|
+
)
|
|
198
220
|
click.echo(FeedbackManager.highlight(message="◎ Watching for changes..."))
|
|
199
221
|
watcher_thread = threading.Thread(
|
|
200
222
|
target=watch_files, args=(filenames, process, shell, folder, build_ok), daemon=True
|
|
201
223
|
)
|
|
202
224
|
watcher_thread.start()
|
|
203
|
-
shell.
|
|
225
|
+
shell.run_shell()
|
|
204
226
|
|
|
205
227
|
|
|
206
228
|
async def build_and_print_resource(tb_client: TinyB, filename: str):
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import concurrent.futures
|
|
3
|
+
import os
|
|
4
|
+
import random
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
from typing import List
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
import humanfriendly
|
|
11
|
+
from prompt_toolkit import PromptSession
|
|
12
|
+
from prompt_toolkit.completion import Completer, Completion
|
|
13
|
+
from prompt_toolkit.history import FileHistory
|
|
14
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
15
|
+
from prompt_toolkit.shortcuts import CompleteStyle
|
|
16
|
+
from prompt_toolkit.styles import Style
|
|
17
|
+
|
|
18
|
+
from tinybird.client import TinyB
|
|
19
|
+
from tinybird.feedback_manager import FeedbackManager, bcolors
|
|
20
|
+
from tinybird.tb.modules.exceptions import CLIException
|
|
21
|
+
from tinybird.tb.modules.table import format_table
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class DynamicCompleter(Completer):
|
|
25
|
+
def __init__(self, datasources: List[str], shared_datasources: List[str], endpoints: List[str], pipes: List[str]):
|
|
26
|
+
self.datasources = datasources
|
|
27
|
+
self.shared_datasources = shared_datasources
|
|
28
|
+
self.endpoints = endpoints
|
|
29
|
+
self.pipes = pipes
|
|
30
|
+
self.static_commands = ["create", "mock", "test", "select"]
|
|
31
|
+
self.test_commands = ["create", "run", "update"]
|
|
32
|
+
self.mock_flags = ["--prompt", "--rows"]
|
|
33
|
+
self.common_rows = ["10", "50", "100", "500", "1000"]
|
|
34
|
+
self.sql_keywords = ["select", "from", "where", "group by", "order by", "limit"]
|
|
35
|
+
|
|
36
|
+
def get_completions(self, document, complete_event):
|
|
37
|
+
text = document.text_before_cursor.strip()
|
|
38
|
+
words = text.split()
|
|
39
|
+
|
|
40
|
+
# Normalize command by removing 'tb' prefix if present
|
|
41
|
+
if words and words[0] == "tb":
|
|
42
|
+
words = words[1:]
|
|
43
|
+
|
|
44
|
+
if not words:
|
|
45
|
+
# Show all available commands when no input
|
|
46
|
+
yield from self._yield_static_commands("")
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
command = words[0].lower()
|
|
50
|
+
|
|
51
|
+
if command == "mock":
|
|
52
|
+
yield from self._handle_mock_completions(words)
|
|
53
|
+
elif command == "test":
|
|
54
|
+
yield from self._handle_test_completions(words)
|
|
55
|
+
elif command == "select" or self._is_sql_query(text.lower()):
|
|
56
|
+
yield from self._handle_sql_completions(text)
|
|
57
|
+
else:
|
|
58
|
+
# Handle general command completions
|
|
59
|
+
yield from self._yield_static_commands(words[-1])
|
|
60
|
+
|
|
61
|
+
def _is_sql_query(self, text: str) -> bool:
|
|
62
|
+
"""Check if the input looks like a SQL query."""
|
|
63
|
+
sql_starters = ["select", "with"]
|
|
64
|
+
return any(text.startswith(starter) for starter in sql_starters)
|
|
65
|
+
|
|
66
|
+
def _handle_sql_completions(self, text: str):
|
|
67
|
+
"""Handle completions for SQL queries."""
|
|
68
|
+
text_lower = text.lower()
|
|
69
|
+
|
|
70
|
+
# Find the last complete word
|
|
71
|
+
words = text_lower.split()
|
|
72
|
+
if not words:
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
# If we just typed 'from' or there's a space after 'from', suggest datasources
|
|
76
|
+
if words[-1] == "from" or (
|
|
77
|
+
"from" in words and len(words) > words.index("from") + 1 and text_lower.endswith(" ")
|
|
78
|
+
):
|
|
79
|
+
for x in self.datasources + self.shared_datasources:
|
|
80
|
+
yield Completion(x, start_position=0, display=x, style="class:completion.datasource")
|
|
81
|
+
for x in self.endpoints + self.pipes:
|
|
82
|
+
yield Completion(x, start_position=0, display=x, style="class:completion.pipe")
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
# If we're starting a query, suggest SQL keywords
|
|
86
|
+
if len(words) <= 2:
|
|
87
|
+
for keyword in self.sql_keywords:
|
|
88
|
+
if keyword.lower().startswith(words[-1]):
|
|
89
|
+
yield Completion(
|
|
90
|
+
keyword, start_position=-len(words[-1]), display=keyword, style="class:completion.keyword"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
def _handle_mock_completions(self, words: List[str]):
|
|
94
|
+
if len(words) == 1:
|
|
95
|
+
# After 'mock', show datasources
|
|
96
|
+
for ds in self.datasources:
|
|
97
|
+
yield Completion(ds, start_position=0, display=ds, style="class:completion.datasource")
|
|
98
|
+
return
|
|
99
|
+
|
|
100
|
+
if len(words) == 2 or len(words) == 4:
|
|
101
|
+
# After datasource or after a flag value, show available flags
|
|
102
|
+
available_flags = [f for f in self.mock_flags if f not in words]
|
|
103
|
+
for flag in available_flags:
|
|
104
|
+
yield Completion(flag, start_position=0, display=flag, style="class:completion.cmd")
|
|
105
|
+
return
|
|
106
|
+
|
|
107
|
+
last_word = words[-1]
|
|
108
|
+
if last_word == "--prompt":
|
|
109
|
+
yield Completion('""', start_position=0, display='"Enter your prompt..."', style="class:completion.cmd")
|
|
110
|
+
elif last_word == "--rows":
|
|
111
|
+
for rows in self.common_rows:
|
|
112
|
+
yield Completion(rows, start_position=0, display=rows, style="class:completion.cmd")
|
|
113
|
+
|
|
114
|
+
def _handle_test_completions(self, words: List[str]):
|
|
115
|
+
if len(words) == 1:
|
|
116
|
+
for cmd in self.test_commands:
|
|
117
|
+
yield Completion(cmd, start_position=0, display=cmd, style="class:completion.cmd")
|
|
118
|
+
return
|
|
119
|
+
elif len(words) == 2:
|
|
120
|
+
for cmd in self.endpoints:
|
|
121
|
+
yield Completion(cmd, start_position=0, display=cmd, style="class:completion.pipe")
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
def _yield_static_commands(self, current_word: str):
|
|
125
|
+
for cmd in self.static_commands:
|
|
126
|
+
if cmd.startswith(current_word):
|
|
127
|
+
yield Completion(
|
|
128
|
+
cmd,
|
|
129
|
+
start_position=-len(current_word) if current_word else 0,
|
|
130
|
+
display=cmd,
|
|
131
|
+
style="class:completion.cmd",
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
for cmd in self.datasources + self.shared_datasources:
|
|
135
|
+
if cmd.startswith(current_word):
|
|
136
|
+
yield Completion(
|
|
137
|
+
cmd,
|
|
138
|
+
start_position=-len(current_word) if current_word else 0,
|
|
139
|
+
display=cmd,
|
|
140
|
+
style="class:completion.datasource",
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
for cmd in self.endpoints + self.pipes:
|
|
144
|
+
if cmd.startswith(current_word):
|
|
145
|
+
yield Completion(
|
|
146
|
+
cmd,
|
|
147
|
+
start_position=-len(current_word) if current_word else 0,
|
|
148
|
+
display=cmd,
|
|
149
|
+
style="class:completion.pipe",
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
style = Style.from_dict(
|
|
154
|
+
{
|
|
155
|
+
"prompt": "fg:#34D399 bold",
|
|
156
|
+
"completion.cmd": "fg:#34D399 bg:#111111 bold",
|
|
157
|
+
"completion.datasource": "fg:#AB49D0 bg:#111111",
|
|
158
|
+
"completion.pipe": "fg:#FEA827 bg:#111111",
|
|
159
|
+
"completion.keyword": "fg:#34D399 bg:#111111",
|
|
160
|
+
}
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
key_bindings = KeyBindings()
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@key_bindings.add("c-d")
|
|
167
|
+
def _(event):
|
|
168
|
+
"""
|
|
169
|
+
Start auto completion. If the menu is showing already, select the next
|
|
170
|
+
completion.
|
|
171
|
+
"""
|
|
172
|
+
b = event.app.current_buffer
|
|
173
|
+
if b.complete_state:
|
|
174
|
+
b.complete_next()
|
|
175
|
+
else:
|
|
176
|
+
b.start_completion(select_first=False)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class BuildShell:
|
|
180
|
+
def __init__(
|
|
181
|
+
self,
|
|
182
|
+
folder: str,
|
|
183
|
+
client: TinyB,
|
|
184
|
+
datasources: List[str],
|
|
185
|
+
shared_datasources: List[str],
|
|
186
|
+
pipes: List[str],
|
|
187
|
+
endpoints: List[str],
|
|
188
|
+
):
|
|
189
|
+
self.history = self.get_history()
|
|
190
|
+
self.folder = folder
|
|
191
|
+
self.client = client
|
|
192
|
+
self.datasources = datasources
|
|
193
|
+
self.shared_datasources = shared_datasources
|
|
194
|
+
self.pipes = pipes
|
|
195
|
+
self.endpoints = endpoints
|
|
196
|
+
self.prompt_message = "\ntb > "
|
|
197
|
+
self.commands = ["create", "mock", "test", "tb", "select"]
|
|
198
|
+
|
|
199
|
+
self.session = PromptSession(
|
|
200
|
+
completer=DynamicCompleter(self.datasources, self.shared_datasources, self.endpoints, self.pipes),
|
|
201
|
+
complete_style=CompleteStyle.COLUMN,
|
|
202
|
+
complete_while_typing=True,
|
|
203
|
+
history=self.history,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
def get_history(self):
|
|
207
|
+
try:
|
|
208
|
+
history_file = os.path.expanduser("~/.tb_history")
|
|
209
|
+
return FileHistory(history_file)
|
|
210
|
+
except Exception:
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
def run_shell(self):
|
|
214
|
+
while True:
|
|
215
|
+
try:
|
|
216
|
+
user_input = self.session.prompt(
|
|
217
|
+
[("class:prompt", self.prompt_message)], style=style, key_bindings=key_bindings
|
|
218
|
+
)
|
|
219
|
+
self.handle_input(user_input)
|
|
220
|
+
except (EOFError, KeyboardInterrupt):
|
|
221
|
+
sys.exit(0)
|
|
222
|
+
except CLIException as e:
|
|
223
|
+
click.echo(str(e))
|
|
224
|
+
except Exception as e:
|
|
225
|
+
# Catch-all for unexpected exceptions
|
|
226
|
+
click.echo(FeedbackManager.error_exception(error=str(e)))
|
|
227
|
+
|
|
228
|
+
def handle_input(self, argline):
|
|
229
|
+
line = argline.strip()
|
|
230
|
+
if not line:
|
|
231
|
+
return
|
|
232
|
+
|
|
233
|
+
# Implement the command logic here
|
|
234
|
+
# Replace do_* methods with equivalent logic:
|
|
235
|
+
command_parts = line.split(maxsplit=1)
|
|
236
|
+
cmd = command_parts[0].lower()
|
|
237
|
+
arg = command_parts[1] if len(command_parts) > 1 else ""
|
|
238
|
+
|
|
239
|
+
if cmd in ["exit", "quit"]:
|
|
240
|
+
sys.exit(0)
|
|
241
|
+
elif cmd == "build":
|
|
242
|
+
self.handle_build(arg)
|
|
243
|
+
elif cmd == "auth":
|
|
244
|
+
self.handle_auth(arg)
|
|
245
|
+
elif cmd == "workspace":
|
|
246
|
+
self.handle_workspace(arg)
|
|
247
|
+
elif cmd == "mock":
|
|
248
|
+
self.handle_mock(arg)
|
|
249
|
+
elif cmd == "tb":
|
|
250
|
+
self.handle_tb(arg)
|
|
251
|
+
else:
|
|
252
|
+
# Check if it looks like a SQL query or run as a tb command
|
|
253
|
+
self.default(line)
|
|
254
|
+
|
|
255
|
+
def handle_build(self, arg):
|
|
256
|
+
click.echo(FeedbackManager.error(message=f"'tb {arg}' command is not available in watch mode"))
|
|
257
|
+
|
|
258
|
+
def handle_auth(self, arg):
|
|
259
|
+
click.echo(FeedbackManager.error(message=f"'tb {arg}' command is not available in watch mode"))
|
|
260
|
+
|
|
261
|
+
def handle_workspace(self, arg):
|
|
262
|
+
click.echo(FeedbackManager.error(message=f"'tb {arg}' command is not available in watch mode"))
|
|
263
|
+
|
|
264
|
+
def handle_mock(self, arg):
|
|
265
|
+
subprocess.run(f"tb mock {arg} --folder {self.folder}", shell=True, text=True)
|
|
266
|
+
|
|
267
|
+
def handle_tb(self, arg):
|
|
268
|
+
click.echo("")
|
|
269
|
+
arg = arg.strip().lower()
|
|
270
|
+
if arg.startswith("build"):
|
|
271
|
+
self.handle_build(arg)
|
|
272
|
+
elif arg.startswith("auth"):
|
|
273
|
+
self.handle_auth(arg)
|
|
274
|
+
elif arg.startswith("workspace"):
|
|
275
|
+
self.handle_workspace(arg)
|
|
276
|
+
elif arg.startswith("mock"):
|
|
277
|
+
self.handle_mock(arg)
|
|
278
|
+
else:
|
|
279
|
+
subprocess.run(f"tb --local {arg}", shell=True, text=True)
|
|
280
|
+
|
|
281
|
+
def default(self, argline):
|
|
282
|
+
click.echo("")
|
|
283
|
+
arg = argline.strip().lower()
|
|
284
|
+
if not arg:
|
|
285
|
+
return
|
|
286
|
+
if arg.startswith("with") or arg.startswith("select"):
|
|
287
|
+
self.run_sql(argline)
|
|
288
|
+
elif len(arg.split()) == 1 and arg in self.endpoints + self.pipes + self.datasources + self.shared_datasources:
|
|
289
|
+
self.run_sql(f"select * from {arg}")
|
|
290
|
+
else:
|
|
291
|
+
subprocess.run(f"tb --local {arg}", shell=True, text=True)
|
|
292
|
+
|
|
293
|
+
def run_sql(self, query, rows_limit=20):
|
|
294
|
+
try:
|
|
295
|
+
q = query.strip()
|
|
296
|
+
if q.lower().startswith("insert"):
|
|
297
|
+
click.echo(FeedbackManager.info_append_data())
|
|
298
|
+
raise CLIException(FeedbackManager.error_invalid_query())
|
|
299
|
+
if q.lower().startswith("delete"):
|
|
300
|
+
raise CLIException(FeedbackManager.error_invalid_query())
|
|
301
|
+
|
|
302
|
+
def run_query_in_thread():
|
|
303
|
+
loop = asyncio.new_event_loop()
|
|
304
|
+
asyncio.set_event_loop(loop)
|
|
305
|
+
try:
|
|
306
|
+
return loop.run_until_complete(
|
|
307
|
+
self.client.query(f"SELECT * FROM ({query}) LIMIT {rows_limit} FORMAT JSON")
|
|
308
|
+
)
|
|
309
|
+
finally:
|
|
310
|
+
loop.close()
|
|
311
|
+
|
|
312
|
+
with concurrent.futures.ThreadPoolExecutor() as executor:
|
|
313
|
+
res = executor.submit(run_query_in_thread).result()
|
|
314
|
+
|
|
315
|
+
except Exception as e:
|
|
316
|
+
click.echo(FeedbackManager.error_exception(error=str(e)))
|
|
317
|
+
|
|
318
|
+
if isinstance(res, dict) and "error" in res:
|
|
319
|
+
click.echo(FeedbackManager.error_exception(error=res["error"]))
|
|
320
|
+
|
|
321
|
+
if isinstance(res, dict) and "data" in res and res["data"]:
|
|
322
|
+
print_table_formatted(res, "QUERY")
|
|
323
|
+
else:
|
|
324
|
+
click.echo(FeedbackManager.info_no_rows())
|
|
325
|
+
|
|
326
|
+
def reprint_prompt(self):
|
|
327
|
+
click.echo(f"{bcolors.OKGREEN}{self.prompt_message}{bcolors.ENDC}", nl=False)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def print_table_formatted(res: dict, name: str):
|
|
331
|
+
rebuild_colors = [bcolors.FAIL, bcolors.OKBLUE, bcolors.WARNING, bcolors.OKGREEN, bcolors.HEADER]
|
|
332
|
+
rebuild_index = random.randint(0, len(rebuild_colors) - 1)
|
|
333
|
+
rebuild_color = rebuild_colors[rebuild_index % len(rebuild_colors)]
|
|
334
|
+
data = []
|
|
335
|
+
limit = 5
|
|
336
|
+
for d in res["data"][:5]:
|
|
337
|
+
data.append(d.values())
|
|
338
|
+
meta = res["meta"]
|
|
339
|
+
row_count = res.get("rows", 0)
|
|
340
|
+
stats = res.get("statistics", {})
|
|
341
|
+
elapsed = stats.get("elapsed", 0)
|
|
342
|
+
cols = len(meta)
|
|
343
|
+
try:
|
|
344
|
+
|
|
345
|
+
def print_message(message: str, color=bcolors.CGREY):
|
|
346
|
+
return f"{color}{message}{bcolors.ENDC}"
|
|
347
|
+
|
|
348
|
+
table = format_table(data, meta)
|
|
349
|
+
colored_char = print_message("│", rebuild_color)
|
|
350
|
+
table_with_marker = "\n".join(f"{colored_char} {line}" for line in table.split("\n"))
|
|
351
|
+
click.echo(f"\n{colored_char} {print_message('⚡', rebuild_color)} Running {name}")
|
|
352
|
+
click.echo(colored_char)
|
|
353
|
+
click.echo(table_with_marker)
|
|
354
|
+
click.echo(colored_char)
|
|
355
|
+
rows_read = humanfriendly.format_number(stats.get("rows_read", 0))
|
|
356
|
+
bytes_read = humanfriendly.format_size(stats.get("bytes_read", 0))
|
|
357
|
+
elapsed = humanfriendly.format_timespan(elapsed) if elapsed >= 1 else f"{elapsed * 1000:.2f}ms"
|
|
358
|
+
stats_message = f"» {bytes_read} ({rows_read} rows x {cols} cols) in {elapsed}"
|
|
359
|
+
rows_message = f"» Showing {limit} first rows" if row_count > limit else "» Showing all rows"
|
|
360
|
+
click.echo(f"{colored_char} {print_message(stats_message, bcolors.OKGREEN)}")
|
|
361
|
+
click.echo(f"{colored_char} {print_message(rows_message, bcolors.CGREY)}")
|
|
362
|
+
except ValueError as exc:
|
|
363
|
+
if str(exc) == "max() arg is an empty sequence":
|
|
364
|
+
click.echo("------------")
|
|
365
|
+
click.echo("Empty")
|
|
366
|
+
click.echo("------------")
|
|
367
|
+
else:
|
|
368
|
+
raise exc
|
|
@@ -14,9 +14,7 @@ class Provider(Enum):
|
|
|
14
14
|
GitLab = 1
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
WORKFLOW_VERSION = "
|
|
18
|
-
|
|
19
|
-
DEFAULT_REQUIREMENTS_FILE = "tinybird-cli>=5,<6"
|
|
17
|
+
WORKFLOW_VERSION = "v0.0.1"
|
|
20
18
|
|
|
21
19
|
GITHUB_CI_YML = """
|
|
22
20
|
name: Tinybird - CI Workflow
|
|
@@ -50,6 +48,8 @@ jobs:
|
|
|
50
48
|
run: curl -LsSf https://api.tinybird.co/static/install.sh | sh
|
|
51
49
|
- name: Build project
|
|
52
50
|
run: tb build
|
|
51
|
+
- name: Test project
|
|
52
|
+
run: tb test run
|
|
53
53
|
"""
|
|
54
54
|
|
|
55
55
|
|
|
@@ -64,110 +64,30 @@ stages:
|
|
|
64
64
|
|
|
65
65
|
GITLAB_CI_YML = """
|
|
66
66
|
tinybird_ci_workflow:
|
|
67
|
+
image: ubuntu:latest
|
|
67
68
|
stage: tests
|
|
68
69
|
interruptible: true
|
|
69
70
|
needs: []
|
|
70
71
|
rules:
|
|
71
|
-
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
72
|
+
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
72
73
|
changes:
|
|
73
|
-
- .gitlab/tinybird/*
|
|
74
|
+
- .gitlab/tinybird/*{% if data_project_dir != '.' %}
|
|
74
75
|
- {{ data_project_dir }}/*
|
|
75
76
|
- {{ data_project_dir }}/**/*{% end %}
|
|
76
77
|
before_script:
|
|
78
|
+
- apt update && apt install -y curl
|
|
77
79
|
- curl -LsSf https://api.tinybird.co/static/install.sh | sh
|
|
78
80
|
script:
|
|
81
|
+
- export PATH="$HOME/.local/bin:$PATH"
|
|
79
82
|
- cd $CI_PROJECT_DIR/{{ data_project_dir }}
|
|
80
83
|
- tb build
|
|
84
|
+
- tb test run
|
|
81
85
|
services:
|
|
82
86
|
- name: tinybirdco/tinybird-local:latest
|
|
83
87
|
alias: tinybird-local
|
|
84
88
|
"""
|
|
85
89
|
|
|
86
90
|
|
|
87
|
-
EXEC_TEST_SH = """
|
|
88
|
-
#!/usr/bin/env bash
|
|
89
|
-
set -euxo pipefail
|
|
90
|
-
|
|
91
|
-
export TB_VERSION_WARNING=0
|
|
92
|
-
|
|
93
|
-
run_test() {
|
|
94
|
-
t=$1
|
|
95
|
-
echo "** Running $t **"
|
|
96
|
-
echo "** $(cat $t)"
|
|
97
|
-
tmpfile=$(mktemp)
|
|
98
|
-
retries=0
|
|
99
|
-
TOTAL_RETRIES=3
|
|
100
|
-
|
|
101
|
-
# When appending fixtures, we need to retry in case of the data is not replicated in time
|
|
102
|
-
while [ $retries -lt $TOTAL_RETRIES ]; do
|
|
103
|
-
# Run the test and store the output in a temporary file
|
|
104
|
-
bash $t $2 >$tmpfile
|
|
105
|
-
exit_code=$?
|
|
106
|
-
if [ "$exit_code" -eq 0 ]; then
|
|
107
|
-
# If the test passed, break the loop
|
|
108
|
-
if diff -B ${t}.result $tmpfile >/dev/null 2>&1; then
|
|
109
|
-
break
|
|
110
|
-
# If the test failed, increment the retries counter and try again
|
|
111
|
-
else
|
|
112
|
-
retries=$((retries+1))
|
|
113
|
-
fi
|
|
114
|
-
# If the bash command failed, print an error message and break the loop
|
|
115
|
-
else
|
|
116
|
-
break
|
|
117
|
-
fi
|
|
118
|
-
done
|
|
119
|
-
|
|
120
|
-
if diff -B ${t}.result $tmpfile >/dev/null 2>&1; then
|
|
121
|
-
echo "✅ Test $t passed"
|
|
122
|
-
rm $tmpfile
|
|
123
|
-
return 0
|
|
124
|
-
elif [ $retries -eq $TOTAL_RETRIES ]; then
|
|
125
|
-
echo "🚨 ERROR: Test $t failed, diff:";
|
|
126
|
-
diff -B ${t}.result $tmpfile
|
|
127
|
-
rm $tmpfile
|
|
128
|
-
return 1
|
|
129
|
-
else
|
|
130
|
-
echo "🚨 ERROR: Test $t failed with bash command exit code $?"
|
|
131
|
-
cat $tmpfile
|
|
132
|
-
rm $tmpfile
|
|
133
|
-
return 1
|
|
134
|
-
fi
|
|
135
|
-
echo ""
|
|
136
|
-
}
|
|
137
|
-
export -f run_test
|
|
138
|
-
|
|
139
|
-
fail=0
|
|
140
|
-
find ./tests -name "*.test" -print0 | xargs -0 -I {} -P 4 bash -c 'run_test "$@"' _ {} || fail=1
|
|
141
|
-
|
|
142
|
-
if [ $fail == 1 ]; then
|
|
143
|
-
exit -1;
|
|
144
|
-
fi
|
|
145
|
-
"""
|
|
146
|
-
|
|
147
|
-
APPEND_FIXTURES_SH = """
|
|
148
|
-
#!/usr/bin/env bash
|
|
149
|
-
set -euxo pipefail
|
|
150
|
-
|
|
151
|
-
directory="datasources/fixtures"
|
|
152
|
-
extensions=("csv" "ndjson")
|
|
153
|
-
|
|
154
|
-
absolute_directory=$(realpath "$directory")
|
|
155
|
-
|
|
156
|
-
for extension in "${extensions[@]}"; do
|
|
157
|
-
file_list=$(find "$absolute_directory" -type f -name "*.$extension")
|
|
158
|
-
|
|
159
|
-
for file_path in $file_list; do
|
|
160
|
-
file_name=$(basename "$file_path")
|
|
161
|
-
file_name_without_extension="${file_name%.*}"
|
|
162
|
-
|
|
163
|
-
command="tb datasource append $file_name_without_extension datasources/fixtures/$file_name"
|
|
164
|
-
echo $command
|
|
165
|
-
$command
|
|
166
|
-
done
|
|
167
|
-
done
|
|
168
|
-
"""
|
|
169
|
-
|
|
170
|
-
|
|
171
91
|
class CICDFile:
|
|
172
92
|
def __init__(
|
|
173
93
|
self,
|
|
@@ -26,8 +26,9 @@ from tinybird.client import (
|
|
|
26
26
|
DoesNotExistException,
|
|
27
27
|
TinyB,
|
|
28
28
|
)
|
|
29
|
-
from tinybird.config import SUPPORTED_CONNECTORS,
|
|
29
|
+
from tinybird.config import SUPPORTED_CONNECTORS, FeatureFlags, get_config
|
|
30
30
|
from tinybird.feedback_manager import FeedbackManager
|
|
31
|
+
from tinybird.tb import __cli__
|
|
31
32
|
from tinybird.tb.modules.common import (
|
|
32
33
|
OLDEST_ROLLBACK,
|
|
33
34
|
CatchAuthExceptions,
|
|
@@ -70,6 +71,7 @@ __old_click_secho = click.secho
|
|
|
70
71
|
DEFAULT_PATTERNS: List[Tuple[str, Union[str, Callable[[str], str]]]] = [
|
|
71
72
|
(r"p\.ey[A-Za-z0-9-_\.]+", lambda v: f"{v[:4]}...{v[-8:]}")
|
|
72
73
|
]
|
|
74
|
+
VERSION = f"{__cli__.__version__} (rev {__cli__.__revision__})"
|
|
73
75
|
|
|
74
76
|
|
|
75
77
|
@click.group(cls=CatchAuthExceptions, context_settings={"help_option_names": ["-h", "--help"]})
|