Skip to content

Commit 22ba035

Browse files
committed
Partial feedback
1 parent 58b39dc commit 22ba035

File tree

4 files changed

+79
-63
lines changed

4 files changed

+79
-63
lines changed

logfire/_internal/auth.py

Lines changed: 50 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import platform
4+
import re
45
import sys
56
import warnings
67
from dataclasses import dataclass
@@ -30,14 +31,37 @@
3031
"""File used to store user tokens."""
3132

3233

34+
PYDANTIC_LOGFIRE_TOKEN_PATTERN = re.compile(
35+
r'^(?P<safe_part>pylf_v(?P<version>[0-9]+)_(?P<region>[a-z]+)_)(?P<token>[a-zA-Z0-9]+)$'
36+
)
37+
38+
39+
class _RegionData(TypedDict):
40+
base_url: str
41+
gcp_region: str
42+
43+
44+
REGIONS: dict[str, _RegionData] = {
45+
'us': {
46+
'base_url': 'https://logfire-us.pydantic.dev',
47+
'gcp_region': 'us-east4',
48+
},
49+
'eu': {
50+
'base_url': 'https://logfire-eu.pydantic.dev',
51+
'gcp_region': 'europe-west4',
52+
},
53+
}
54+
"""The existing Logfire regions."""
55+
56+
3357
class UserTokenData(TypedDict):
3458
"""User token data."""
3559

3660
token: str
3761
expiration: str
3862

3963

40-
class TokensFileData(TypedDict):
64+
class UserTokensFileData(TypedDict):
4165
"""Content of the file containing the user tokens."""
4266

4367
tokens: dict[str, UserTokenData]
@@ -87,43 +111,46 @@ def __str__(self) -> str:
87111
@dataclass
88112
class UserTokenCollection:
89113
"""A collection of user tokens."""
90-
user_tokens: list[UserToken]
114+
115+
user_tokens: dict[str, UserToken]
116+
"""A mapping between base URLs and user tokens."""
91117

92118
@classmethod
93-
def from_tokens(cls, tokens: TokensFileData) -> Self:
94-
return cls(
95-
user_tokens=[
96-
UserToken.from_user_token_data(url, data) # pyright: ignore[reportArgumentType], waiting for PEP 728
97-
for url, data in tokens.items()
98-
]
99-
)
119+
def empty(cls) -> Self:
120+
"""Create an empty user token collection."""
121+
return cls(user_tokens={})
122+
123+
@classmethod
124+
def from_file_data(cls, file_data: UserTokensFileData) -> Self:
125+
return cls(user_tokens={url: UserToken(base_url=url, **data) for url, data in file_data['tokens'].items()})
100126

101127
@classmethod
102128
def from_tokens_file(cls, file: Path) -> Self:
103-
return cls.from_tokens(cast(TokensFileData, read_toml_file(file)))
129+
return cls.from_file_data(cast(UserTokensFileData, read_toml_file(file)))
104130

105131
def get_token(self, base_url: str | None = None) -> UserToken:
132+
tokens_list = list(self.user_tokens.values())
106133
if base_url is not None:
107-
token = next((t for t in self.user_tokens if t.base_url == base_url), None)
134+
token = next((t for t in tokens_list if t.base_url == base_url), None)
108135
if token is None:
109136
raise LogfireConfigError(
110137
f'No user token was found matching the {base_url} Logfire URL. '
111138
'Please run `logfire auth` to authenticate.'
112139
)
113140
else:
114-
if len(self.user_tokens) == 1:
115-
token = self.user_tokens[0]
116-
elif len(self.user_tokens) >= 2:
141+
if len(tokens_list) == 1:
142+
token = tokens_list[0]
143+
elif len(tokens_list) >= 2:
117144
choices_str = '\n'.join(
118145
f'{i}. {token} ({"expired" if token.is_expired else "valid"})'
119-
for i, token in enumerate(self.user_tokens, start=1)
146+
for i, token in enumerate(tokens_list, start=1)
120147
)
121148
int_choice = IntPrompt.ask(
122149
f'Multiple user tokens found. Please select one:\n{choices_str}\n',
123-
choices=[str(i) for i in range(1, len(self.user_tokens) + 1)],
150+
choices=[str(i) for i in range(1, len(tokens_list) + 1)],
124151
)
125-
token = self.user_tokens[int_choice - 1]
126-
else: # self.user_tokens == []
152+
token = tokens_list[int_choice - 1]
153+
else: # tokens_list == []
127154
raise LogfireConfigError('No user tokens are available. Please run `logfire auth` to authenticate.')
128155

