66from pathlib import Path
77from typing import Any
88
9- from ..config import get_client_path , get_config_path , get_datasources_path , get_pipes_path
10- from ..utils .package_manager import detect_package_manager_run_cmd
11- from .login import run_login
9+ from ..config import find_existing_config_path
1210
1311
14- DATASOURCES_TEMPLATE = '''from tinybird_sdk import define_datasource, t , engine
12+ RESOURCES_TEMPLATE = '''from tinybird_sdk import define_datasource, define_endpoint , engine, node, p, t
1513
1614
15+ # --- Datasources ---
16+
1717page_views = define_datasource("page_views", {
1818 "description": "Page view tracking data",
1919 "schema": {
2626 "sorting_key": ["pathname", "timestamp"],
2727 }),
2828})
29- '''
3029
31- PIPES_TEMPLATE = '''from tinybird_sdk import define_endpoint, node, t, p
3230
31+ # --- Endpoints ---
3332
3433top_pages = define_endpoint("top_pages", {
3534 "description": "Get the most visited pages",
5958})
6059'''
6160
62- CLIENT_TEMPLATE = '''from tinybird_sdk import Tinybird
63- from .datasources import page_views
64- from .pipes import top_pages
6561
62+ def _client_template (resources_import_path : str ) -> str :
63+ return f'''import os
6664
67- tinybird = Tinybird({
68- "datasources": {"page_views": page_views},
69- "pipes": {"top_pages": top_pages},
70- })
65+ from tinybird_sdk import Tinybird
66+ from { resources_import_path } import page_views, top_pages
67+
68+ tinybird = Tinybird(
69+ {{
70+ "datasources": {{"page_views": page_views}},
71+ "pipes": {{"top_pages": top_pages}},
72+ "base_url": os.getenv("TINYBIRD_API_URL", "https://api.tinybird.co"),
73+ "token": os.getenv("TINYBIRD_TOKEN"),
74+ }}
75+ )
76+ '''
77+
78+
79+ def _main_template (client_import_path : str ) -> str :
80+ return f'''from datetime import datetime, timezone
81+
82+ from dotenv import load_dotenv
83+
84+
85+ def main():
86+ load_dotenv(".env.local")
87+
88+ from { client_import_path } import tinybird
89+
90+ now = datetime.now(timezone.utc).isoformat(timespec="milliseconds")
91+
92+ # Ingest data using the Events API
93+ tinybird.page_views.ingest(
94+ {{
95+ "timestamp": now,
96+ "session_id": "abc123",
97+ "pathname": "/home",
98+ "referrer": "https://google.com",
99+ }}
100+ )
101+
102+ # Query the endpoint
103+ result = tinybird.top_pages.query(
104+ {{
105+ "start_date": "2026-01-01 00:00:00",
106+ "end_date": now,
107+ "limit": 5,
108+ }}
109+ )
110+
111+ for row in result["data"]:
112+ print(row["pathname"], row["views"])
113+
114+
115+ if __name__ == "__main__":
116+ main()
71117'''
72118
73119
74120@dataclass (frozen = True , slots = True )
75121class InitOptions :
76122 cwd : str | None = None
123+ folder : str | None = None
77124 force : bool = False
78- skip_login : bool = False
79- dev_mode : str | None = None
80- client_path : str | None = None
81125
82126
83127@dataclass (frozen = True , slots = True )
84128class InitResult :
85129 success : bool
130+ resources_path : str | None = None
86131 client_path : str | None = None
87- logged_in : bool | None = None
88- workspace_name : str | None = None
89- user_email : str | None = None
90- existing_datafiles : list [str ] | None = None
132+ main_path : str | None = None
91133 error : str | None = None
92134
93135
@@ -106,46 +148,87 @@ def _write_file(path: Path, content: str, force: bool) -> None:
106148 path .write_text (content , encoding = "utf-8" )
107149
108150
151+ def _run_tinybird_cli_init (argv : list [str ]) -> int :
152+ try :
153+ from tinybird .tb .cli import cli as upstream_cli # type: ignore[import-not-found]
154+ except ModuleNotFoundError :
155+ return 1
156+
157+ try :
158+ upstream_cli .main (args = argv , prog_name = "tinybird" )
159+ return 0
160+ except SystemExit as error :
161+ code = error .code
162+ if code is None :
163+ return 0
164+ return code if isinstance (code , int ) else 1
165+
166+
109167def run_init (options : InitOptions | dict [str , Any ] | None = None ) -> InitResult :
110168 normalized = options if isinstance (options , InitOptions ) else InitOptions (** (options or {}))
111169 cwd = Path (normalized .cwd or os .getcwd ()).resolve ()
112170
113171 try :
114- config_path = Path (get_config_path (str (cwd )))
115- datasources_path = Path (get_datasources_path (str (cwd )))
116- pipes_path = Path (get_pipes_path (str (cwd )))
117- client_path = Path (normalized .client_path ) if normalized .client_path else Path (get_client_path (str (cwd )))
118- if not client_path .is_absolute ():
119- client_path = cwd / client_path
120-
121- existing_datafiles = find_existing_datafiles (str (cwd ))
122-
123- include = [str (datasources_path .relative_to (cwd )), str (pipes_path .relative_to (cwd ))]
124- include .extend (existing_datafiles )
125-
126- config_payload = {
127- "include" : include ,
128- "token" : "${TINYBIRD_TOKEN}" ,
129- "base_url" : "${TINYBIRD_URL}" ,
130- "dev_mode" : normalized .dev_mode or "branch" ,
131- }
132-
133- _write_file (config_path , json .dumps (config_payload , indent = 2 ) + "\n " , normalized .force )
134- _write_file (datasources_path , DATASOURCES_TEMPLATE , normalized .force )
135- _write_file (pipes_path , PIPES_TEMPLATE , normalized .force )
136- _write_file (client_path , CLIENT_TEMPLATE , normalized .force )
137-
138- login_result = None
139- if not normalized .skip_login :
140- login_result = run_login ({"cwd" : str (cwd )})
172+ # 1. Delegate to tinybird CLI for interactive flow (dev mode, CI/CD, skills, login)
173+ cli_argv = ["init" , "--type" , "python" ]
174+ if normalized .folder :
175+ cli_argv .extend (["--folder" , normalized .folder ])
176+ _run_tinybird_cli_init (cli_argv )
177+
178+ # 2. Determine the target folder for SDK template files
179+ # Priority: --folder flag > folder from tinybird.config.json > default
180+ folder : Path | None = None
181+ if normalized .folder :
182+ folder = Path (normalized .folder )
183+ if not folder .is_absolute ():
184+ folder = cwd / folder
185+ else :
186+ # Read the folder the tinybird CLI configured (from the interactive prompt)
187+ config_path_check = find_existing_config_path (str (cwd ))
188+ if config_path_check and config_path_check .endswith (".json" ):
189+ with open (config_path_check , "r" , encoding = "utf-8" ) as fp :
190+ cli_config = json .load (fp )
191+ include = cli_config .get ("include" , [])
192+ if include :
193+ folder = cwd / Path (include [0 ])
194+
195+ if folder is None :
196+ src = cwd / "src"
197+ folder = (src / "lib" ) if src .is_dir () else (cwd / "lib" )
198+
199+ resources_path = folder / "tinybird_resources.py"
200+ client_path = folder / "client.py"
201+ main_path = cwd / "main.py"
202+
203+ # Compute import paths based on folder relative to cwd
204+ relative_folder = str (folder .relative_to (cwd )).replace (os .sep , "." )
205+ resources_import = f"{ relative_folder } .tinybird_resources"
206+ client_import = f"{ relative_folder } .client"
207+
208+ _write_file (resources_path , RESOURCES_TEMPLATE , normalized .force )
209+ _write_file (client_path , _client_template (resources_import ), normalized .force )
210+ # Always overwrite main.py — the default from `uv init` is a placeholder
211+ _write_file (main_path , _main_template (client_import ), force = True )
212+
213+ # 3. Add the resources file to tinybird.config.json include list
214+ config_path = find_existing_config_path (str (cwd ))
215+ if config_path and config_path .endswith (".json" ):
216+ relative_resources = str (resources_path .relative_to (cwd ))
217+ with open (config_path , "r" , encoding = "utf-8" ) as fp :
218+ config_data = json .load (fp )
219+ include = config_data .get ("include" , [])
220+ if relative_resources not in include :
221+ include .append (relative_resources )
222+ config_data ["include" ] = include
223+ with open (config_path , "w" , encoding = "utf-8" ) as fp :
224+ json .dump (config_data , fp , indent = 2 )
225+ fp .write ("\n " )
141226
142227 return InitResult (
143228 success = True ,
229+ resources_path = str (resources_path .relative_to (cwd )),
144230 client_path = str (client_path .relative_to (cwd )),
145- logged_in = login_result .success if login_result else False ,
146- workspace_name = login_result .workspace_name if login_result else None ,
147- user_email = login_result .user_email if login_result else None ,
148- existing_datafiles = existing_datafiles ,
231+ main_path = str (main_path .relative_to (cwd )),
149232 )
150233 except Exception as error :
151234 return InitResult (success = False , error = str (error ))
0 commit comments