tinybird 0.0.1.dev246__py3-none-any.whl → 0.0.1.dev248__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.

Files changed (30) hide show
  1. tinybird/ch_utils/constants.py +2 -0
  2. tinybird/prompts.py +2 -0
  3. tinybird/tb/__cli__.py +2 -2
  4. tinybird/tb/modules/agent/agent.py +107 -18
  5. tinybird/tb/modules/agent/models.py +6 -0
  6. tinybird/tb/modules/agent/prompts.py +57 -29
  7. tinybird/tb/modules/agent/tools/append.py +55 -0
  8. tinybird/tb/modules/agent/tools/build.py +1 -0
  9. tinybird/tb/modules/agent/tools/create_datafile.py +8 -3
  10. tinybird/tb/modules/agent/tools/deploy.py +1 -1
  11. tinybird/tb/modules/agent/tools/mock.py +59 -0
  12. tinybird/tb/modules/agent/tools/plan.py +1 -1
  13. tinybird/tb/modules/agent/tools/read_fixture_data.py +28 -0
  14. tinybird/tb/modules/agent/utils.py +296 -3
  15. tinybird/tb/modules/build.py +4 -1
  16. tinybird/tb/modules/build_common.py +2 -3
  17. tinybird/tb/modules/cli.py +9 -1
  18. tinybird/tb/modules/create.py +1 -1
  19. tinybird/tb/modules/feedback_manager.py +1 -0
  20. tinybird/tb/modules/llm.py +1 -1
  21. tinybird/tb/modules/login.py +6 -301
  22. tinybird/tb/modules/login_common.py +310 -0
  23. tinybird/tb/modules/mock.py +3 -69
  24. tinybird/tb/modules/mock_common.py +71 -0
  25. tinybird/tb/modules/project.py +9 -0
  26. {tinybird-0.0.1.dev246.dist-info → tinybird-0.0.1.dev248.dist-info}/METADATA +1 -1
  27. {tinybird-0.0.1.dev246.dist-info → tinybird-0.0.1.dev248.dist-info}/RECORD +30 -25
  28. {tinybird-0.0.1.dev246.dist-info → tinybird-0.0.1.dev248.dist-info}/WHEEL +0 -0
  29. {tinybird-0.0.1.dev246.dist-info → tinybird-0.0.1.dev248.dist-info}/entry_points.txt +0 -0
  30. {tinybird-0.0.1.dev246.dist-info → tinybird-0.0.1.dev248.dist-info}/top_level.txt +0 -0
@@ -1,112 +1,12 @@
1
- import http.server
2
- import os
3
- import platform
4
- import random
5
- import shutil
6
- import socketserver
7
- import string
8
- import subprocess
9
- import sys
10
- import threading
11
- import time
12
- import urllib.parse
13
- import webbrowser
14
- from typing import Any, Dict, Optional
15
- from urllib.parse import urlencode
1
+ from typing import Optional
16
2
 
17
3
  import click
18
- import requests
19
4
 
20
- from tinybird.tb.config import DEFAULT_API_HOST
21
- from tinybird.tb.modules.cli import CLIConfig, cli
22
- from tinybird.tb.modules.common import ask_for_region_interactively, get_regions
23
- from tinybird.tb.modules.exceptions import CLILoginException
24
- from tinybird.tb.modules.feedback_manager import FeedbackManager
5
+ from tinybird.tb.modules.cli import cli
6
+ from tinybird.tb.modules.login_common import login
25
7
 
26
- SERVER_MAX_WAIT_TIME = 180
27
8
 