129156
if token.is_expired:
@@ -132,28 +159,21 @@ def get_token(self, base_url: str | None = None) -> UserToken:
132159

133160
def is_logged_in(self, base_url: str | None = None) -> bool:
134161
if base_url is not None:
135-
tokens = (t for t in self.user_tokens if t.base_url == base_url)
162+
tokens = (t for t in self.user_tokens.values() if t.base_url == base_url)
136163
else:
137-
tokens = self.user_tokens
164+
tokens = self.user_tokens.values()
138165
return any(not t.is_expired for t in tokens)
139166

140167
def add_token(self, base_url: str, token: UserTokenData) -> UserToken:
141-
existing_token = next((t for t in self.user_tokens if t.base_url == base_url), None)
142-
if existing_token:
143-
token_index = self.user_tokens.index(existing_token)
144-
self.user_tokens.remove(existing_token)
145-
else:
146-
token_index = len(self.user_tokens)
147-
148168
user_token = UserToken.from_user_token_data(base_url, token)
149-
self.user_tokens.insert(token_index, user_token)
169+
self.user_tokens[base_url] = UserToken.from_user_token_data(base_url, token)
150170
return user_token
151171

152172
def dump(self, path: Path) -> None:
153173
# There's no standard library package to write TOML files, so we'll write it manually.
154174
with path.open('w') as f:
155-
for user_token in self.user_tokens:
156-
f.write(f'[tokens."{user_token.base_url}"]\n')
175+
for base_url, user_token in self.user_tokens.items():
176+
f.write(f'[tokens."{base_url}"]\n')
157177
f.write(f'token = "{user_token.token}"\n')
158178
f.write(f'expiration = "{user_token.expiration}"\n')
159179

logfire/_internal/cli.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ def parse_auth(args: argparse.Namespace) -> None:
209209
if DEFAULT_FILE.is_file():
210210
tokens_collection = default_token_collection()
211211
else:
212-
tokens_collection = UserTokenCollection.from_tokens({'tokens': {}})
212+
tokens_collection = UserTokenCollection.empty()
213213

214214
logged_in = tokens_collection.is_logged_in(logfire_url)
215215

@@ -270,8 +270,7 @@ def parse_auth(args: argparse.Namespace) -> None:
270270
def parse_list_projects(args: argparse.Namespace) -> None:
271271
"""List user projects."""
272272
logfire_url: str | None = args.logfire_url
273-
user_token = default_token_collection().get_token(logfire_url)
274-
client = LogfireClient(user_token)
273+
client = LogfireClient.from_url(logfire_url)
275274

276275
projects = client.get_user_projects()
277276
if projects:
@@ -303,8 +302,7 @@ def parse_create_new_project(args: argparse.Namespace) -> None:
303302
"""Create a new project."""
304303
data_dir = Path(args.data_dir)
305304
logfire_url: str | None = args.logfire_url
306-
user_token = default_token_collection().get_token(logfire_url)
307-
client = LogfireClient(user_token)
305+
client = LogfireClient.from_url(logfire_url)
308306

309307
project_name = args.project_name
310308
organization = args.org
@@ -315,16 +313,15 @@ def parse_create_new_project(args: argparse.Namespace) -> None:
315313
default_organization=default_organization,
316314
project_name=project_name,
317315
)
318-
credentials = _write_credentials(project_info, data_dir, user_token.base_url)
316+
credentials = _write_credentials(project_info, data_dir, client.base_url)
319317
sys.stderr.write(f'Project created successfully. You will be able to view it at: {credentials.project_url}\n')
320318

321319

322320
def parse_use_project(args: argparse.Namespace) -> None:
323321
"""Use an existing project."""
324322
data_dir = Path(args.data_dir)
325323
logfire_url: str | None = args.logfire_url
326-
user_token = default_token_collection().get_token(logfire_url)
327-
client = LogfireClient(user_token)
324+
client = LogfireClient.from_url(logfire_url)
328325

329326
project_name = args.project_name
330327
organization = args.org

