Skip to content

Commit ce01469

Browse files
committed
Add docstrings
1 parent 51d7e48 commit ce01469

File tree

5 files changed

+72
-8
lines changed

5 files changed

+72
-8
lines changed

logfire/variables/config.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,53 +14,68 @@
1414

1515
@dataclass(kw_only=True)
1616
class ValueEquals:
17+
"""Condition that matches when an attribute equals a specific value."""
18+
1719
attribute: str
1820
value: Any
1921
kind: Literal['value-equals'] = 'value-equals'
2022

2123
def matches(self, attributes: Mapping[str, Any]) -> bool:
24+
"""Check if the attribute equals the expected value."""
2225
return attributes.get(self.attribute, object()) == self.value
2326

2427

2528
@dataclass(kw_only=True)
2629
class ValueDoesNotEqual:
30+
"""Condition that matches when an attribute does not equal a specific value."""
31+
2732
attribute: str
2833
value: Any
2934
kind: Literal['value-does-not-equal'] = 'value-does-not-equal'
3035

3136
def matches(self, attributes: Mapping[str, Any]) -> bool:
37+
"""Check if the attribute does not equal the specified value."""
3238
return attributes.get(self.attribute, object()) != self.value
3339

3440

3541
@dataclass(kw_only=True)
3642
class ValueIsIn:
43+
"""Condition that matches when an attribute value is in a set of values."""
44+
3745
attribute: str
3846
values: Sequence[Any]
3947
kind: Literal['value-is-in'] = 'value-is-in'
4048

4149
def matches(self, attributes: Mapping[str, Any]) -> bool:
50+
"""Check if the attribute value is in the allowed set."""
4251
value = attributes.get(self.attribute, object())
4352
return value in self.values
4453

4554

4655
@dataclass(kw_only=True)
4756
class ValueIsNotIn:
57+
"""Condition that matches when an attribute value is not in a set of values."""
58+
4859
attribute: str
4960
values: Sequence[Any]
5061
kind: Literal['value-is-not-in'] = 'value-is-not-in'
5162

5263
def matches(self, attributes: Mapping[str, Any]) -> bool:
64+
"""Check if the attribute value is not in the excluded set."""
5365
value = attributes.get(self.attribute, object())
5466
return value not in self.values
5567

5668

5769
@dataclass(kw_only=True)
5870
class ValueMatchesRegex:
71+
"""Condition that matches when an attribute value matches a regex pattern."""
72+
5973
attribute: str
6074
pattern: str | re.Pattern[str]
6175
kind: Literal['value-matches-regex'] = 'value-matches-regex'
6276

6377
def matches(self, attributes: Mapping[str, Any]) -> bool:
78+
"""Check if the attribute value matches the regex pattern."""
6479
value = attributes.get(self.attribute)
6580
if not isinstance(value, str):
6681
return False
@@ -69,11 +84,14 @@ def matches(self, attributes: Mapping[str, Any]) -> bool:
6984

7085
@dataclass(kw_only=True)
7186
class ValueDoesNotMatchRegex:
87+
"""Condition that matches when an attribute value does not match a regex pattern."""
88+
7289
attribute: str
7390
pattern: str | re.Pattern[str]
7491
kind: Literal['value-does-not-match-regex'] = 'value-does-not-match-regex'
7592

7693
def matches(self, attributes: Mapping[str, Any]) -> bool:
94+
"""Check if the attribute value does not match the regex pattern."""
7795
value = attributes.get(self.attribute)
7896
if not isinstance(value, str):
7997
return False
@@ -82,19 +100,25 @@ def matches(self, attributes: Mapping[str, Any]) -> bool:
82100

83101
@dataclass(kw_only=True)
84102
class KeyIsPresent:
103+
"""Condition that matches when an attribute key is present."""
104+
85105
attribute: str
86106
kind: Literal['key-is-present'] = 'key-is-present'
87107

88108
def matches(self, attributes: Mapping[str, Any]) -> bool:
109+
"""Check if the attribute key exists in the attributes."""
89110
return self.attribute in attributes
90111

91112

92113
@dataclass(kw_only=True)
93114
class KeyIsNotPresent:
115+
"""Condition that matches when an attribute key is not present."""
116+
94117
attribute: str
95118
kind: Literal['key-is-not-present'] = 'key-is-not-present'
96119

97120
def matches(self, attributes: Mapping[str, Any]) -> bool:
121+
"""Check if the attribute key does not exist in the attributes."""
98122
return self.attribute not in attributes
99123

100124

@@ -123,6 +147,8 @@ def matches(self, attributes: Mapping[str, Any]) -> bool:
123147

124148
@dataclass(kw_only=True)
125149
class Rollout:
150+
"""Configuration for variant selection with weighted probabilities."""
151+
126152
variants: dict[VariantKey, float]
127153