28
-
29
- class AuthHandler(http.server.SimpleHTTPRequestHandler):
30
- def do_GET(self):
31
- # The access_token is in the URL fragment, which is not sent to the server
32
- # We'll send a small HTML page that extracts the token and sends it back to the server
33
- self.send_response(200)
34
- self.send_header("Content-type", "text/html")
35
- self.end_headers()
36
- self.wfile.write(
37
- """
38
- <html>
39
- <head>
40
- <style>
41
- body {{
42
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
43
- background: #f5f5f5;
44
- display: flex;
45
- align-items: center;
46
- justify-content: center;
47
- height: 100vh;
48
- margin: 0;
49
- }}
50
- </style>
51
- </head>
52
- <body>
53
- <script>
54
- const searchParams = new URLSearchParams(window.location.search);
55
- const code = searchParams.get('code');
56
- const workspace = searchParams.get('workspace');
57
- const region = searchParams.get('region');
58
- const provider = searchParams.get('provider');
59
- const host = "{auth_host}";
60
- fetch('/?code=' + code, {{method: 'POST'}})
61
- .then(() => {{
62
- window.location.href = host + "/" + provider + "/" + region + "/cli-login?workspace=" + workspace;
63
- }});
64
- </script>
65
- </body>
66
- </html>
67
- """.format(auth_host=self.server.auth_host).encode() # type: ignore
68
- )
69
-
70
- def do_POST(self):
71
- parsed_path = urllib.parse.urlparse(self.path)
72
- query_params = urllib.parse.parse_qs(parsed_path.query)
73
-
74
- if "code" in query_params:
75
- code = query_params["code"][0]
76
- self.server.auth_callback(code) # type: ignore
77
- self.send_response(200)
78
- self.end_headers()
79
- else:
80
- self.send_error(400, "Missing 'code' parameter")
81
-
82
- self.server.shutdown()
83
-
84
- def log_message(self, format, *args):
85
- # Suppress log messages
86
- return
87
-
88
-
89
- AUTH_SERVER_PORT = 49160
90
-
91
-
92
- class AuthServer(socketserver.TCPServer):
93
- allow_reuse_address = True
94
-
95
- def __init__(self, server_address, RequestHandlerClass, auth_callback, auth_host):
96
- self.auth_callback = auth_callback
97
- self.auth_host = auth_host
98
- super().__init__(server_address, RequestHandlerClass)
99
-
100
-
101
- def start_server(auth_callback, auth_host):
102
- with AuthServer(("", AUTH_SERVER_PORT), AuthHandler, auth_callback, auth_host) as httpd:
103
- httpd.timeout = 30
104
- start_time = time.time()
105
- while time.time() - start_time < SERVER_MAX_WAIT_TIME: # Run for a maximum of 180 seconds
106
- httpd.handle_request()
107
-
108
-
109
- @cli.command()
9
+ @cli.command("login", help="Authenticate using the browser.")
110
10
  @click.option(
111
11
  "--host",
112
12
  type=str,
@@ -135,200 +35,5 @@ def start_server(auth_callback, auth_host):
135
35
  default="browser",
136
36
  help="Set the authentication method to use. Default: browser.",
137
37
  )