logfire/_internal/client.py

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,17 @@
11
from __future__ import annotations
22

3-
from typing import TYPE_CHECKING, Any
3+
from typing import Any
44
from urllib.parse import urljoin
55

66
from requests import Response, Session
7+
from typing_extensions import Self
78

89
from logfire.exceptions import LogfireConfigError
910
from logfire.version import VERSION
1011

12+
from .auth import UserToken, UserTokenCollection, default_token_collection
1113
from .utils import UnexpectedResponse
1214

13-
if TYPE_CHECKING:
14-
from .auth import UserToken
15-
16-
1715
UA_HEADER = f'logfire/{VERSION}'
1816

1917

@@ -27,6 +25,12 @@ def __init__(self, reason: str, /) -> None:
2725

2826

2927
class LogfireClient:
28+
"""A Logfire HTTP client to interact with the API.
29+
30+
Args:
31+
user_token: The user token to use when authenticating against the API.
32+
"""
33+
3034
def __init__(self, user_token: UserToken) -> None:
3135
if user_token.is_expired:
3236
raise RuntimeError
@@ -35,6 +39,20 @@ def __init__(self, user_token: UserToken) -> None:
3539
self._session = Session()
3640
self._session.headers.update({'Authorization': self._token, 'User-Agent': UA_HEADER})
3741

42+
@classmethod
43+
def from_url(cls, base_url: str | None, token_collection: UserTokenCollection | None = None) -> Self:
44+
"""Create a client from the provided base URL.
45+
46+
Args:
47+
base_url: The base URL to use when looking for a user token. If `None`, will prompt
48+
the user into selecting a token from the token collection (or, if only one available,
49+
use it directly).
50+
token_collection: The token collection to use when looking for the user token. Defaults
51+
to the default token collection from `~/.logfire/default.toml`.
52+
"""
53+
token_collection = token_collection or default_token_collection()
54+
return cls(user_token=token_collection.get_token(base_url))
55+
3856
def _get(self, endpoint: str) -> Response:
3957
response = self._session.get(urljoin(self.base_url, endpoint))
4058
UnexpectedResponse.raise_for_status(response)

logfire/_internal/config.py

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,6 @@
6565
from logfire.version import VERSION
6666

6767
from ..propagate import NoExtractTraceContextPropagator, WarnOnExtractTraceContextPropagator
68-
from .auth import default_token_collection
6968
from .client import InvalidProjectName, LogfireClient, ProjectAlreadyExists
7069
from .config_params import ParamManager, PydanticPluginRecordValues
7170
from .constants import (
@@ -897,8 +896,7 @@ def add_span_processor(span_processor: SpanProcessor) -> None:
897896
# if we still don't have a token, try initializing a new project and writing a new creds file
898897
# note, we only do this if `send_to_logfire` is explicitly `True`, not 'if-token-present'
899898
if self.send_to_logfire is True and credentials is None:
900-
user_token = default_token_collection().get_token(self.advanced.base_url)
901-
client = LogfireClient(user_token)
899+
client = LogfireClient.from_url(self.advanced.base_url)
902900
credentials = LogfireCredentials.initialize_project(client=client)
903901
credentials.write_creds_file(self.data_dir)
904902

@@ -1596,23 +1594,6 @@ def _get_creds_file(creds_dir: Path) -> Path:
15961594
return creds_dir / CREDENTIALS_FILENAME
15971595

15981596

1599-
def _get_token_repr(url: str, token: str) -> str:
1600-
region = 'us'
1601-
if match := PYDANTIC_LOGFIRE_TOKEN_PATTERN.match(token):
1602-
region = match.group('region')
1603-
if region not in REGIONS:
1604-
region = 'us'
1605-
1606-
token_repr = f'{region.upper()} ({url}) - '
1607-
if match:
1608-
# new_token, include prefix and 5 chars
1609-
token_repr += match.group('safe_part') + match.group('token')[:5]
1610-
else:
1611-
token_repr += token[:5]
1612-
token_repr += '****'
1613-
return token_repr
1614-
1615-
16161597
def get_base_url_from_token(token: str) -> str:
16171598
"""Get the base API URL from the token's region."""
16181599
# default to US for tokens that were created before regions were added:

0 commit comments

Comments
 (0)