tinybird 0.0.1.dev188__py3-none-any.whl → 0.0.1.dev190__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of tinybird might be problematic. Click here for more details.
- tinybird/tb/__cli__.py +2 -2
- tinybird/tb/client.py +1 -5
- tinybird/tb/modules/build.py +9 -0
- tinybird/tb/modules/common.py +17 -5
- tinybird/tb/modules/connection.py +20 -0
- tinybird/tb/modules/create.py +23 -5
- tinybird/tb/modules/datasource.py +442 -13
- tinybird/tb/modules/deployment.py +8 -0
- tinybird/tb/modules/project.py +44 -0
- {tinybird-0.0.1.dev188.dist-info → tinybird-0.0.1.dev190.dist-info}/METADATA +1 -1
- {tinybird-0.0.1.dev188.dist-info → tinybird-0.0.1.dev190.dist-info}/RECORD +14 -14
- {tinybird-0.0.1.dev188.dist-info → tinybird-0.0.1.dev190.dist-info}/WHEEL +0 -0
- {tinybird-0.0.1.dev188.dist-info → tinybird-0.0.1.dev190.dist-info}/entry_points.txt +0 -0
- {tinybird-0.0.1.dev188.dist-info → tinybird-0.0.1.dev190.dist-info}/top_level.txt +0 -0
tinybird/tb/__cli__.py
CHANGED
|
@@ -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__ = '0.0.1.
|
|
8
|
-
__revision__ = '
|
|
7
|
+
__version__ = '0.0.1.dev190'
|
|
8
|
+
__revision__ = '8a00bcf'
|
tinybird/tb/client.py
CHANGED
|
@@ -1024,11 +1024,7 @@ class TinyB:
|
|
|
1024
1024
|
)
|
|
1025
1025
|
|
|
1026
1026
|
async def kafka_list_topics(self, connection_id: str, timeout=5):
|
|
1027
|
-
resp = await self._req(
|
|
1028
|
-
f"/v0/connectors/{connection_id}/preview?preview_activity=false",
|
|
1029
|
-
connect_timeout=timeout,
|
|
1030
|
-
request_timeout=timeout,
|
|
1031
|
-
)
|
|
1027
|
+
resp = await self._req(f"/v0/connectors/{connection_id}/preview?preview_activity=false", timeout=timeout)
|
|
1032
1028
|
return [x["topic"] for x in resp["preview"]]
|
|
1033
1029
|
|
|
1034
1030
|
async def get_gcp_service_account_details(self) -> Dict[str, Any]:
|
tinybird/tb/modules/build.py
CHANGED
|
@@ -38,6 +38,15 @@ def build(ctx: click.Context, watch: bool) -> None:
|
|
|
38
38
|
"""
|
|
39
39
|
project: Project = ctx.ensure_object(dict)["project"]
|
|
40
40
|
tb_client: TinyB = ctx.ensure_object(dict)["client"]
|
|
41
|
+
|
|
42
|
+
if project.has_deeper_level():
|
|
43
|
+
click.echo(
|
|
44
|
+
FeedbackManager.warning(
|
|
45
|
+
message="Default max_depth is 2, but project has deeper level. "
|
|
46
|
+
"You can change max_depth by running `tb --max-depth <depth> <cmd>`"
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
|
|
41
50
|
click.echo(FeedbackManager.highlight_building_project())
|
|
42
51
|
process(project=project, tb_client=tb_client, watch=False)
|
|
43
52
|
if watch:
|
tinybird/tb/modules/common.py
CHANGED
|
@@ -424,6 +424,14 @@ async def _analyze(filename: str, client: TinyB, format: str, connector: Optiona
|
|
|
424
424
|
return meta, data
|
|
425
425
|
|
|
426
426
|
|
|
427
|
+
async def analyze_file(filename: str, client: TinyB, format: str):
|
|
428
|
+
meta, data = await _analyze(filename, client, format)
|
|
429
|
+
schema = meta["analysis"]["schema"]
|
|
430
|
+
schema = schema.replace(", ", ",\n ")
|
|
431
|
+
content = f"""DESCRIPTION >\n Generated from {filename}\n\nSCHEMA >\n {schema}"""
|
|
432
|
+
return content
|
|
433
|
+
|
|
434
|
+
|
|
427
435
|
async def _generate_datafile(
|
|
428
436
|
filename: str,
|
|
429
437
|
client: TinyB,
|
|
@@ -971,7 +979,7 @@ async def push_data(
|
|
|
971
979
|
cb.prev_done = 0 # type: ignore[attr-defined]
|
|
972
980
|
|
|
973
981
|
if not silent:
|
|
974
|
-
click.echo(FeedbackManager.
|
|
982
|
+
click.echo(FeedbackManager.highlight(message=f"\n» Appending data to {datasource_name}..."))
|
|
975
983
|
|
|
976
984
|
if isinstance(url, list):
|
|
977
985
|
urls = url
|
|
@@ -1882,15 +1890,19 @@ def get_gcs_connection_name(project_folder) -> str:
|
|
|
1882
1890
|
return get_connection_name(project_folder=project_folder, connection_type="GCS")
|
|
1883
1891
|
|
|
1884
1892
|
|
|
1885
|
-
def
|
|
1886
|
-
|
|
1893
|
+
def get_kafka_connection_name(project_folder: str, connection_name: Optional[str] = None) -> str:
|
|
1894
|
+
return get_connection_name(project_folder=project_folder, connection_type="KAFKA", connection_name=connection_name)
|
|
1895
|
+
|
|
1896
|
+
|
|
1897
|
+
def get_connection_name(project_folder: str, connection_type: str, connection_name: Optional[str] = None) -> str:
|
|
1887
1898
|
valid_pattern = r"^[a-zA-Z][a-zA-Z0-9_]*$"
|
|
1888
1899
|
|
|
1889
1900
|
while not connection_name:
|
|
1901
|
+
short_id = str(uuid.uuid4())[:4]
|
|
1890
1902
|
connection_name = click.prompt(
|
|
1891
|
-
|
|
1892
|
-
prompt_suffix="\n> ",
|
|
1903
|
+
"Enter a name (only alphanumeric characters and underscores)",
|
|
1893
1904
|
show_default=True,
|
|
1905
|
+
default=f"{connection_type.lower()}_{short_id}",
|
|
1894
1906
|
)
|
|
1895
1907
|
assert isinstance(connection_name, str)
|
|
1896
1908
|
|
|
@@ -18,6 +18,7 @@ from tinybird.tb.modules.common import (
|
|
|
18
18
|
echo_safe_humanfriendly_tables_format_smart_table,
|
|
19
19
|
get_gcs_connection_name,
|
|
20
20
|
get_gcs_svc_account_creds,
|
|
21
|
+
get_kafka_connection_name,
|
|
21
22
|
get_s3_connection_name,
|
|
22
23
|
production_aws_iamrole_only,
|
|
23
24
|
run_aws_iamrole_connection_flow,
|
|
@@ -26,6 +27,7 @@ from tinybird.tb.modules.common import (
|
|
|
26
27
|
from tinybird.tb.modules.create import (
|
|
27
28
|
generate_aws_iamrole_connection_file_with_secret,
|
|
28
29
|
generate_gcs_connection_file_with_secrets,
|
|
30
|
+
generate_kafka_connection_with_secrets,
|
|
29
31
|
)
|
|
30
32
|
from tinybird.tb.modules.feedback_manager import FeedbackManager
|
|
31
33
|
from tinybird.tb.modules.project import Project
|
|
@@ -275,3 +277,21 @@ async def connection_create_gcs(ctx: Context) -> None:
|
|
|
275
277
|
connection_path=connection_path,
|
|
276
278
|
)
|
|
277
279
|
)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
@connection_create.command(name="kafka", short_help="Creates a Kafka connection.")
|
|
283
|
+
@click.option("--name", help="The name of the connection")
|
|
284
|
+
@click.pass_context
|
|
285
|
+
@coro
|
|
286
|
+
async def connection_create_kafka(ctx: Context, name: Optional[str] = None) -> None:
|
|
287
|
+
"""
|
|
288
|
+
Creates a Kafka connection.
|
|
289
|
+
|
|
290
|
+
\b
|
|
291
|
+
$ tb connection create kafka
|
|
292
|
+
"""
|
|
293
|
+
click.echo(FeedbackManager.highlight(message="» Creating Kafka connection..."))
|
|
294
|
+
project: Project = ctx.ensure_object(dict)["project"]
|
|
295
|
+
name = get_kafka_connection_name(project.folder, name)
|
|
296
|
+
await generate_kafka_connection_with_secrets(name=name, folder=project.folder)
|
|
297
|
+
click.echo(FeedbackManager.success(message="✓ Done!"))
|
tinybird/tb/modules/create.py
CHANGED
|
@@ -409,10 +409,14 @@ def generate_connection_file(name: str, content: str, folder: str, skip_feedback
|
|
|
409
409
|
|
|
410
410
|
|
|
411
411
|
async def generate_aws_iamrole_connection_file_with_secret(
|
|
412
|
-
name: str, service: str, role_arn_secret_name: str, region: str, folder: str
|
|
412
|
+
name: str, service: str, role_arn_secret_name: str, region: str, folder: str, with_default_secret: bool = False
|
|
413
413
|
) -> Path:
|
|
414
|
+
if with_default_secret:
|
|
415
|
+
default_secret = ', "arn:aws:iam::123456789012:role/my-role"'
|
|
416
|
+
else:
|
|
417
|
+
default_secret = ""
|
|
414
418
|
content = f"""TYPE {service}
|
|
415
|
-
S3_ARN {{{{ tb_secret("{role_arn_secret_name}") }}}}
|
|
419
|
+
S3_ARN {{{{ tb_secret("{role_arn_secret_name}"{default_secret}) }}}}
|
|
416
420
|
S3_REGION {region}
|
|
417
421
|
"""
|
|
418
422
|
file_path = generate_connection_file(name, content, folder, skip_feedback=True)
|
|
@@ -483,6 +487,7 @@ async def create_resources_from_data(
|
|
|
483
487
|
data: str,
|
|
484
488
|
project: Project,
|
|
485
489
|
config: Dict[str, Any],
|
|
490
|
+
skip_pipes: bool = False,
|
|
486
491
|
) -> List[Path]:
|
|
487
492
|
local_client = await get_tinybird_local_client(config)
|
|
488
493
|
folder_path = project.path
|
|
@@ -495,7 +500,7 @@ async def create_resources_from_data(
|
|
|
495
500
|
result.append(ds_file)
|
|
496
501
|
name = ds_file.stem
|
|
497
502
|
no_pipes = len(project.get_pipe_files()) == 0
|
|
498
|
-
if no_pipes:
|
|
503
|
+
if not skip_pipes and no_pipes:
|
|
499
504
|
pipe_file = generate_pipe_file(
|
|
500
505
|
f"{name}_endpoint",
|
|
501
506
|
f"""
|
|
@@ -510,7 +515,9 @@ TYPE ENDPOINT
|
|
|
510
515
|
return result
|
|
511
516
|
|
|
512
517
|
|
|
513
|
-
async def create_resources_from_url(
|
|
518
|
+
async def create_resources_from_url(
|
|
519
|
+
url: str, project: Project, config: Dict[str, Any], skip_pipes: bool = False
|
|
520
|
+
) -> List[Path]:
|
|
514
521
|
result: List[Path] = []
|
|
515
522
|
local_client = await get_tinybird_local_client(config)
|
|
516
523
|
format = url.split(".")[-1]
|
|
@@ -518,7 +525,7 @@ async def create_resources_from_url(url: str, project: Project, config: Dict[str
|
|
|
518
525
|
result.append(ds_file)
|
|
519
526
|
name = ds_file.stem
|
|
520
527
|
no_pipes = len(project.get_pipe_files()) == 0
|
|
521
|
-
if no_pipes:
|
|
528
|
+
if not skip_pipes and no_pipes:
|
|
522
529
|
pipe_file = generate_pipe_file(
|
|
523
530
|
f"{name}_endpoint",
|
|
524
531
|
f"""
|
|
@@ -531,3 +538,14 @@ TYPE ENDPOINT
|
|
|
531
538
|
)
|
|
532
539
|
result.append(pipe_file)
|
|
533
540
|
return result
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
async def generate_kafka_connection_with_secrets(name: str, folder: str) -> Path:
|
|
544
|
+
content = """TYPE kafka
|
|
545
|
+
KAFKA_BOOTSTRAP_SERVERS {{ tb_secret("KAFKA_SERVERS", "localhost:9092") }}
|
|
546
|
+
KAFKA_SECURITY_PROTOCOL SASL_SSL
|
|
547
|
+
KAFKA_SASL_MECHANISM PLAIN
|
|
548
|
+
KAFKA_KEY {{ tb_secret("KAFKA_USERNAME", "") }}
|
|
549
|
+
KAFKA_SECRET {{ tb_secret("KAFKA_PASSWORD", "") }}
|
|
550
|
+
"""
|
|
551
|
+
return generate_connection_file(name, content, folder, skip_feedback=True)
|
|
@@ -7,22 +7,37 @@ import asyncio
|
|
|
7
7
|
import json
|
|
8
8
|
import os
|
|
9
9
|
import re
|
|
10
|
+
import uuid
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from pathlib import Path
|
|
10
13
|
from typing import Optional
|
|
14
|
+
from urllib.parse import urlparse
|
|
11
15
|
|
|
12
16
|
import click
|
|
13
17
|
import humanfriendly
|
|
18
|
+
import requests
|
|
14
19
|
from click import Context
|
|
15
20
|
|
|
21
|
+
from tinybird.syncasync import sync_to_async
|
|
16
22
|
from tinybird.tb.client import AuthNoTokenException, DoesNotExistException, TinyB
|
|
17
23
|
from tinybird.tb.modules.cli import cli
|
|
18
24
|
from tinybird.tb.modules.common import (
|
|
19
25
|
_analyze,
|
|
26
|
+
analyze_file,
|
|
20
27
|
coro,
|
|
21
28
|
echo_safe_humanfriendly_tables_format_smart_table,
|
|
22
29
|
get_format_from_filename_or_url,
|
|
23
30
|
load_connector_config,
|
|
31
|
+
normalize_datasource_name,
|
|
24
32
|
push_data,
|
|
25
33
|
)
|
|
34
|
+
from tinybird.tb.modules.config import CLIConfig
|
|
35
|
+
from tinybird.tb.modules.create import (
|
|
36
|
+
create_resources_from_prompt,
|
|
37
|
+
generate_aws_iamrole_connection_file_with_secret,
|
|
38
|
+
generate_gcs_connection_file_with_secrets,
|
|
39
|
+
generate_kafka_connection_with_secrets,
|
|
40
|
+
)
|
|
26
41
|
from tinybird.tb.modules.datafile.common import get_name_version
|
|
27
42
|
from tinybird.tb.modules.datafile.fixture import persist_fixture
|
|
28
43
|
from tinybird.tb.modules.exceptions import CLIDatasourceException
|
|
@@ -107,33 +122,169 @@ async def datasource_ls(ctx: Context, match: Optional[str], format_: str):
|
|
|
107
122
|
|
|
108
123
|
|
|
109
124
|
@datasource.command(name="append")
|
|
110
|
-
@click.argument("datasource_name", required=
|
|
111
|
-
@click.argument("
|
|
125
|
+
@click.argument("datasource_name", required=False)
|
|
126
|
+
@click.argument("data", nargs=-1, required=False)
|
|
127
|
+
@click.option("--url", type=str, help="URL to append data from")
|
|
128
|
+
@click.option("--file", type=str, help="Local file to append data from")
|
|
129
|
+
@click.option("--events", type=str, help="Events to append data from")
|
|
112
130
|
@click.option("--concurrency", help="How many files to submit concurrently", default=1, hidden=True)
|
|
113
131
|
@click.pass_context
|
|
114
132
|
@coro
|
|
115
133
|
async def datasource_append(
|
|
116
134
|
ctx: Context,
|
|
117
135
|
datasource_name: str,
|
|
118
|
-
|
|
136
|
+
data: Optional[str],
|
|
137
|
+
url: str,
|
|
138
|
+
file: str,
|
|
139
|
+
events: str,
|
|
119
140
|
concurrency: int,
|
|
120
141
|
):
|
|
121
142
|
"""
|
|
122
143
|
Appends data to an existing data source from URL, local file or a connector
|
|
123
144
|
|
|
124
|
-
-
|
|
145
|
+
- Events API: `tb datasource append [datasource_name] --events '{"a":"b, "c":"d"}'`\n
|
|
146
|
+
- Local File: `tb datasource append [datasource_name] --file /path/to/local/file`\n
|
|
147
|
+
- Remote URL: `tb datasource append [datasource_name] --url https://url_to_csv`\n
|
|
148
|
+
- Kafka, S3 and GCS: https://www.tinybird.co/docs/forward/get-data-in/connectors\n
|
|
125
149
|
|
|
126
|
-
|
|
150
|
+
More info: https://www.tinybird.co/docs/forward/get-data-in
|
|
127
151
|
"""
|
|
128
|
-
|
|
152
|
+
env: str = ctx.ensure_object(dict)["env"]
|
|
129
153
|
client: TinyB = ctx.obj["client"]
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
154
|
+
|
|
155
|
+
# If data is passed as argument, we detect if it's a JSON object, a URL or a file
|
|
156
|
+
if data:
|
|
157
|
+
try:
|
|
158
|
+
json.loads(data)
|
|
159
|
+
events = data
|
|
160
|
+
except Exception:
|
|
161
|
+
pass
|
|
162
|
+
if urlparse(data).scheme in ("http", "https"):
|
|
163
|
+
url = data
|
|
164
|
+
if not events and not url:
|
|
165
|
+
file = data
|
|
166
|
+
|
|
167
|
+
# If data is not passed as argument, we use the data from the options
|
|
168
|
+
if not data:
|
|
169
|
+
data = file or url or events
|
|
170
|
+
|
|
171
|
+
if env == "local":
|
|
172
|
+
tip = "Did you build your project? Run `tb build` first."
|
|
173
|
+
else:
|
|
174
|
+
tip = "Did you deploy your project? Run `tb --cloud deploy` first."
|
|
175
|
+
|
|
176
|
+
datasources = await client.datasources()
|
|
177
|
+
if not datasources:
|
|
178
|
+
raise CLIDatasourceException(FeedbackManager.error(message=f"No data sources found. {tip}"))
|
|
179
|
+
|
|
180
|
+
if datasource_name and datasource_name not in [ds["name"] for ds in datasources]:
|
|
181
|
+
raise CLIDatasourceException(FeedbackManager.error(message=f"Datasource {datasource_name} not found. {tip}"))
|
|
182
|
+
|
|
183
|
+
if not datasource_name:
|
|
184
|
+
datasource_index = -1
|
|
185
|
+
|
|
186
|
+
click.echo(FeedbackManager.info(message="\n? Which data source do you want to ingest data into?"))
|
|
187
|
+
while datasource_index == -1:
|
|
188
|
+
for index, datasource in enumerate(datasources):
|
|
189
|
+
click.echo(f" [{index + 1}] {datasource['name']}")
|
|
190
|
+
click.echo(
|
|
191
|
+
FeedbackManager.gray(message="Tip: Run tb datasource append [datasource_name] to skip this step.")
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
datasource_index = click.prompt("\nSelect option", default=1)
|
|
195
|
+
|
|
196
|
+
if datasource_index == 0:
|
|
197
|
+
click.echo(FeedbackManager.warning(message="Datasource type selection cancelled by user"))
|
|
198
|
+
return None
|
|
199
|
+
|
|
200
|
+
try:
|
|
201
|
+
datasource_name = datasources[int(datasource_index) - 1]["name"]
|
|
202
|
+
except Exception:
|
|
203
|
+
datasource_index = -1
|
|
204
|
+
|
|
205
|
+
if not datasource_name:
|
|
206
|
+
raise CLIDatasourceException(FeedbackManager.error_datasource_name())
|
|
207
|
+
|
|
208
|
+
if not data:
|
|
209
|
+
data_index = -1
|
|
210
|
+
options = (
|
|
211
|
+
"Events API",
|
|
212
|
+
"Local File",
|
|
213
|
+
"Remote URL",
|
|
214
|
+
)
|
|
215
|
+
click.echo(FeedbackManager.info(message="\n? How do you want to ingest data?"))
|
|
216
|
+
while data_index == -1:
|
|
217
|
+
for index, option in enumerate(options):
|
|
218
|
+
click.echo(f" [{index + 1}] {option}")
|
|
219
|
+
click.echo(
|
|
220
|
+
FeedbackManager.gray(
|
|
221
|
+
message="Tip: Run tb datasource append [datasource_name] --events | --file | --url to skip this step"
|
|
222
|
+
)
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
data_index = click.prompt("\nSelect option", default=1)
|
|
226
|
+
|
|
227
|
+
if data_index == 0:
|
|
228
|
+
click.echo(FeedbackManager.warning(message="Data selection cancelled by user"))
|
|
229
|
+
return None
|
|
230
|
+
|
|
231
|
+
try:
|
|
232
|
+
data_index = int(data_index)
|
|
233
|
+
except Exception:
|
|
234
|
+
data_index = -1
|
|
235
|
+
|
|
236
|
+
if data_index == 1:
|
|
237
|
+
events = click.prompt("Events data")
|
|
238
|
+
elif data_index == 2:
|
|
239
|
+
data = click.prompt("Path to local file")
|
|
240
|
+
elif data_index == 3:
|
|
241
|
+
data = click.prompt("URL to remote file")
|
|
242
|
+
else:
|
|
243
|
+
raise CLIDatasourceException(FeedbackManager.error(message="Invalid ingestion option"))
|
|
244
|
+
|
|
245
|
+
if events:
|
|
246
|
+
click.echo(FeedbackManager.highlight(message=f"\n» Sending events to {datasource_name}"))
|
|
247
|
+
try:
|
|
248
|
+
json_data = json.loads(events)
|
|
249
|
+
except Exception:
|
|
250
|
+
raise CLIDatasourceException(FeedbackManager.error(message="Invalid events data"))
|
|
251
|
+
|
|
252
|
+
response = await sync_to_async(requests.post)(
|
|
253
|
+
f"{client.host}/v0/events?name={datasource_name}",
|
|
254
|
+
headers={"Authorization": f"Bearer {client.token}"},
|
|
255
|
+
json=json_data,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
try:
|
|
259
|
+
res = response.json()
|
|
260
|
+
except Exception:
|
|
261
|
+
raise CLIDatasourceException(FeedbackManager.error(message=response.text))
|
|
262
|
+
|
|
263
|
+
successful_rows = res["successful_rows"]
|
|
264
|
+
quarantined_rows = res["quarantined_rows"]
|
|
265
|
+
if successful_rows > 0:
|
|
266
|
+
click.echo(
|
|
267
|
+
FeedbackManager.success(
|
|
268
|
+
message=f"✓ {successful_rows} row{'' if successful_rows == 1 else 's'} appended!"
|
|
269
|
+
)
|
|
270
|
+
)
|
|
271
|
+
if quarantined_rows > 0:
|
|
272
|
+
raise CLIDatasourceException(
|
|
273
|
+
FeedbackManager.error(
|
|
274
|
+
message=f"{quarantined_rows} row{'' if quarantined_rows == 1 else 's'} went to quarantine"
|
|
275
|
+
)
|
|
276
|
+
)
|
|
277
|
+
else:
|
|
278
|
+
click.echo(FeedbackManager.highlight(message=f"\n» Appending data to {datasource_name}"))
|
|
279
|
+
await push_data(
|
|
280
|
+
client,
|
|
281
|
+
datasource_name,
|
|
282
|
+
data,
|
|
283
|
+
mode="append",
|
|
284
|
+
concurrency=concurrency,
|
|
285
|
+
silent=True,
|
|
286
|
+
)
|
|
287
|
+
click.echo(FeedbackManager.success(message="✓ Rows appended!"))
|
|
137
288
|
|
|
138
289
|
|
|
139
290
|
@datasource.command(name="replace")
|
|
@@ -473,3 +624,281 @@ async def datasource_sync(ctx: Context, datasource_name: str, yes: bool):
|
|
|
473
624
|
raise
|
|
474
625
|
except Exception as e:
|
|
475
626
|
raise CLIDatasourceException(FeedbackManager.error_syncing_datasource(datasource=datasource_name, error=str(e)))
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
@datasource.command(name="create")
|
|
630
|
+
@click.option("--name", type=str, help="Name of the data source")
|
|
631
|
+
@click.option("--blank", is_flag=True, default=False, help="Create a blank data source")
|
|
632
|
+
@click.option("--file", type=str, help="Create a data source from a local file")
|
|
633
|
+
@click.option("--url", type=str, help="Create a data source from a remote URL")
|
|
634
|
+
@click.option("--connection", type=str, help="Create a data source from a connection")
|
|
635
|
+
@click.option("--prompt", type=str, help="Create a data source from a prompt")
|
|
636
|
+
@click.option("--s3", is_flag=True, default=False, help="Create a data source from a S3 connection")
|
|
637
|
+
@click.option("--gcs", is_flag=True, default=False, help="Create a data source from a GCS connection")
|
|
638
|
+
@click.option("--kafka", is_flag=True, default=False, help="Create a data source from a Kafka connection")
|
|
639
|
+
@click.pass_context
|
|
640
|
+
@coro
|
|
641
|
+
async def datasource_create(
|
|
642
|
+
ctx: Context,
|
|
643
|
+
name: str,
|
|
644
|
+
blank: bool,
|
|
645
|
+
file: str,
|
|
646
|
+
url: str,
|
|
647
|
+
connection: str,
|
|
648
|
+
prompt: str,
|
|
649
|
+
s3: bool,
|
|
650
|
+
gcs: bool,
|
|
651
|
+
kafka: bool,
|
|
652
|
+
):
|
|
653
|
+
project: Project = ctx.ensure_object(dict)["project"]
|
|
654
|
+
client: TinyB = ctx.ensure_object(dict)["client"]
|
|
655
|
+
config = ctx.ensure_object(dict)["config"]
|
|
656
|
+
env: str = ctx.ensure_object(dict)["env"]
|
|
657
|
+
|
|
658
|
+
if env == "cloud":
|
|
659
|
+
raise CLIDatasourceException(
|
|
660
|
+
FeedbackManager.error(message="`tb datasource create` is not available against Tinybird Cloud.")
|
|
661
|
+
)
|
|
662
|
+
|
|
663
|
+
datasource_types = (
|
|
664
|
+
"Blank",
|
|
665
|
+
"Local file",
|
|
666
|
+
"Remote URL",
|
|
667
|
+
"Kafka",
|
|
668
|
+
"S3",
|
|
669
|
+
"GCS",
|
|
670
|
+
)
|
|
671
|
+
datasource_type: Optional[str] = None
|
|
672
|
+
connection_file: Optional[str] = None
|
|
673
|
+
ds_content = ""
|
|
674
|
+
|
|
675
|
+
if file:
|
|
676
|
+
datasource_type = "Local file"
|
|
677
|
+
elif url:
|
|
678
|
+
datasource_type = "Remote URL"
|
|
679
|
+
elif blank:
|
|
680
|
+
datasource_type = "Blank"
|
|
681
|
+
elif connection:
|
|
682
|
+
connection_files = project.get_connection_files()
|
|
683
|
+
connection_file = next((f for f in connection_files if f.endswith(f"{connection}.connection")), None)
|
|
684
|
+
if connection_file:
|
|
685
|
+
connection_content = Path(connection_file).read_text()
|
|
686
|
+
if project.is_kafka_connection(connection_content):
|
|
687
|
+
datasource_type = "Kafka"
|
|
688
|
+
elif project.is_s3_connection(connection_content):
|
|
689
|
+
datasource_type = "S3"
|
|
690
|
+
elif project.is_gcs_connection(connection_content):
|
|
691
|
+
datasource_type = "GCS"
|
|
692
|
+
elif s3:
|
|
693
|
+
datasource_type = "S3"
|
|
694
|
+
elif gcs:
|
|
695
|
+
datasource_type = "GCS"
|
|
696
|
+
elif kafka:
|
|
697
|
+
datasource_type = "Kafka"
|
|
698
|
+
elif prompt:
|
|
699
|
+
click.echo(FeedbackManager.gray(message="\n» Creating .datasource file..."))
|
|
700
|
+
user_token = config.get("user_token")
|
|
701
|
+
if not user_token:
|
|
702
|
+
raise Exception("This action requires authentication. Run 'tb login' first.")
|
|
703
|
+
project_config = CLIConfig.get_project_config()
|
|
704
|
+
tb_client: TinyB = project_config.get_client(token=config.get("token"), host=config.get("host"))
|
|
705
|
+
await create_resources_from_prompt(tb_client, user_token, prompt, project)
|
|
706
|
+
click.echo(FeedbackManager.success(message="✓ .datasource created!"))
|
|
707
|
+
return
|
|
708
|
+
|
|
709
|
+
if datasource_type is None:
|
|
710
|
+
click.echo(FeedbackManager.highlight(message="? Where do you want to create your .datasource from?"))
|
|
711
|
+
datasource_type_index = -1
|
|
712
|
+
|
|
713
|
+
while datasource_type_index == -1:
|
|
714
|
+
for index, datasource_type in enumerate(datasource_types):
|
|
715
|
+
click.echo(f" [{index + 1}] {datasource_type}")
|
|
716
|
+
click.echo(
|
|
717
|
+
FeedbackManager.gray(
|
|
718
|
+
message="Tip: Run `tb datasource create --file | --url | --connection` to skip this step."
|
|
719
|
+
)
|
|
720
|
+
)
|
|
721
|
+
datasource_type_index = click.prompt("\nSelect option", default=1)
|
|
722
|
+
|
|
723
|
+
if datasource_type_index == 0:
|
|
724
|
+
click.echo(FeedbackManager.warning(message="Datasource type selection cancelled by user"))
|
|
725
|
+
return None
|
|
726
|
+
|
|
727
|
+
try:
|
|
728
|
+
datasource_type = datasource_types[int(datasource_type_index) - 1]
|
|
729
|
+
except Exception:
|
|
730
|
+
datasource_type_index = -1
|
|
731
|
+
|
|
732
|
+
if not datasource_type:
|
|
733
|
+
click.echo(
|
|
734
|
+
FeedbackManager.error(
|
|
735
|
+
message=f"Invalid option: {datasource_type_index}. Please select a valid option from the list above."
|
|
736
|
+
)
|
|
737
|
+
)
|
|
738
|
+
return
|
|
739
|
+
|
|
740
|
+
connection_required = datasource_type in ("Kafka", "S3", "GCS")
|
|
741
|
+
|
|
742
|
+
if connection_required:
|
|
743
|
+
click.echo(FeedbackManager.gray(message="\n» Creating .datasource file..."))
|
|
744
|
+
connection_type = datasource_type.lower()
|
|
745
|
+
|
|
746
|
+
def get_connection_files():
|
|
747
|
+
connection_files = []
|
|
748
|
+
if connection_type == "kafka":
|
|
749
|
+
connection_files = project.get_kafka_connection_files()
|
|
750
|
+
elif connection_type == "s3":
|
|
751
|
+
connection_files = project.get_s3_connection_files()
|
|
752
|
+
elif connection_type == "gcs":
|
|
753
|
+
connection_files = project.get_gcs_connection_files()
|
|
754
|
+
return connection_files
|
|
755
|
+
|
|
756
|
+
connection_files = get_connection_files()
|
|
757
|
+
if len(connection_files) == 0:
|
|
758
|
+
click.echo(FeedbackManager.error(message=f"x No {datasource_type} connections found."))
|
|
759
|
+
if click.confirm(
|
|
760
|
+
FeedbackManager.highlight(message=f"\n? Do you want to create a {datasource_type} connection? [Y/n]"),
|
|
761
|
+
show_default=False,
|
|
762
|
+
default=True,
|
|
763
|
+
):
|
|
764
|
+
click.echo(FeedbackManager.gray(message="\n» Creating .connection file..."))
|
|
765
|
+
default_connection_name = f"{datasource_type.lower()}_{generate_short_id()}"
|
|
766
|
+
connection_name = click.prompt(
|
|
767
|
+
FeedbackManager.highlight(message=f"? Connection name [{default_connection_name}]"),
|
|
768
|
+
show_default=False,
|
|
769
|
+
default=default_connection_name,
|
|
770
|
+
)
|
|
771
|
+
if datasource_type == "Kafka":
|
|
772
|
+
await generate_kafka_connection_with_secrets(connection_name, folder=project.folder)
|
|
773
|
+
elif datasource_type == "S3":
|
|
774
|
+
await generate_aws_iamrole_connection_file_with_secret(
|
|
775
|
+
connection_name,
|
|
776
|
+
service="s3",
|
|
777
|
+
role_arn_secret_name="S3_ARN",
|
|
778
|
+
region="eu-west-1",
|
|
779
|
+
folder=project.folder,
|
|
780
|
+
with_default_secret=True,
|
|
781
|
+
)
|
|
782
|
+
elif datasource_type == "GCS":
|
|
783
|
+
await generate_gcs_connection_file_with_secrets(
|
|
784
|
+
connection_name,
|
|
785
|
+
service="gcs",
|
|
786
|
+
svc_account_creds="GCS_SERVICE_ACCOUNT_CREDENTIALS_JSON",
|
|
787
|
+
folder=project.folder,
|
|
788
|
+
)
|
|
789
|
+
click.echo(FeedbackManager.info(message=f"/connections/{connection_name}.connection"))
|
|
790
|
+
click.echo(FeedbackManager.success(message="✓ .connection created!"))
|
|
791
|
+
connection_files = get_connection_files()
|
|
792
|
+
else:
|
|
793
|
+
click.echo(
|
|
794
|
+
FeedbackManager.info(message=f"→ Run `tb connection create {datasource_type.lower()}` to add one.")
|
|
795
|
+
)
|
|
796
|
+
return
|
|
797
|
+
|
|
798
|
+
if not connection_file:
|
|
799
|
+
connection_file = connection_files[0]
|
|
800
|
+
connection_path = Path(connection_file)
|
|
801
|
+
connection = connection_path.stem
|
|
802
|
+
|
|
803
|
+
ds_content = """SCHEMA >
|
|
804
|
+
`timestamp` DateTime `json:$.timestamp`,
|
|
805
|
+
`session_id` String `json:$.session_id`
|
|
806
|
+
"""
|
|
807
|
+
|
|
808
|
+
if datasource_type == "Local file":
|
|
809
|
+
click.echo(FeedbackManager.gray(message="\n» Creating .datasource file..."))
|
|
810
|
+
if not file:
|
|
811
|
+
file = click.prompt(FeedbackManager.highlight(message="? Path"))
|
|
812
|
+
if file.startswith("~"):
|
|
813
|
+
file = os.path.expanduser(file)
|
|
814
|
+
|
|
815
|
+
folder_path = project.path
|
|
816
|
+
path = folder_path / file
|
|
817
|
+
if not path.exists():
|
|
818
|
+
path = Path(file)
|
|
819
|
+
|
|
820
|
+
data_format = path.suffix.lstrip(".")
|
|
821
|
+
ds_content = await analyze_file(str(path), client, format=data_format)
|
|
822
|
+
default_name = normalize_datasource_name(path.stem)
|
|
823
|
+
name = name or click.prompt(
|
|
824
|
+
FeedbackManager.highlight(message=f"? Data source name [{default_name}]"),
|
|
825
|
+
default=default_name,
|
|
826
|
+
show_default=False,
|
|
827
|
+
)
|
|
828
|
+
|
|
829
|
+
if datasource_type == "Remote URL":
|
|
830
|
+
click.echo(FeedbackManager.gray(message="\n» Creating .datasource file..."))
|
|
831
|
+
if not url:
|
|
832
|
+
url = click.prompt(FeedbackManager.highlight(message="? URL"))
|
|
833
|
+
format = url.split(".")[-1]
|
|
834
|
+
ds_content = await analyze_file(url, client, format)
|
|
835
|
+
default_name = normalize_datasource_name(Path(url).stem)
|
|
836
|
+
name = name or click.prompt(
|
|
837
|
+
FeedbackManager.highlight(message=f"? Data source name [{default_name}]"),
|
|
838
|
+
default=default_name,
|
|
839
|
+
show_default=False,
|
|
840
|
+
)
|
|
841
|
+
|
|
842
|
+
if datasource_type == "Blank":
|
|
843
|
+
click.echo(FeedbackManager.gray(message="\n» Creating .datasource file..."))
|
|
844
|
+
|
|
845
|
+
if datasource_type not in ("Remote URL", "Local file"):
|
|
846
|
+
default_name = f"ds_{generate_short_id()}"
|
|
847
|
+
name = name or click.prompt(
|
|
848
|
+
FeedbackManager.highlight(message=f"? Data source name [{default_name}]"),
|
|
849
|
+
default=default_name,
|
|
850
|
+
show_default=False,
|
|
851
|
+
)
|
|
852
|
+
|
|
853
|
+
if datasource_type == "Kafka":
|
|
854
|
+
connections = await client.connections("kafka")
|
|
855
|
+
connection_id = next((c["id"] for c in connections if c["name"] == connection), connection)
|
|
856
|
+
try:
|
|
857
|
+
topics = await client.kafka_list_topics(connection_id) if connection_id else []
|
|
858
|
+
except Exception:
|
|
859
|
+
topics = []
|
|
860
|
+
topic = topics[0] if len(topics) > 0 else "topic_0"
|
|
861
|
+
group_id = generate_kafka_group_id(topic)
|
|
862
|
+
ds_content += f"""
|
|
863
|
+
KAFKA_CONNECTION_NAME {connection}
|
|
864
|
+
KAFKA_TOPIC {{{{ tb_secret("KAFKA_TOPIC", "{topic}") }}}}
|
|
865
|
+
KAFKA_GROUP_ID {{{{ tb_secret("KAFKA_GROUP_ID", "{group_id}") }}}}
|
|
866
|
+
"""
|
|
867
|
+
|
|
868
|
+
if datasource_type == "S3":
|
|
869
|
+
if not connection:
|
|
870
|
+
connections = await client.connections("s3")
|
|
871
|
+
connection = next((c["name"] for c in connections if c["name"] == connection), connection)
|
|
872
|
+
ds_content += f"""
|
|
873
|
+
IMPORT_CONNECTION_NAME "{connection}"
|
|
874
|
+
IMPORT_BUCKET_URI "s3://my-bucket/*.csv"
|
|
875
|
+
IMPORT_SCHEDULE "@auto"
|
|
876
|
+
"""
|
|
877
|
+
|
|
878
|
+
if datasource_type == "GCS":
|
|
879
|
+
if not connection:
|
|
880
|
+
connections = await client.connections("gcs")
|
|
881
|
+
connection = next((c["name"] for c in connections if c["name"] == connection), connection)
|
|
882
|
+
ds_content += f"""
|
|
883
|
+
IMPORT_CONNECTION_NAME "{connection}"
|
|
884
|
+
IMPORT_BUCKET_URI "gs://my-bucket/*.csv"
|
|
885
|
+
IMPORT_SCHEDULE "@auto"
|
|
886
|
+
"""
|
|
887
|
+
|
|
888
|
+
click.echo(FeedbackManager.info(message=f"/datasources/{name}.datasource"))
|
|
889
|
+
datasources_path = project.path / "datasources"
|
|
890
|
+
if not datasources_path.exists():
|
|
891
|
+
datasources_path.mkdir()
|
|
892
|
+
ds_file = datasources_path / f"{name}.datasource"
|
|
893
|
+
if not ds_file.exists():
|
|
894
|
+
ds_file.touch()
|
|
895
|
+
ds_file.write_text(ds_content)
|
|
896
|
+
click.echo(FeedbackManager.success(message="✓ .datasource created!"))
|
|
897
|
+
|
|
898
|
+
|
|
899
|
+
def generate_short_id():
|
|
900
|
+
return str(uuid.uuid4())[:4]
|
|
901
|
+
|
|
902
|
+
|
|
903
|
+
def generate_kafka_group_id(topic: str):
|
|
904
|
+
return f"{topic}_{int(datetime.timestamp(datetime.now()))}"
|
|
@@ -521,6 +521,14 @@ def create_deployment(
|
|
|
521
521
|
TINYBIRD_API_URL = f"{client.host}/v1/deploy"
|
|
522
522
|
TINYBIRD_API_KEY = client.token
|
|
523
523
|
|
|
524
|
+
if project.has_deeper_level():
|
|
525
|
+
click.echo(
|
|
526
|
+
FeedbackManager.warning(
|
|
527
|
+
message="\nDefault max_depth is 2, but project has deeper level. "
|
|
528
|
+
"You can change max_depth by running `tb --max-depth <depth> <cmd>`"
|
|
529
|
+
)
|
|
530
|
+
)
|
|
531
|
+
|
|
524
532
|
files = [
|
|
525
533
|
("context://", ("cli-version", "1.0.0", "text/plain")),
|
|
526
534
|
]
|
tinybird/tb/modules/project.py
CHANGED
|
@@ -30,6 +30,17 @@ class Project:
|
|
|
30
30
|
project_files.extend(glob.glob(f"{self.path}{'/*' * level}/*.{extension}", recursive=True))
|
|
31
31
|
return project_files
|
|
32
32
|
|
|
33
|
+
def has_deeper_level(self) -> bool:
|
|
34
|
+
"""Check if there are folders with depth greater than max_depth in project path.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
bool: True if there are folders deeper than max_depth, False otherwise
|
|
38
|
+
"""
|
|
39
|
+
for obj in glob.glob(f"{self.path}{'/*' * (self.max_depth - 1)}/*", recursive=False):
|
|
40
|
+
if Path(obj).is_dir():
|
|
41
|
+
return True
|
|
42
|
+
return False
|
|
43
|
+
|
|
33
44
|
def get_project_files(self) -> List[str]:
|
|
34
45
|
project_files: List[str] = []
|
|
35
46
|
for extension in self.extensions:
|
|
@@ -66,6 +77,15 @@ class Project:
|
|
|
66
77
|
def get_connection_files(self) -> List[str]:
|
|
67
78
|
return self.get_files("connection")
|
|
68
79
|
|
|
80
|
+
def get_kafka_connection_files(self) -> List[str]:
|
|
81
|
+
return [f for f in self.get_connection_files() if self.is_kafka_connection(Path(f).read_text())]
|
|
82
|
+
|
|
83
|
+
def get_s3_connection_files(self) -> List[str]:
|
|
84
|
+
return [f for f in self.get_connection_files() if self.is_s3_connection(Path(f).read_text())]
|
|
85
|
+
|
|
86
|
+
def get_gcs_connection_files(self) -> List[str]:
|
|
87
|
+
return [f for f in self.get_connection_files() if self.is_gcs_connection(Path(f).read_text())]
|
|
88
|
+
|
|
69
89
|
def get_pipe_datafile(self, filename: str) -> Optional[Datafile]:
|
|
70
90
|
try:
|
|
71
91
|
return parse_pipe(filename).datafile
|
|
@@ -96,3 +116,27 @@ class Project:
|
|
|
96
116
|
@staticmethod
|
|
97
117
|
def is_endpoint(content: str) -> bool:
|
|
98
118
|
return re.search(r"TYPE endpoint", content, re.IGNORECASE) is not None
|
|
119
|
+
|
|
120
|
+
@staticmethod
|
|
121
|
+
def is_kafka_connection(content: str) -> bool:
|
|
122
|
+
return re.search(r"TYPE kafka", content, re.IGNORECASE) is not None
|
|
123
|
+
|
|
124
|
+
@staticmethod
|
|
125
|
+
def is_s3_connection(content: str) -> bool:
|
|
126
|
+
return re.search(r"TYPE s3", content, re.IGNORECASE) is not None
|
|
127
|
+
|
|
128
|
+
@staticmethod
|
|
129
|
+
def is_gcs_connection(content: str) -> bool:
|
|
130
|
+
return re.search(r"TYPE gcs", content, re.IGNORECASE) is not None
|
|
131
|
+
|
|
132
|
+
@staticmethod
|
|
133
|
+
def is_kafka_datasource(content: str) -> bool:
|
|
134
|
+
return re.search(r"KAFKA_CONNECTION_NAME", content, re.IGNORECASE) is not None
|
|
135
|
+
|
|
136
|
+
@staticmethod
|
|
137
|
+
def is_s3_datasource(content: str) -> bool:
|
|
138
|
+
return re.search(r"IMPORT_CONNECTION_NAME", content, re.IGNORECASE) is not None
|
|
139
|
+
|
|
140
|
+
@staticmethod
|
|
141
|
+
def is_gcs_datasource(content: str) -> bool:
|
|
142
|
+
return re.search(r"IMPORT_CONNECTION_NAME", content, re.IGNORECASE) is not None
|
|
@@ -12,21 +12,21 @@ tinybird/syncasync.py,sha256=IPnOx6lMbf9SNddN1eBtssg8vCLHMt76SuZ6YNYm-Yk,27761
|
|
|
12
12
|
tinybird/tornado_template.py,sha256=jjNVDMnkYFWXflmT8KU_Ssbo5vR8KQq3EJMk5vYgXRw,41959
|
|
13
13
|
tinybird/ch_utils/constants.py,sha256=aYvg2C_WxYWsnqPdZB1ZFoIr8ZY-XjUXYyHKE9Ansj0,3890
|
|
14
14
|
tinybird/ch_utils/engine.py,sha256=X4tE9OrfaUy6kO9cqVEzyI9cDcmOF3IAssRRzsTsfEQ,40781
|
|
15
|
-
tinybird/tb/__cli__.py,sha256=
|
|
15
|
+
tinybird/tb/__cli__.py,sha256=Uk4h64toVqAfnRJ4rIo4K3vfl_FunHW228-sIHLBDt0,247
|
|
16
16
|
tinybird/tb/check_pypi.py,sha256=rW4QmDRbtgKdUUwJCnBkVjmTjZSZGN-XgZhx7vMkC0w,1009
|
|
17
17
|
tinybird/tb/cli.py,sha256=u3eGOhX0MHkuT6tiwaZ0_3twqLmqKXDAOxF7yV_Nn9Q,1075
|
|
18
|
-
tinybird/tb/client.py,sha256=
|
|
18
|
+
tinybird/tb/client.py,sha256=CO-dQw8h28X6T6IO-Z79yPBKaJQT1Rwya5b6gexvw58,56491
|
|
19
19
|
tinybird/tb/config.py,sha256=jT9xndpeCY_g0HdB5qE2EquC0TFRRnkPnQFWZWd04jo,3998
|
|
20
|
-
tinybird/tb/modules/build.py,sha256=
|
|
20
|
+
tinybird/tb/modules/build.py,sha256=KvF0s8hGgY_rZs7jSqYiauCk3MAlCmW_gQtnsJDJWBk,19411
|
|
21
21
|
tinybird/tb/modules/cicd.py,sha256=Njb6eZOHHbUkoJJx6KoixO9PsfA_T-3Ybkya9-50Ca8,7328
|
|
22
22
|
tinybird/tb/modules/cli.py,sha256=dXZs-MuqYPvxStVj7aLg36LwXtEB8NzTobDmHV9nzZI,15508
|
|
23
|
-
tinybird/tb/modules/common.py,sha256=
|
|
23
|
+
tinybird/tb/modules/common.py,sha256=6SYrDH-GhWm8N8S2GmHdIbvf1mUYIzWYlz0WcjDIhGw,83947
|
|
24
24
|
tinybird/tb/modules/config.py,sha256=ziqW_t_mRVvWOd85VoB4vKyvgMkEfpXDf9H4v38p2xc,11422
|
|
25
|
-
tinybird/tb/modules/connection.py,sha256=
|
|
25
|
+
tinybird/tb/modules/connection.py,sha256=z1xWP2gtjKEbjc4ZF1aD7QUgl8V--wf2IRNy-4sRFm8,9779
|
|
26
26
|
tinybird/tb/modules/copy.py,sha256=2Mm4FWKehOG7CoOhiF1m9UZJgJn0W1_cMolqju8ONYg,5805
|
|
27
|
-
tinybird/tb/modules/create.py,sha256=
|
|
28
|
-
tinybird/tb/modules/datasource.py,sha256=
|
|
29
|
-
tinybird/tb/modules/deployment.py,sha256=
|
|
27
|
+
tinybird/tb/modules/create.py,sha256=2uW-4t7c7e4xkZ-GpK_8XaA-nuXwklq7rTks4k6qrtI,20917
|
|
28
|
+
tinybird/tb/modules/datasource.py,sha256=gycHinGTDxZMDqwbIaRJkR2SUxdhbr1prCFQHkssgVs,35124
|
|
29
|
+
tinybird/tb/modules/deployment.py,sha256=OGJdriqywhCtsG7rKLFtVSLjoEbbva1Nb29-jQBk3wM,27432
|
|
30
30
|
tinybird/tb/modules/deprecations.py,sha256=rrszC1f_JJeJ8mUxGoCxckQTJFBCR8wREf4XXXN-PRc,4507
|
|
31
31
|
tinybird/tb/modules/dev_server.py,sha256=57FCKuWpErwYUYgHspYDkLWEm9F4pbvVOtMrFXX1fVU,10129
|
|
32
32
|
tinybird/tb/modules/endpoint.py,sha256=XySDt3pk66vxOZ0egUfz4bY8bEk3BjOXkv-L0OIJ3sc,12083
|
|
@@ -45,7 +45,7 @@ tinybird/tb/modules/materialization.py,sha256=QJX5kCPhhm6IXBO1JsalVfbQdypCe_eOUD
|
|
|
45
45
|
tinybird/tb/modules/mock.py,sha256=IyHweMUM6bUH8IhyiX2tTMpdVpTFUeAJ41lZ5P42-HQ,5303
|
|
46
46
|
tinybird/tb/modules/open.py,sha256=OuctINN77oexpSjth9uoIZPCelKO4Li-yyVxeSnk1io,1371
|
|
47
47
|
tinybird/tb/modules/pipe.py,sha256=AQKEDagO6e3psPVjJkS_MDbn8aK-apAiLp26k7jgAV0,2432
|
|
48
|
-
tinybird/tb/modules/project.py,sha256=
|
|
48
|
+
tinybird/tb/modules/project.py,sha256=iIEaBQsdLXyzJ_45Paf1jwbSrtTwWv131VCuPsTQttA,5215
|
|
49
49
|
tinybird/tb/modules/regions.py,sha256=QjsL5H6Kg-qr0aYVLrvb1STeJ5Sx_sjvbOYO0LrEGMk,166
|
|
50
50
|
tinybird/tb/modules/secret.py,sha256=WsqzxxLh9W_jkuHL2JofMXdIJy0lT5WEI-7bQSIDgAc,2921
|
|
51
51
|
tinybird/tb/modules/shell.py,sha256=Zd_4Ak_5tKVX-cw6B4ag36xZeEGHeh-jZpAsIXkoMoE,14116
|
|
@@ -80,8 +80,8 @@ tinybird/tb_cli_modules/config.py,sha256=IsgdtFRnUrkY8-Zo32lmk6O7u3bHie1QCxLwgp4
|
|
|
80
80
|
tinybird/tb_cli_modules/exceptions.py,sha256=pmucP4kTF4irIt7dXiG-FcnI-o3mvDusPmch1L8RCWk,3367
|
|
81
81
|
tinybird/tb_cli_modules/regions.py,sha256=QjsL5H6Kg-qr0aYVLrvb1STeJ5Sx_sjvbOYO0LrEGMk,166
|
|
82
82
|
tinybird/tb_cli_modules/telemetry.py,sha256=Hh2Io8ZPROSunbOLuMvuIFU4TqwWPmQTqal4WS09K1A,10449
|
|
83
|
-
tinybird-0.0.1.
|
|
84
|
-
tinybird-0.0.1.
|
|
85
|
-
tinybird-0.0.1.
|
|
86
|
-
tinybird-0.0.1.
|
|
87
|
-
tinybird-0.0.1.
|
|
83
|
+
tinybird-0.0.1.dev190.dist-info/METADATA,sha256=MDRLrgiaBGisoaGthlSBKhjMSvdw8XSWPF5VEUsjrvM,1608
|
|
84
|
+
tinybird-0.0.1.dev190.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
|
85
|
+
tinybird-0.0.1.dev190.dist-info/entry_points.txt,sha256=LwdHU6TfKx4Qs7BqqtaczEZbImgU7Abe9Lp920zb_fo,43
|
|
86
|
+
tinybird-0.0.1.dev190.dist-info/top_level.txt,sha256=VqqqEmkAy7UNaD8-V51FCoMMWXjLUlR0IstvK7tJYVY,54
|
|
87
|
+
tinybird-0.0.1.dev190.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|