Skip to content

Commit 670fe79

Browse files
authored
Trigger auth command from prompt (#1423)
1 parent 4dc9c87 commit 670fe79

File tree

4 files changed

+92
-78
lines changed

4 files changed

+92
-78
lines changed

logfire/_internal/cli/__init__.py

Lines changed: 3 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -8,27 +8,26 @@
88
import platform
99
import sys
1010
import warnings
11-
import webbrowser
1211
from collections.abc import Sequence
1312
from operator import itemgetter
1413
from pathlib import Path
1514
from typing import Any
16-
from urllib.parse import urlparse
1715

1816
import requests
1917
from opentelemetry import trace
2018
from rich.console import Console
2119

22-
from logfire._internal.cli.prompt import parse_prompt
2320
from logfire.exceptions import LogfireConfigError
2421
from logfire.propagate import ContextCarrier, get_context
2522

2623
from ...version import VERSION
27-
from ..auth import DEFAULT_FILE, HOME_LOGFIRE, UserTokenCollection, poll_for_token, request_device_code
24+
from ..auth import HOME_LOGFIRE
2825
from ..client import LogfireClient
2926
from ..config import REGIONS, LogfireCredentials, get_base_url_from_token
3027
from ..config_params import ParamManager
3128
from ..tracer import SDKTracerProvider
29+
from .auth import parse_auth
30+
from .prompt import parse_prompt
3231
from .run import collect_instrumentation_context, parse_run, print_otel_summary
3332

3433
BASE_OTEL_INTEGRATION_URL = 'https://opentelemetry-python-contrib.readthedocs.io/en/latest/instrumentation/'
@@ -117,68 +116,6 @@ def parse_inspect(args: argparse.Namespace) -> None:
117116
console.print('No recommended packages found. You are all set!', style='green') # pragma: no cover
118117

119118

120-
def parse_auth(args: argparse.Namespace) -> None:
121-
"""Authenticate with Logfire.
122-
123-
This will authenticate your machine with Logfire and store the credentials.
124-
"""
125-
logfire_url: str | None = args.logfire_url
126-
127-
tokens_collection = UserTokenCollection()
128-
logged_in = tokens_collection.is_logged_in(logfire_url)
129-
130-
if logged_in:
131-
sys.stderr.writelines(
132-
(
133-
f'You are already logged in. (Your credentials are stored in {DEFAULT_FILE})\n',
134-
'If you would like to log in using a different account, use the --region argument:\n',
135-
'logfire --region <region> auth\n',
136-
)
137-
)
138-
return
139-
140-
sys.stderr.writelines(
141-
(
142-
'\n',
143-
'Welcome to Logfire! 🔥\n',
144-
'Before you can send data to Logfire, we need to authenticate you.\n',
145-
'\n',
146-
)
147-
)
148-
if not logfire_url:
149-
selected_region = -1
150-
while not (1 <= selected_region <= len(REGIONS)):
151-
sys.stderr.write('Logfire is available in multiple data regions. Please select one:\n')
152-
for i, (region_id, region_data) in enumerate(REGIONS.items(), start=1):
153-
sys.stderr.write(f'{i}. {region_id.upper()} (GCP region: {region_data["gcp_region"]})\n')
154-
155-
try:
156-
selected_region = int(
157-
input(f'Selected region [{"/".join(str(i) for i in range(1, len(REGIONS) + 1))}]: ')
158-
)
159-
except ValueError:
160-
selected_region = -1
161-
logfire_url = list(REGIONS.values())[selected_region - 1]['base_url']
162-
163-
device_code, frontend_auth_url = request_device_code(args._session, logfire_url)
164-
frontend_host = urlparse(frontend_auth_url).netloc
165-
input(f'Press Enter to open {frontend_host} in your browser...')
166-
try:
167-
webbrowser.open(frontend_auth_url, new=2)
168-
except webbrowser.Error:
169-
pass
170-
sys.stderr.writelines(
171-
(
172-
f"Please open {frontend_auth_url} in your browser to authenticate if it hasn't already.\n",
173-
'Waiting for you to authenticate with Logfire...\n',
174-
)
175-
)
176-
177-
tokens_collection.add_token(logfire_url, poll_for_token(args._session, device_code, logfire_url))
178-
sys.stderr.write('Successfully authenticated!\n')
179-
sys.stderr.write(f'\nYour Logfire credentials are stored in {DEFAULT_FILE}\n')
180-
181-
182119
def parse_list_projects(args: argparse.Namespace) -> None:
183120
"""List user projects."""
184121
client = LogfireClient.from_url(args.logfire_url)

logfire/_internal/cli/auth.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
from __future__ import annotations
2+
3+
import argparse
4+
import sys
5+
import webbrowser
6+
from urllib.parse import urlparse
7+
8+
from ..auth import DEFAULT_FILE, UserTokenCollection, poll_for_token, request_device_code
9+
from ..config import REGIONS
10+
11+
12+
def parse_auth(args: argparse.Namespace) -> None:
13+
"""Authenticate with Logfire.
14+
15+
This will authenticate your machine with Logfire and store the credentials.
16+
"""
17+
logfire_url: str | None = args.logfire_url
18+
19+
tokens_collection = UserTokenCollection()
20+
logged_in = tokens_collection.is_logged_in(logfire_url)
21+
22+
if logged_in:
23+
sys.stderr.writelines(
24+
(
25+
f'You are already logged in. (Your credentials are stored in {DEFAULT_FILE})\n',
26+
'If you would like to log in using a different account, use the --region argument:\n',
27+
'logfire --region <region> auth\n',
28+
)
29+
)
30+
return
31+
32+
sys.stderr.writelines(
33+
(
34+
'\n',
35+
'Welcome to Logfire! 🔥\n',
36+
'Before you can send data to Logfire, we need to authenticate you.\n',
37+
'\n',
38+
)
39+
)
40+
if not logfire_url:
41+
selected_region = -1
42+
while not (1 <= selected_region <= len(REGIONS)):
43+
sys.stderr.write('Logfire is available in multiple data regions. Please select one:\n')
44+
for i, (region_id, region_data) in enumerate(REGIONS.items(), start=1):
45+
sys.stderr.write(f'{i}. {region_id.upper()} (GCP region: {region_data["gcp_region"]})\n')
46+
47+
try:
48+
selected_region = int(
49+
input(f'Selected region [{"/".join(str(i) for i in range(1, len(REGIONS) + 1))}]: ')
50+
)
51+
except ValueError:
52+
selected_region = -1
53+
logfire_url = list(REGIONS.values())[selected_region - 1]['base_url']
54+
55+
device_code, frontend_auth_url = request_device_code(args._session, logfire_url)
56+
frontend_host = urlparse(frontend_auth_url).netloc
57+
58+
# We are not using the `prompt` parameter from `input` here because we want to write to stderr.
59+
sys.stderr.write(f'Press Enter to open {frontend_host} in your browser...\n')
60+
input()
61+
62+
try:
63+
webbrowser.open(frontend_auth_url, new=2)
64+
except webbrowser.Error:
65+
pass
66+
sys.stderr.writelines(
67+
(
68+
f"Please open {frontend_auth_url} in your browser to authenticate if it hasn't already.\n",
69+
'Waiting for you to authenticate with Logfire...\n',
70+
)
71+
)
72+
73+
tokens_collection.add_token(logfire_url, poll_for_token(args._session, device_code, logfire_url))
74+
sys.stderr.write('Successfully authenticated!\n')
75+
sys.stderr.write(f'\nYour Logfire credentials are stored in {DEFAULT_FILE}\n')

logfire/_internal/cli/prompt.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
from rich.console import Console
1616

17+
from logfire._internal.cli.auth import parse_auth
1718
from logfire._internal.client import LogfireClient
1819
from logfire.exceptions import LogfireConfigError
1920

@@ -34,9 +35,9 @@ def parse_prompt(args: argparse.Namespace) -> None:
3435

3536
try:
3637
client = LogfireClient.from_url(args.logfire_url)
37-
except LogfireConfigError as e: # pragma: no cover
38-
console.print(e.args[0], style='red')
39-
return
38+
except LogfireConfigError: # pragma: no cover
39+
parse_auth(args)
40+
client = LogfireClient.from_url(args.logfire_url)
4041

4142
if args.claude:
4243
configure_claude(client, args.organization, args.project, console)

tests/test_cli.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -333,8 +333,8 @@ def test_auth(tmp_path: Path, webbrowser_error: bool, capsys: pytest.CaptureFixt
333333
with ExitStack() as stack:
334334
stack.enter_context(patch('logfire._internal.auth.DEFAULT_FILE', auth_file))
335335
# Necessary to assert that credentials are written to the `auth_file` (which happens from the `cli` module)
336-
stack.enter_context(patch('logfire._internal.cli.DEFAULT_FILE', auth_file))
337-
stack.enter_context(patch('logfire._internal.cli.input'))
336+
stack.enter_context(patch('logfire._internal.cli.auth.DEFAULT_FILE', auth_file))
337+
stack.enter_context(patch('logfire._internal.cli.auth.input'))
338338
webbrowser_open = stack.enter_context(
339339
patch('webbrowser.open', side_effect=webbrowser.Error if webbrowser_error is True else None)
340340
)
@@ -369,6 +369,7 @@ def test_auth(tmp_path: Path, webbrowser_error: bool, capsys: pytest.CaptureFixt
369369
'Welcome to Logfire! 🔥',
370370
'Before you can send data to Logfire, we need to authenticate you.',
371371
'',
372+
'Press Enter to open example.com in your browser...',
372373
"Please open http://example.com/auth in your browser to authenticate if it hasn't already.",
373374
'Waiting for you to authenticate with Logfire...',
374375
'Successfully authenticated!',
@@ -384,8 +385,8 @@ def test_auth_temp_failure(tmp_path: Path) -> None:
384385
auth_file = tmp_path / 'default.toml'
385386
with ExitStack() as stack:
386387
stack.enter_context(patch('logfire._internal.auth.DEFAULT_FILE', auth_file))
387-
stack.enter_context(patch('logfire._internal.cli.input'))
388-
stack.enter_context(patch('logfire._internal.cli.webbrowser.open'))
388+
stack.enter_context(patch('logfire._internal.cli.auth.input'))
389+
stack.enter_context(patch('logfire._internal.cli.auth.webbrowser.open'))
389390

390391
m = requests_mock.Mocker()
391392
stack.enter_context(m)
@@ -409,8 +410,8 @@ def test_auth_permanent_failure(tmp_path: Path) -> None:
409410
auth_file = tmp_path / 'default.toml'
410411
with ExitStack() as stack:
411412
stack.enter_context(patch('logfire._internal.auth.DEFAULT_FILE', auth_file))
412-
stack.enter_context(patch('logfire._internal.cli.input'))
413-
stack.enter_context(patch('logfire._internal.cli.webbrowser.open'))
413+
stack.enter_context(patch('logfire._internal.cli.auth.input'))
414+
stack.enter_context(patch('logfire._internal.cli.auth.webbrowser.open'))
414415

415416
m = requests_mock.Mocker()
416417
stack.enter_context(m)
@@ -439,11 +440,11 @@ def test_auth_no_region_specified(tmp_path: Path) -> None:
439440
with ExitStack() as stack:
440441
stack.enter_context(patch('logfire._internal.auth.DEFAULT_FILE', auth_file))
441442
# Necessary to assert that credentials are written to the `auth_file` (which happens from the `cli` module)
442-
stack.enter_context(patch('logfire._internal.cli.DEFAULT_FILE', auth_file))
443+
stack.enter_context(patch('logfire._internal.cli.auth.DEFAULT_FILE', auth_file))
443444
# 'not_an_int' is used as the first input to test that invalid inputs are supported,
444445
# '2' will result in the EU region being used:
445-
stack.enter_context(patch('logfire._internal.cli.input', side_effect=['not_an_int', '2', '']))
446-
stack.enter_context(patch('logfire._internal.cli.webbrowser.open'))
446+
stack.enter_context(patch('logfire._internal.cli.auth.input', side_effect=['not_an_int', '2', '']))
447+
stack.enter_context(patch('logfire._internal.cli.auth.webbrowser.open'))
447448

448449
m = requests_mock.Mocker()
449450
stack.enter_context(m)

0 commit comments

Comments
 (0)