128154
@field_validator('variants')
@@ -135,6 +161,7 @@ def _validate_variant_proportions(cls, v: dict[VariantKey, float]):
135161
return v
136162

137163
def select_variant(self, seed: str | None) -> VariantKey | None:
164+
"""Select a variant based on configured weights using optional seeded randomness."""
138165
rand = random.Random(seed)
139166

140167
population: list[VariantKey | None] = []
@@ -153,6 +180,8 @@ def select_variant(self, seed: str | None) -> VariantKey | None:
153180

154181
@dataclass(kw_only=True)
155182
class Variant:
183+
"""A specific variant of a managed variable with its serialized value."""
184+
156185
key: VariantKey
157186
serialized_value: str
158187
# format: Literal['json', 'yaml'] # TODO: Consider supporting yaml, and not just JSON; allows comments and better formatting
@@ -162,12 +191,16 @@ class Variant:
162191

163192
@dataclass(kw_only=True)
164193
class RolloutOverride:
194+
"""An override of the default rollout when specific conditions are met."""
195+
165196
conditions: list[Condition]
166197
rollout: Rollout
167198

168199

169200
@dataclass(kw_only=True)
170201
class VariableConfig:
202+
"""Configuration for a single managed variable including variants and rollout rules."""
203+
171204
name: VariableName
172205
variants: dict[VariantKey, Variant]
173206
rollout: Rollout
@@ -228,6 +261,8 @@ def resolve_variant(
228261

229262
@dataclass(kw_only=True)
230263
class VariablesConfig:
264+
"""Container for all managed variable configurations."""
265+
231266
variables: dict[VariableName, VariableConfig]
232267

233268
@model_validator(mode='after')
@@ -239,6 +274,7 @@ def _validate_variables(self):
239274
return self
240275

241276
def get_validation_errors(self, variables: list[Variable[Any]]) -> dict[str, dict[str | None, Exception]]:
277+
"""Validate that all variable variants can be deserialized to their expected types."""
242278
errors: dict[str, dict[str | None, Exception]] = {}
243279
for variable in variables:
244280
try:
@@ -256,6 +292,7 @@ def get_validation_errors(self, variables: list[Variable[Any]]) -> dict[str, dic
256292

257293
@staticmethod
258294
def validate_python(data: Any) -> VariablesConfig:
295+
"""Parse and validate a VariablesConfig from a Python object."""
259296
return _VariablesConfigAdapter.validate_python(data)
260297

261298

logfire/variables/providers/abstract.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,24 +11,31 @@
1111

1212

1313
class VariableProvider(ABC):
14+
"""Abstract base class for variable value providers."""
15+
1416
@abstractmethod
1517
def get_serialized_value(
1618
self,
1719
variable_name: str,
1820
targeting_key: str | None = None,
1921
attributes: Mapping[str, Any] | None = None,
2022
) -> VariableResolutionDetails[str | None]:
23+
"""Retrieve the serialized value for a variable."""
2124
raise NotImplementedError
2225

2326
def shutdown(self):
27+
"""Clean up any resources used by the provider."""
2428
pass
2529

2630

2731
class NoOpVariableProvider(VariableProvider):
32+
"""A variable provider that always returns None, used when no provider is configured."""
33+
2834
def get_serialized_value(
2935
self,
3036
variable_name: str,
3137
targeting_key: str | None = None,
3238
attributes: Mapping[str, Any] | None = None,
3339
) -> VariableResolutionDetails[str | None]:
40+
"""Return None for all variable lookups."""
3441
return VariableResolutionDetails(value=None, _reason='resolved')

logfire/variables/providers/local.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212

1313
class LogfireLocalProvider(VariableProvider):
14+
"""Variable provider that resolves values from a local in-memory configuration."""
15+
1416
def __init__(
1517
self,
1618
config: VariablesConfig | Callable[[], VariablesConfig],
@@ -31,6 +33,7 @@ def get_serialized_value(
3133
targeting_key: str | None = None,
3234
attributes: Mapping[str, Any] | None = None,
3335
) -> VariableResolutionDetails[str | None]:
36+
"""Resolve a variable's serialized value from the local configuration."""
3437
variables_config = self.get_config()
3538

3639
variable_config = variables_config.variables.get(variable_name)

logfire/variables/providers/remote.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,11 @@
2525
# TODO: Do we need to provide a mechanism for whether the LogfireRemoteProvider should block to retrieve the config
2626
# during startup or do synchronize in the background?
2727
class LogfireRemoteProvider(VariableProvider):
28-
# The threading implementation draws heavily from opentelemetry.sdk._shared_internal.BatchProcessor
29-
# You might look there to better understand where some of this logic came from if it seems wrong.
28+
"""Variable provider that fetches configuration from a remote Logfire API.
29+
30+
The threading implementation draws heavily from opentelemetry.sdk._shared_internal.BatchProcessor.
31+
"""
32+
3033
def __init__(
3134
self,
3235
base_url: str,
@@ -103,6 +106,7 @@ def _get(self, endpoint: str, *, params: dict[str, Any] | None = None, error_mes
103106
warnings.warn(error_message, category=RuntimeWarning)
104107

105108
def refresh(self, force: bool = False):
109+
"""Fetch the latest variable configuration from the remote API."""
106110
# TODO: Probably makes sense to replace this with something that just polls for a version number or hash
107111
# or similar, rather than the whole config, and only grabs the whole config if that version or hash changes.
108112
with self._refresh_lock: # Make at most one request at a time
@@ -134,6 +138,7 @@ def get_serialized_value(
134138
targeting_key: str | None = None,
135139
attributes: Mapping[str, Any] | None = None,
136140
) -> VariableResolutionDetails[str | None]:
141+
"""Resolve a variable's serialized value from the remote configuration."""
137142
if self._pid != os.getpid():
138143
self._reset_once.do_once(self._at_fork_reinit)
139144

@@ -159,6 +164,7 @@ def get_serialized_value(
159164
return VariableResolutionDetails(value=variant.serialized_value, variant=variant.key, _reason='resolved')
160165

161166
def shutdown(self):
167+
"""Stop the background polling thread and clean up resources."""
162168
if self._shutdown:
163169
return
164170
self._shutdown = True

logfire/variables/variable.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626

2727
@dataclass(kw_only=True)
2828
class VariableResolutionDetails(Generic[T_co]):
29+
"""Details about a variable resolution including value, variant, and any errors."""
30+
2931
value: T_co
3032
variant: str | None = None
3133
exception: Exception | None = None
@@ -34,15 +36,20 @@ class VariableResolutionDetails(Generic[T_co]):
3436
]
3537

3638
def with_value(self, v: T) -> VariableResolutionDetails[T]:
39+
"""Return a copy of this result with a different value."""
3740
return replace(self, value=v) # pyright: ignore[reportReturnType]
3841

3942

4043
class ResolveFunction(Protocol[T_co]):
44+
"""Protocol for functions that resolve variable values based on context."""
45+
4146
def __call__(self, targeting_key: str | None, attributes: Mapping[str, Any] | None) -> T_co:
47+
"""Resolve the variable value given a targeting key and attributes."""
4248
raise NotImplementedError
4349

4450

4551
def is_resolve_function(f: Any) -> TypeIs[ResolveFunction[Any]]:
52+
"""Check if a callable matches the ResolveFunction signature."""
4653
if not callable(f):
4754
return False
4855
signature = inspect.signature(f)
@@ -53,12 +60,13 @@ def is_resolve_function(f: Any) -> TypeIs[ResolveFunction[Any]]:
5360

5461

5562
class Variable(Generic[T]):
56-
"""TODO: Need to add otel instrumentation in some way
57-
Should the default be that logfire dumps a span with the details into the project for you?
58-
And there's no in-process otel? But you can enable that?
59-
TODO: Add get_sync method or similar
60-
TODO: Need to decide how this is going to work. Options:
61-
"""
63+
"""A managed variable that can be resolved dynamically based on configuration."""
64+
65+
# TODO: Need to add otel instrumentation in some way
66+
# Should the default be that logfire dumps a span with the details into the project for you?
67+
# And there's no in-process otel? But you can enable that?
68+
# TODO: Add get_sync method or similar
69+
# TODO: Need to decide how this is going to work. Options:
6270

6371
name: str
6472
default: T | ResolveFunction[T]
@@ -82,6 +90,7 @@ def __init__(
8290

8391
@contextmanager
8492
def override(self, value: T | ResolveFunction[T]) -> Iterator[None]:
93+
"""Context manager to temporarily override this variable's value."""
8594
current = _VARIABLE_OVERRIDES.get() or {}
8695
token = _VARIABLE_OVERRIDES.set({**current, self.name: value})
8796
try:
@@ -90,11 +99,13 @@ def override(self, value: T | ResolveFunction[T]) -> Iterator[None]:
9099
_VARIABLE_OVERRIDES.reset(token)
91100

92101
async def get(self, targeting_key: str | None = None, attributes: Mapping[str, Any] | None = None) -> T:
102+
"""Resolve and return the variable's value."""
93103
return (await self.get_details(targeting_key, attributes)).value
94104

95105
async def get_details(
96106
self, targeting_key: str | None = None, attributes: Mapping[str, Any] | None = None
97107
) -> VariableResolutionDetails[T]:
108+
"""Resolve the variable and return full details including variant and any errors."""
98109
merged_attributes = self._get_merged_attributes(attributes)
99110

100111
# TODO: How much of the following code should be in the try: except:?

0 commit comments

Comments
 (0)