138
- def login(host: Optional[str], auth_host: str, workspace: str, interactive: bool, method: str):
139
- """Authenticate using the browser."""
140
- try:
141
- cli_config = CLIConfig.get_project_config()
142
- if not host and cli_config.get_token():
143
- host = cli_config.get_host(use_defaults_if_needed=False)
144
- if not host or interactive:
145
- if interactive:
146
- click.echo(FeedbackManager.highlight(message="» Select one region from the list below:"))
147
- else:
148
- click.echo(FeedbackManager.highlight(message="» No region detected, select one from the list below:"))
149
-
150
- regions = get_regions(cli_config)
151
- selected_region = ask_for_region_interactively(regions)
152
-
153
- # If the user cancels the selection, we'll exit
154
- if not selected_region:
155
- sys.exit(1)
156
- host = selected_region.get("api_host")
157
-
158
- if not host:
159
- host = DEFAULT_API_HOST
160
-
161
- host = host.rstrip("/")
162
- auth_host = auth_host.rstrip("/")
163
-
164
- if method == "code":
165
- display_code, one_time_code = create_one_time_code()
166
- click.echo(FeedbackManager.info(message=f"First, copy your one-time code: {display_code}"))
167
- click.echo(FeedbackManager.info(message="Press [Enter] to continue in the browser..."))
168
- input()
169
- click.echo(FeedbackManager.highlight(message="» Opening browser for authentication..."))
170
- params = {
171
- "apiHost": host,
172
- "code": one_time_code,
173
- "method": "code",
174
- }
175
- auth_url = f"{auth_host}/api/cli-login?{urlencode(params)}"
176
- open_url(auth_url)
177
- click.echo(
178
- FeedbackManager.info(message="\nIf browser does not open, please open the following URL manually:")
179
- )
180
- click.echo(FeedbackManager.info(message=auth_url))
181
-
182
- def poll_for_tokens():
183
- while True:
184
- params = {
185
- "apiHost": host,
186
- "cliCode": one_time_code,
187
- "method": "code",
188
- }
189
- response = requests.get(f"{auth_host}/api/cli-login?{urlencode(params)}")
190
-
191
- try:
192
- if response.status_code == 200:
193
- data = response.json()
194
- user_token = data.get("user_token", "")
195
- workspace_token = data.get("workspace_token", "")
196
- if user_token and workspace_token:
197
- authenticate_with_tokens(data, host, cli_config)
198
- break
199
- except Exception:
200
- pass
201
-
202
- time.sleep(2)
203
-
204
- poll_for_tokens()
205
- return
206
-
207
- auth_event = threading.Event()
208
- auth_code: list[str] = [] # Using a list to store the code, as it's mutable
209
-
210
- def auth_callback(code):
211
- auth_code.append(code)
212
- auth_event.set()
213
-
214
- click.echo(FeedbackManager.highlight(message="» Opening browser for authentication..."))
215
- # Start the local server in a separate thread
216
- server_thread = threading.Thread(target=start_server, args=(auth_callback, auth_host))
217
- server_thread.daemon = True
218
- server_thread.start()
219
-
220
- # Open the browser to the auth page
221
- params = {
222
- "apiHost": host,
223
- }
224
-
225
- if workspace:
226
- params["workspace"] = workspace
227
-
228
- auth_url = f"{auth_host}/api/cli-login?{urlencode(params)}"
229
- open_url(auth_url)
230
-
231
- click.echo(FeedbackManager.info(message="\nIf browser does not open, please open the following URL manually:"))
232
- click.echo(FeedbackManager.info(message=auth_url))
233
-
234
- # Wait for the authentication to complete or timeout
235
- if auth_event.wait(timeout=SERVER_MAX_WAIT_TIME): # Wait for up to 180 seconds
236
- params = {}
237
- params["code"] = auth_code[0]
238
- response = requests.get(
239
- f"{auth_host}/api/cli-login?{urlencode(params)}",
240
- )
241
-
242
- data = response.json()
243
- authenticate_with_tokens(data, host, cli_config)
244
- else:
245
- raise Exception("Authentication failed or timed out.")
246
- except Exception as e:
247
- raise CLILoginException(FeedbackManager.error(message=str(e)))
248
-
249
-
250
- def _running_in_wsl() -> bool:
251
- """Return True when Python is executing inside a WSL distro."""
252
- # Fast positive check (modern WSL always sets at least one of these):
253
- if "WSL_DISTRO_NAME" in os.environ or "WSL_INTEROP" in os.environ:
254
- return True
255
-
256
- # Fall back to kernel /proc data
257
- release = platform.uname().release.lower()
258
- if "microsoft" in release: # covers stock WSL kernels
259
- return True
260
- try:
261
- if "microsoft" in open("/proc/version").read().lower():
262
- return True
263
- except FileNotFoundError:
264
- pass
265
- return False
266
-
267
-
268
- def open_url(url: str, *, new_tab: bool = False) -> bool:
269
- # 1. Try the standard library first on CPython ≥ 3.11 this already
270
- # recognises WSL and fires up the Windows default browser for us.
271
- try:
272
- wb: Any = webbrowser.get() # mypy: Any for Py < 3.10
273
- if new_tab:
274
- if wb.open_new_tab(url):
275
- return True
276
- else:
277
- if wb.open(url):
278
- return True
279
- except webbrowser.Error:
280
- pass # keep going
281
-
282
- # 2. Inside WSL, prefer `wslview` if the user has it (wslu package).
283
- if _running_in_wsl() and shutil.which("wslview"):
284
- subprocess.Popen(["wslview", url])
285
- return True
286
-
287
- # 3. Secondary WSL fallback use Windows **start** through cmd.exe.
288
- # Empty "" argument is required so long URLs are not treated as a window title.
289
- if _running_in_wsl():
290
- subprocess.Popen(["cmd.exe", "/c", "start", "", url])
291
- return True
292
-
293
- # 4. Unix last-ditch fallback xdg-open (most minimal container images have it)
294
- if shutil.which("xdg-open"):
295
- subprocess.Popen(["xdg-open", url])
296
- return True
297
-
298
- # 5. If everything failed, let the caller know.
299
- return False
300
-
301
-
302
- def create_one_time_code():
303
- """Create a random one-time code for the authentication process in the format of A2C4-D2G4 (only uppercase letters and digits)"""
304
- seperator = "-"
305
- full_code = "".join(random.choices(string.ascii_uppercase + string.digits, k=8))
306
- parts = [full_code[:4], full_code[4:]]
307
- return seperator.join(parts), full_code
308
-
309
-
310
- def authenticate_with_tokens(data: Dict[str, Any], host: Optional[str], cli_config: CLIConfig):
311
- cli_config.set_token(data.get("workspace_token", ""))
312
- host = host or data.get("api_host", "")
313
- cli_config.set_token_for_host(data.get("workspace_token", ""), host)
314
- cli_config.set_user_token(data.get("user_token", ""))
315
- cli_config.set_host(host)
316
- ws = cli_config.get_client(token=data.get("workspace_token", ""), host=host).workspace_info(version="v1")
317
- for k in ("id", "name", "user_email", "user_id", "scope"):
318
- if k in ws:
319
- cli_config[k] = ws[k]
320
-
321
- path = os.path.join(os.getcwd(), ".tinyb")
322
- cli_config.persist_to_file(override_with_path=path)
323
-
324
- auth_info: Dict[str, Any] = cli_config.get_user_client().check_auth_login()
325
- if not auth_info.get("is_valid", False):
326
- raise Exception(FeedbackManager.error_auth_login_not_valid(host=cli_config.get_host()))
327
-
328
- if not auth_info.get("is_user", False):
329
- raise Exception(FeedbackManager.error_auth_login_not_user(host=cli_config.get_host()))
330
-
331
- click.echo(FeedbackManager.gray(message="\nWorkspace: ") + FeedbackManager.info(message=ws["name"]))
332
- click.echo(FeedbackManager.gray(message="User: ") + FeedbackManager.info(message=ws["user_email"]))
333
- click.echo(FeedbackManager.gray(message="Host: ") + FeedbackManager.info(message=host))
334
- click.echo(FeedbackManager.success(message="\n✓ Authentication successful!"))
38
+ def login_cmd(host: Optional[str], auth_host: str, workspace: str, interactive: bool, method: str):
39
+ login(host, auth_host, workspace, interactive, method)
@@ -0,0 +1,310 @@
1
+ import http.server
2
+ import os
3
+ import platform
4
+ import random
5
+ import shutil
6
+ import socketserver
7
+ import string
8
+ import subprocess
9
+ import sys
10
+ import threading
11
+ import time
12
+ import urllib.parse
13
+ import webbrowser
14
+ from typing import Any, Dict, Optional
15
+ from urllib.parse import urlencode
16
+
17
+ import click
18
+ import requests
19
+
20
+ from tinybird.tb.config import DEFAULT_API_HOST
21
+ from tinybird.tb.modules.common import ask_for_region_interactively, get_regions
22
+ from tinybird.tb.modules.config import CLIConfig
23
+ from tinybird.tb.modules.exceptions import CLILoginException
24
+ from tinybird.tb.modules.feedback_manager import FeedbackManager
25
+
26
+ SERVER_MAX_WAIT_TIME = 180
27
+
28
+
29
+ class AuthHandler(http.server.SimpleHTTPRequestHandler):
30
+ def do_GET(self):
31
+ # The access_token is in the URL fragment, which is not sent to the server
32
+ # We'll send a small HTML page that extracts the token and sends it back to the server
33
+ self.send_response(200)
34
+ self.send_header("Content-type", "text/html")
35
+ self.end_headers()
36
+ self.wfile.write(
37
+ """
38
+ <html>
39
+ <head>
40
+ <style>
41
+ body {{
42
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
43
+ background: #f5f5f5;
44
+ display: flex;
45
+ align-items: center;
46
+ justify-content: center;
47
+ height: 100vh;
48
+ margin: 0;
49
+ }}
50
+ </style>
51
+ </head>
52
+ <body>
53
+ <script>
54
+ const searchParams = new URLSearchParams(window.location.search);
55
+ const code = searchParams.get('code');
56
+ const workspace = searchParams.get('workspace');
57
+ const region = searchParams.get('region');
58
+ const provider = searchParams.get('provider');
59
+ const host = "{auth_host}";
60
+ fetch('/?code=' + code, {{method: 'POST'}})
61
+ .then(() => {{
62
+ window.location.href = host + "/" + provider + "/" + region + "/cli-login?workspace=" + workspace;
63
+ }});
64
+ </script>
65
+ </body>
66
+ </html>
67
+ """.format(auth_host=self.server.auth_host).encode() # type: ignore
68
+ )
69
+
70
+ def do_POST(self):
71
+ parsed_path = urllib.parse.urlparse(self.path)
72
+ query_params = urllib.parse.parse_qs(parsed_path.query)
73
+
74
+ if "code" in query_params:
75
+ code = query_params["code"][0]
76
+ self.server.auth_callback(code) # type: ignore
77
+ self.send_response(200)
78
+ self.end_headers()
79
+ else:
80
+ self.send_error(400, "Missing 'code' parameter")
81
+
82
+ self.server.shutdown()
83
+
84
+ def log_message(self, format, *args):
85
+ # Suppress log messages
86
+ return
87
+
88
+
89
+ AUTH_SERVER_PORT = 49160
90
+
91
+
92
+ class AuthServer(socketserver.TCPServer):
93
+ allow_reuse_address = True
94
+
95
+ def __init__(self, server_address, RequestHandlerClass, auth_callback, auth_host):
96
+ self.auth_callback = auth_callback
97
+ self.auth_host = auth_host
98
+ super().__init__(server_address, RequestHandlerClass)
99
+
100
+
101
+ def start_server(auth_callback, auth_host):
102
+ with AuthServer(("", AUTH_SERVER_PORT), AuthHandler, auth_callback, auth_host) as httpd:
103
+ httpd.timeout = 30
104
+ start_time = time.time()
105
+ while time.time() - start_time < SERVER_MAX_WAIT_TIME: # Run for a maximum of 180 seconds
106
+ httpd.handle_request()
107
+
108
+
109
+ def login(
110
+ host: Optional[str],
111
+ auth_host: str = "https://cloud.tinybird.co",
112
+ workspace: Optional[str] = None,
113
+ interactive: bool = False,
114
+ method: str = "browser",
115
+ ):
116
+ try:
117
+ cli_config = CLIConfig.get_project_config()
118
+ if not host and cli_config.get_token():
119
+ host = cli_config.get_host(use_defaults_if_needed=False)
120
+ if not host or interactive:
121
+ if interactive:
122
+ click.echo(FeedbackManager.highlight(message="» Select one region from the list below:"))
123
+ else:
124
+ click.echo(FeedbackManager.highlight(message="» No region detected, select one from the list below:"))
125
+
126
+ regions = get_regions(cli_config)
127
+ selected_region = ask_for_region_interactively(regions)
128
+
129
+ # If the user cancels the selection, we'll exit
130
+ if not selected_region:
131
+ sys.exit(1)
132
+ host = selected_region.get("api_host")
133
+
134
+ if not host:
135
+ host = DEFAULT_API_HOST
136
+
137
+ host = host.rstrip("/")
138
+ auth_host = auth_host.rstrip("/")
139
+
140
+ if method == "code":
141
+ display_code, one_time_code = create_one_time_code()
142
+ click.echo(FeedbackManager.info(message=f"First, copy your one-time code: {display_code}"))
143
+ click.echo(FeedbackManager.info(message="Press [Enter] to continue in the browser..."))
144
+ input()
145
+ click.echo(FeedbackManager.highlight(message="» Opening browser for authentication..."))
146
+ params = {
147
+ "apiHost": host,
148
+ "code": one_time_code,
149
+ "method": "code",
150
+ }
151
+ auth_url = f"{auth_host}/api/cli-login?{urlencode(params)}"
152
+ open_url(auth_url)
153
+ click.echo(
154
+ FeedbackManager.info(message="\nIf browser does not open, please open the following URL manually:")
155
+ )
156
+ click.echo(FeedbackManager.info(message=auth_url))
157
+
158
+ def poll_for_tokens():
159
+ while True:
160
+ params = {
161
+ "apiHost": host,
162
+ "cliCode": one_time_code,
163
+ "method": "code",
164
+ }
165
+ response = requests.get(f"{auth_host}/api/cli-login?{urlencode(params)}")
166
+
167
+ try:
168
+ if response.status_code == 200:
169
+ data = response.json()
170
+ user_token = data.get("user_token", "")
171
+ workspace_token = data.get("workspace_token", "")
172
+ if user_token and workspace_token:
173
+ authenticate_with_tokens(data, host, cli_config)
174
+ break
175
+ except Exception:
176
+ pass
177
+
178
+ time.sleep(2)
179
+
180
+ poll_for_tokens()
181
+ return
182
+
183
+ auth_event = threading.Event()
184
+ auth_code: list[str] = [] # Using a list to store the code, as it's mutable
185
+
186
+ def auth_callback(code):
187
+ auth_code.append(code)
188
+ auth_event.set()
189
+
190
+ click.echo(FeedbackManager.highlight(message="» Opening browser for authentication..."))
191
+ # Start the local server in a separate thread
192
+ server_thread = threading.Thread(target=start_server, args=(auth_callback, auth_host))
193
+ server_thread.daemon = True
194
+ server_thread.start()
195
+
196
+ # Open the browser to the auth page
197
+ params = {
198
+ "apiHost": host,
199
+ }
200
+
201
+ if workspace:
202
+ params["workspace"] = workspace
203
+
204
+ auth_url = f"{auth_host}/api/cli-login?{urlencode(params)}"
205
+ open_url(auth_url)
206
+
207
+ click.echo(FeedbackManager.info(message="\nIf browser does not open, please open the following URL manually:"))
208
+ click.echo(FeedbackManager.info(message=auth_url))
209
+
210
+ # Wait for the authentication to complete or timeout
211
+ if auth_event.wait(timeout=SERVER_MAX_WAIT_TIME): # Wait for up to 180 seconds
212
+ params = {}
213
+ params["code"] = auth_code[0]
214
+ response = requests.get(
215
+ f"{auth_host}/api/cli-login?{urlencode(params)}",
216
+ )
217
+
218
+ data = response.json()
219
+ authenticate_with_tokens(data, host, cli_config)
220
+ else:
221
+ raise Exception("Authentication failed or timed out.")
222
+ except Exception as e:
223
+ raise CLILoginException(FeedbackManager.error(message=str(e)))
224
+
225
+
226
+ def _running_in_wsl() -> bool:
227
+ """Return True when Python is executing inside a WSL distro."""
228
+ # Fast positive check (modern WSL always sets at least one of these):
229
+ if "WSL_DISTRO_NAME" in os.environ or "WSL_INTEROP" in os.environ:
230
+ return True
231
+
232
+ # Fall back to kernel /proc data
233
+ release = platform.uname().release.lower()
234
+ if "microsoft" in release: # covers stock WSL kernels
235
+ return True
236
+ try:
237
+ if "microsoft" in open("/proc/version").read().lower():
238
+ return True
239
+ except FileNotFoundError:
240
+ pass
241
+ return False
242
+
243
+
244
+ def open_url(url: str, *, new_tab: bool = False) -> bool:
245
+ # 1. Try the standard library first on CPython ≥ 3.11 this already
246
+ # recognises WSL and fires up the Windows default browser for us.
247
+ try:
248
+ wb: Any = webbrowser.get() # mypy: Any for Py < 3.10
249
+ if new_tab:
250
+ if wb.open_new_tab(url):
251
+ return True
252
+ else:
253
+ if wb.open(url):
254
+ return True
255
+ except webbrowser.Error:
256
+ pass # keep going
257
+
258
+ # 2. Inside WSL, prefer `wslview` if the user has it (wslu package).
259
+ if _running_in_wsl() and shutil.which("wslview"):
260
+ subprocess.Popen(["wslview", url])
261
+ return True
262
+
263
+ # 3. Secondary WSL fallback use Windows **start** through cmd.exe.
264
+ # Empty "" argument is required so long URLs are not treated as a window title.
265
+ if _running_in_wsl():
266
+ subprocess.Popen(["cmd.exe", "/c", "start", "", url])
267
+ return True
268
+
269
+ # 4. Unix last-ditch fallback xdg-open (most minimal container images have it)
270
+ if shutil.which("xdg-open"):
271
+ subprocess.Popen(["xdg-open", url])
272
+ return True
273
+
274
+ # 5. If everything failed, let the caller know.
275
+ return False
276
+
277
+
278
+ def create_one_time_code():
279
+ """Create a random one-time code for the authentication process in the format of A2C4-D2G4 (only uppercase letters and digits)"""
280
+ seperator = "-"
281
+ full_code = "".join(random.choices(string.ascii_uppercase + string.digits, k=8))
282
+ parts = [full_code[:4], full_code[4:]]
283
+ return seperator.join(parts), full_code
284
+
285
+
286
+ def authenticate_with_tokens(data: Dict[str, Any], host: Optional[str], cli_config: CLIConfig):
287
+ cli_config.set_token(data.get("workspace_token", ""))
288
+ host = host or data.get("api_host", "")
289
+ cli_config.set_token_for_host(data.get("workspace_token", ""), host)
290
+ cli_config.set_user_token(data.get("user_token", ""))
291
+ cli_config.set_host(host)
292
+ ws = cli_config.get_client(token=data.get("workspace_token", ""), host=host).workspace_info(version="v1")
293
+ for k in ("id", "name", "user_email", "user_id", "scope"):
294
+ if k in ws:
295
+ cli_config[k] = ws[k]
296
+
297
+ path = os.path.join(os.getcwd(), ".tinyb")
298
+ cli_config.persist_to_file(override_with_path=path)
299
+
300
+ auth_info: Dict[str, Any] = cli_config.get_user_client().check_auth_login()
301
+ if not auth_info.get("is_valid", False):
302
+ raise Exception(FeedbackManager.error_auth_login_not_valid(host=cli_config.get_host()))
303
+
304
+ if not auth_info.get("is_user", False):
305
+ raise Exception(FeedbackManager.error_auth_login_not_user(host=cli_config.get_host()))
306
+
307
+ click.echo(FeedbackManager.gray(message="\nWorkspace: ") + FeedbackManager.info(message=ws["name"]))
308
+ click.echo(FeedbackManager.gray(message="User: ") + FeedbackManager.info(message=ws["user_email"]))
309
+ click.echo(FeedbackManager.gray(message="Host: ") + FeedbackManager.info(message=host))
310
+ click.echo(FeedbackManager.success(message="\n✓ Authentication successful!"))