logfire-api 4.35.0__tar.gz → 4.36.0__tar.gz

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.
Files changed (126) hide show
  1. {logfire_api-4.35.0 → logfire_api-4.36.0}/PKG-INFO +1 -1
  2. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/__init__.pyi +2 -1
  3. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/config.pyi +8 -4
  4. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/logs.pyi +2 -2
  5. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/main.pyi +19 -10
  6. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/metrics.pyi +2 -2
  7. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/tracer.pyi +2 -2
  8. logfire_api-4.36.0/logfire_api/variables/__init__.pyi +6 -0
  9. logfire_api-4.36.0/logfire_api/variables/_handlebars.pyi +52 -0
  10. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/variables/abstract.pyi +36 -6
  11. logfire_api-4.36.0/logfire_api/variables/composition.pyi +102 -0
  12. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/variables/config.pyi +12 -5
  13. logfire_api-4.36.0/logfire_api/variables/template_validation.pyi +53 -0
  14. logfire_api-4.36.0/logfire_api/variables/variable.pyi +248 -0
  15. {logfire_api-4.35.0 → logfire_api-4.36.0}/pyproject.toml +1 -1
  16. logfire_api-4.35.0/logfire_api/variables/__init__.pyi +0 -5
  17. logfire_api-4.35.0/logfire_api/variables/variable.pyi +0 -130
  18. {logfire_api-4.35.0 → logfire_api-4.36.0}/.gitignore +0 -0
  19. {logfire_api-4.35.0 → logfire_api-4.36.0}/README.md +0 -0
  20. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/__init__.py +0 -0
  21. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/__init__.pyi +0 -0
  22. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/ast_utils.pyi +0 -0
  23. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/async_.pyi +0 -0
  24. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/auth.pyi +0 -0
  25. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/auto_trace/__init__.pyi +0 -0
  26. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/auto_trace/import_hook.pyi +0 -0
  27. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/auto_trace/rewrite_ast.pyi +0 -0
  28. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/auto_trace/types.pyi +0 -0
  29. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/baggage.pyi +0 -0
  30. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/cli/__init__.pyi +0 -0
  31. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/cli/ai_tools.pyi +0 -0
  32. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/cli/auth.pyi +0 -0
  33. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/cli/gateway.pyi +0 -0
  34. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/cli/gateway_auth.pyi +0 -0
  35. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/cli/prompt.pyi +0 -0
  36. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/cli/run.pyi +0 -0
  37. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/cli.pyi +0 -0
  38. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/client.pyi +0 -0
  39. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/collect_system_info.pyi +0 -0
  40. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/config_params.pyi +0 -0
  41. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/constants.pyi +0 -0
  42. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/db_statement_summary.pyi +0 -0
  43. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/exporters/__init__.pyi +0 -0
  44. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/exporters/console.pyi +0 -0
  45. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/exporters/dynamic_batch.pyi +0 -0
  46. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/exporters/logs.pyi +0 -0
  47. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/exporters/otlp.pyi +0 -0
  48. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/exporters/processor_wrapper.pyi +0 -0
  49. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/exporters/quiet_metrics.pyi +0 -0
  50. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/exporters/remove_pending.pyi +0 -0
  51. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/exporters/tail_sampling.pyi +0 -0
  52. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/exporters/wrapper.pyi +0 -0
  53. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/formatter.pyi +0 -0
  54. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/forwarding.pyi +0 -0
  55. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/instrument.pyi +0 -0
  56. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/integrations/__init__.pyi +0 -0
  57. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/integrations/aiohttp_client.pyi +0 -0
  58. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/integrations/aiohttp_server.pyi +0 -0
  59. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/integrations/asgi.pyi +0 -0
  60. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/integrations/asyncpg.pyi +0 -0
  61. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/integrations/aws_lambda.pyi +0 -0
  62. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/integrations/celery.pyi +0 -0
  63. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/integrations/claude_agent_sdk.pyi +0 -0
  64. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/integrations/django.pyi +0 -0
  65. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/integrations/dspy.pyi +0 -0
  66. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/integrations/executors.pyi +0 -0
  67. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/integrations/fastapi.pyi +0 -0
  68. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/integrations/flask.pyi +0 -0
  69. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/integrations/google_genai.pyi +0 -0
  70. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/integrations/httpx.pyi +0 -0
  71. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/integrations/litellm.pyi +0 -0
  72. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/integrations/mcp.pyi +0 -0
  73. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/integrations/mysql.pyi +0 -0
  74. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/integrations/openai_agents.pyi +0 -0
  75. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/integrations/print.pyi +0 -0
  76. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/integrations/psycopg.pyi +0 -0
  77. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/integrations/pydantic_ai.pyi +0 -0
  78. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/integrations/pymongo.pyi +0 -0
  79. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/integrations/pytest.pyi +0 -0
  80. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/integrations/redis.pyi +0 -0
  81. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/integrations/requests.pyi +0 -0
  82. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/integrations/sqlalchemy.pyi +0 -0
  83. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/integrations/sqlite3.pyi +0 -0
  84. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/integrations/starlette.pyi +0 -0
  85. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/integrations/surrealdb.pyi +0 -0
  86. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/integrations/system_metrics.pyi +0 -0
  87. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/integrations/wsgi.pyi +0 -0
  88. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/json_encoder.pyi +0 -0
  89. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/json_formatter.pyi +0 -0
  90. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/json_schema.pyi +0 -0
  91. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/json_types.pyi +0 -0
  92. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/scrubbing.pyi +0 -0
  93. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/server_response.pyi +0 -0
  94. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/stack_info.pyi +0 -0
  95. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/ulid.pyi +0 -0
  96. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/_internal/utils.pyi +0 -0
  97. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/cli.pyi +0 -0
  98. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/db_api.pyi +0 -0
  99. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/exceptions.pyi +0 -0
  100. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/experimental/__init__.pyi +0 -0
  101. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/experimental/annotations.pyi +0 -0
  102. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/experimental/api_client.pyi +0 -0
  103. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/experimental/datasets/__init__.pyi +0 -0
  104. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/experimental/forwarding.pyi +0 -0
  105. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/experimental/query_client.pyi +0 -0
  106. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/integrations/__init__.pyi +0 -0
  107. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/integrations/aiohttp_client.pyi +0 -0
  108. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/integrations/flask.pyi +0 -0
  109. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/integrations/httpx.pyi +0 -0
  110. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/integrations/logging.pyi +0 -0
  111. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/integrations/loguru.pyi +0 -0
  112. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/integrations/psycopg.pyi +0 -0
  113. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/integrations/pydantic.pyi +0 -0
  114. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/integrations/redis.pyi +0 -0
  115. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/integrations/sqlalchemy.pyi +0 -0
  116. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/integrations/structlog.pyi +0 -0
  117. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/integrations/wsgi.pyi +0 -0
  118. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/propagate.pyi +0 -0
  119. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/py.typed +0 -0
  120. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/query_client.pyi +0 -0
  121. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/sampling/__init__.pyi +0 -0
  122. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/sampling/_tail_sampling.pyi +0 -0
  123. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/types.pyi +0 -0
  124. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/variables/local.pyi +0 -0
  125. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/variables/remote.pyi +0 -0
  126. {logfire_api-4.35.0 → logfire_api-4.36.0}/logfire_api/version.pyi +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: logfire-api
3
- Version: 4.35.0
3
+ Version: 4.36.0
4
4
  Summary: Shim for the Logfire SDK which does nothing unless Logfire is installed
5
5
  Author-email: Pydantic Team <engineering@pydantic.dev>, Samuel Colvin <samuel@pydantic.dev>, Hasan Ramezani <hasan@pydantic.dev>, Adrian Garcia Badaracco <adrian@pydantic.dev>, David Montague <david@pydantic.dev>, Marcelo Trylesinski <marcelo@pydantic.dev>, David Hewitt <david.hewitt@pydantic.dev>, Alex Hall <alex@pydantic.dev>
6
6
  License-Expression: MIT
@@ -16,7 +16,7 @@ from logfire.propagate import attach_context as attach_context, get_context as g
16
16
  from logfire.sampling import SamplingOptions as SamplingOptions
17
17
  from typing import Any
18
18
 
19
- __all__ = ['Logfire', 'LogfireSpan', 'LevelName', 'AdvancedOptions', 'ConsoleOptions', 'CodeSource', 'PydanticPlugin', 'configure', 'span', 'instrument', 'log', 'trace', 'debug', 'notice', 'info', 'warn', 'warning', 'error', 'exception', 'fatal', 'force_flush', 'log_slow_async_callbacks', 'install_auto_tracing', 'instrument_asgi', 'instrument_wsgi', 'instrument_pydantic', 'instrument_pydantic_ai', 'instrument_fastapi', 'instrument_openai', 'instrument_openai_agents', 'instrument_anthropic', 'instrument_google_genai', 'instrument_litellm', 'instrument_dspy', 'instrument_print', 'instrument_asyncpg', 'instrument_httpx', 'instrument_celery', 'instrument_requests', 'instrument_psycopg', 'instrument_django', 'instrument_flask', 'instrument_starlette', 'instrument_aiohttp_client', 'instrument_aiohttp_server', 'instrument_sqlalchemy', 'instrument_sqlite3', 'instrument_aws_lambda', 'instrument_redis', 'instrument_pymongo', 'instrument_mysql', 'instrument_surrealdb', 'instrument_system_metrics', 'instrument_mcp', 'instrument_claude_agent_sdk', 'AutoTraceModule', 'with_tags', 'with_settings', 'suppress_scopes', 'shutdown', 'no_auto_trace', 'ScrubMatch', 'ScrubbingOptions', 'VERSION', 'add_non_user_code_prefix', 'suppress_instrumentation', 'StructlogProcessor', 'LogfireLoggingHandler', 'loguru_handler', 'SamplingOptions', 'MetricsOptions', 'VariablesOptions', 'LocalVariablesOptions', 'variables', 'var', 'variables_clear', 'variables_get', 'variables_push', 'variables_push_types', 'variables_validate', 'variables_push_config', 'variables_pull_config', 'variables_build_config', 'logfire_info', 'get_baggage', 'set_baggage', 'get_context', 'attach_context', 'url_from_eval', 'forward_export_request', 'forward_export_request_starlette']
19
+ __all__ = ['Logfire', 'LogfireSpan', 'LevelName', 'AdvancedOptions', 'ConsoleOptions', 'CodeSource', 'PydanticPlugin', 'configure', 'span', 'instrument', 'log', 'trace', 'debug', 'notice', 'info', 'warn', 'warning', 'error', 'exception', 'fatal', 'force_flush', 'log_slow_async_callbacks', 'install_auto_tracing', 'instrument_asgi', 'instrument_wsgi', 'instrument_pydantic', 'instrument_pydantic_ai', 'instrument_fastapi', 'instrument_openai', 'instrument_openai_agents', 'instrument_anthropic', 'instrument_google_genai', 'instrument_litellm', 'instrument_dspy', 'instrument_print', 'instrument_asyncpg', 'instrument_httpx', 'instrument_celery', 'instrument_requests', 'instrument_psycopg', 'instrument_django', 'instrument_flask', 'instrument_starlette', 'instrument_aiohttp_client', 'instrument_aiohttp_server', 'instrument_sqlalchemy', 'instrument_sqlite3', 'instrument_aws_lambda', 'instrument_redis', 'instrument_pymongo', 'instrument_mysql', 'instrument_surrealdb', 'instrument_system_metrics', 'instrument_mcp', 'instrument_claude_agent_sdk', 'AutoTraceModule', 'with_tags', 'with_settings', 'suppress_scopes', 'shutdown', 'no_auto_trace', 'ScrubMatch', 'ScrubbingOptions', 'VERSION', 'add_non_user_code_prefix', 'suppress_instrumentation', 'StructlogProcessor', 'LogfireLoggingHandler', 'loguru_handler', 'SamplingOptions', 'MetricsOptions', 'VariablesOptions', 'LocalVariablesOptions', 'variables', 'var', 'template_var', 'variables_clear', 'variables_get', 'variables_push', 'variables_push_types', 'variables_validate', 'variables_push_config', 'variables_pull_config', 'variables_build_config', 'logfire_info', 'get_baggage', 'set_baggage', 'get_context', 'attach_context', 'url_from_eval', 'forward_export_request', 'forward_export_request_starlette']
20
20
 
21
21
  DEFAULT_LOGFIRE_INSTANCE = Logfire()
22
22
  span = DEFAULT_LOGFIRE_INSTANCE.span
@@ -74,6 +74,7 @@ error = DEFAULT_LOGFIRE_INSTANCE.error
74
74
  fatal = DEFAULT_LOGFIRE_INSTANCE.fatal
75
75
  exception = DEFAULT_LOGFIRE_INSTANCE.exception
76
76
  var = DEFAULT_LOGFIRE_INSTANCE.var
77
+ template_var = DEFAULT_LOGFIRE_INSTANCE.template_var
77
78
  variables_clear = DEFAULT_LOGFIRE_INSTANCE.variables_clear
78
79
  variables_get = DEFAULT_LOGFIRE_INSTANCE.variables_get
79
80
  variables_push = DEFAULT_LOGFIRE_INSTANCE.variables_push
@@ -81,8 +81,8 @@ class PydanticPlugin:
81
81
  This class is deprecated for external use. Use `logfire.instrument_pydantic()` instead.
82
82
  """
83
83
  record: PydanticPluginRecordValues = ...
84
- include: set[str] = field(default_factory=set)
85
- exclude: set[str] = field(default_factory=set)
84
+ include: set[str] = field(default_factory=set[str])
85
+ exclude: set[str] = field(default_factory=set[str])
86
86
 
87
87
  @dataclass
88
88
  class MetricsOptions:
@@ -99,6 +99,8 @@ class CodeSource:
99
99
  revision: str
100
100
  root_path: str = ...
101
101
 
102
+ TemplateMismatchPolicy: Incomplete
103
+
102
104
  @dataclass
103
105
  class VariablesOptions:
104
106
  """Configuration for managed variables using the Logfire remote API.
@@ -112,6 +114,7 @@ class VariablesOptions:
112
114
  include_resource_attributes_in_context: bool = ...
113
115
  include_baggage_in_context: bool = ...
114
116
  instrument: bool = ...
117
+ template_mismatch_policy: TemplateMismatchPolicy = ...
115
118
  def __post_init__(self) -> None: ...
116
119
 
117
120
  @dataclass
@@ -125,6 +128,7 @@ class LocalVariablesOptions:
125
128
  include_resource_attributes_in_context: bool = ...
126
129
  include_baggage_in_context: bool = ...
127
130
  instrument: bool = ...
131
+ template_mismatch_policy: TemplateMismatchPolicy = ...
128
132
 
129
133
  class DeprecatedKwargs(TypedDict): ...
130
134
 
@@ -286,8 +290,8 @@ class LogfireConfig(_LogfireConfigData):
286
290
  This is used internally and should not be called by users of the SDK.
287
291
 
288
292
  If no provider has been explicitly configured (i.e. `variables=` was not passed to
289
- `configure()`), but a `LOGFIRE_API_KEY` is available, a `LogfireRemoteVariableProvider`
290
- will be lazily created on the first call.
293
+ `configure()`), but `configure()` has been called and a `LOGFIRE_API_KEY` is available,
294
+ a `LogfireRemoteVariableProvider` will be lazily created on the first call.
291
295
 
292
296
  Returns:
293
297
  The variable provider.
@@ -11,9 +11,9 @@ from weakref import WeakSet
11
11
  class ProxyLoggerProvider(LoggerProvider):
12
12
  """A logger provider that wraps another internal logger provider allowing it to be re-assigned."""
13
13
  provider: LoggerProvider
14
- loggers: WeakSet[ProxyLogger] = dataclasses.field(default_factory=WeakSet)
14
+ loggers: WeakSet[ProxyLogger] = dataclasses.field(default_factory=WeakSet['ProxyLogger'])
15
15
  lock: Lock = dataclasses.field(default_factory=Lock)
16
- suppressed_scopes: set[str] = dataclasses.field(default_factory=set)
16
+ suppressed_scopes: set[str] = dataclasses.field(default_factory=set[str])
17
17
  min_level: int = ...
18
18
  def get_logger(self, name: str, version: str | None = None, schema_url: str | None = None, attributes: _ExtendedAttributes | None = None) -> Logger: ...
19
19
  def set_min_level(self, min_level: int) -> None: ...
@@ -14,10 +14,10 @@ from ..integrations.psycopg import CommenterOptions as PsycopgCommenterOptions
14
14
  from ..integrations.redis import RequestHook as RedisRequestHook, ResponseHook as RedisResponseHook
15
15
  from ..integrations.sqlalchemy import CommenterOptions as SQLAlchemyCommenterOptions
16
16
  from ..integrations.wsgi import RequestHook as WSGIRequestHook, ResponseHook as WSGIResponseHook
17
- from ..variables import ResolveFunction as ResolveFunction, ValidationReport as ValidationReport, Variable as Variable, VariablesConfig as VariablesConfig
17
+ from ..variables import ResolveFunction as ResolveFunction, TemplateVariable as TemplateVariable, ValidationReport as ValidationReport, Variable as Variable, VariablesConfig as VariablesConfig
18
18
  from ..version import VERSION as VERSION
19
19
  from .auto_trace import AutoTraceModule as AutoTraceModule, install_auto_tracing as install_auto_tracing
20
- from .config import GLOBAL_CONFIG as GLOBAL_CONFIG, LogfireConfig as LogfireConfig
20
+ from .config import GLOBAL_CONFIG as GLOBAL_CONFIG, LogfireConfig as LogfireConfig, TemplateMismatchPolicy as TemplateMismatchPolicy
21
21
  from .config_params import PydanticPluginRecordValues as PydanticPluginRecordValues
22
22
  from .constants import ATTRIBUTES_JSON_SCHEMA_KEY as ATTRIBUTES_JSON_SCHEMA_KEY, ATTRIBUTES_LOG_LEVEL_NUM_KEY as ATTRIBUTES_LOG_LEVEL_NUM_KEY, ATTRIBUTES_MESSAGE_KEY as ATTRIBUTES_MESSAGE_KEY, ATTRIBUTES_MESSAGE_TEMPLATE_KEY as ATTRIBUTES_MESSAGE_TEMPLATE_KEY, ATTRIBUTES_SAMPLE_RATE_KEY as ATTRIBUTES_SAMPLE_RATE_KEY, ATTRIBUTES_SPAN_TYPE_KEY as ATTRIBUTES_SPAN_TYPE_KEY, ATTRIBUTES_TAGS_KEY as ATTRIBUTES_TAGS_KEY, DISABLE_CONSOLE_KEY as DISABLE_CONSOLE_KEY, LEVEL_NUMBERS as LEVEL_NUMBERS, LevelName as LevelName, OTLP_MAX_INT_SIZE as OTLP_MAX_INT_SIZE, log_level_attributes as log_level_attributes
23
23
  from .formatter import logfire_format as logfire_format, logfire_format_with_magic as logfire_format_with_magic
@@ -64,6 +64,7 @@ from wsgiref.types import WSGIApplication
64
64
 
65
65
  ExcInfo = SysExcInfo | BaseException | bool | None
66
66
  T = TypeVar('T')
67
+ InputsT = TypeVar('InputsT')
67
68
 
68
69
  class Logfire:
69
70
  """The main logfire class."""
@@ -1214,16 +1215,21 @@ class Logfire:
1214
1215
  def var(self, name: str, *, default: T, description: str | None = None) -> Variable[T]: ...
1215
1216
  @overload
1216
1217
  def var(self, name: str, *, type: type[T], default: T | ResolveFunction[T], description: str | None = None) -> Variable[T]: ...
1218
+ @overload
1219
+ def template_var(self, name: str, *, default: T, inputs_type: type[InputsT], description: str | None = None, template_mismatch_policy: TemplateMismatchPolicy | None = None) -> TemplateVariable[T, InputsT]: ...
1220
+ @overload
1221
+ def template_var(self, name: str, *, type: type[T], default: T | ResolveFunction[T], inputs_type: type[InputsT], description: str | None = None, template_mismatch_policy: TemplateMismatchPolicy | None = None) -> TemplateVariable[T, InputsT]: ...
1217
1222
  def variables_clear(self) -> None:
1218
1223
  """Clear all registered variables from this Logfire instance.
1219
1224
 
1220
- This removes all variables previously registered via [`var()`][logfire.Logfire.var],
1225
+ This removes all variables previously registered via [`var()`][logfire.Logfire.var]
1226
+ or [`template_var()`][logfire.Logfire.template_var],
1221
1227
  allowing them to be re-registered. This is primarily intended for use in tests
1222
1228
  to ensure a clean state between test cases.
1223
1229
  """
1224
- def variables_get(self) -> list[Variable[Any]]:
1230
+ def variables_get(self) -> list[Variable[Any] | TemplateVariable[Any, Any]]:
1225
1231
  """Get all variables registered with this Logfire instance."""
1226
- def variables_push(self, variables: list[Variable[Any]] | None = None, *, dry_run: bool = False, yes: bool = False, strict: bool = False) -> bool:
1232
+ def variables_push(self, variables: list[Variable[Any] | TemplateVariable[Any, Any]] | None = None, *, dry_run: bool = False, yes: bool = False, strict: bool = False) -> bool:
1227
1233
  """Push variable definitions (metadata only) to the configured variable provider.
1228
1234
 
1229
1235
  This method syncs local variable definitions with the provider:
@@ -1239,7 +1245,8 @@ class Logfire:
1239
1245
  registered with this Logfire instance will be pushed.
1240
1246
  dry_run: If True, only show what would change without applying.
1241
1247
  yes: If True, skip confirmation prompt.
1242
- strict: If True, fail if any existing label values are incompatible with new schemas.
1248
+ strict: If True, fail if any existing label values are incompatible with new schemas
1249
+ or any reference errors are found.
1243
1250
 
1244
1251
  Returns:
1245
1252
  True if changes were applied (or would be applied in dry_run mode), False otherwise.
@@ -1316,7 +1323,7 @@ class Logfire:
1316
1323
  )
1317
1324
  ```
1318
1325
  """
1319
- def variables_validate(self, variables: list[Variable[Any]] | None = None) -> ValidationReport:
1326
+ def variables_validate(self, variables: list[Variable[Any] | TemplateVariable[Any, Any]] | None = None) -> ValidationReport:
1320
1327
  """Validate that provider-side variable label values match local type definitions.
1321
1328
 
1322
1329
  This method fetches the current variable configuration from the provider and
@@ -1350,8 +1357,10 @@ class Logfire:
1350
1357
  def variables_push_config(self, config: VariablesConfig, *, mode: Literal['merge', 'replace'] = 'merge', dry_run: bool = False, yes: bool = False) -> bool:
1351
1358
  '''Push a VariablesConfig to the configured provider.
1352
1359
 
1353
- This method pushes a complete VariablesConfig (including labels and rollouts)
1354
- to the provider. It\'s useful for:
1360
+ This method pushes a VariablesConfig (including labels and rollouts) to the
1361
+ provider. For remote providers, version records are created from label
1362
+ entries with inline serialized values; `latest_version` is pull/read state
1363
+ derived by the server and is not pushed directly. It\'s useful for:
1355
1364
  - Pushing configs generated or modified locally
1356
1365
  - Pushing configs read from files
1357
1366
  - Partial updates (merge mode) or full replacement (replace mode)
@@ -1396,7 +1405,7 @@ class Logfire:
1396
1405
  print(config.model_dump_json(indent=2))
1397
1406
  ```
1398
1407
  '''
1399
- def variables_build_config(self, variables: list[Variable[Any]] | None = None) -> VariablesConfig:
1408
+ def variables_build_config(self, variables: list[Variable[Any] | TemplateVariable[Any, Any]] | None = None) -> VariablesConfig:
1400
1409
  '''Build a VariablesConfig from registered Variable instances.
1401
1410
 
1402
1411
  This creates a minimal config with just the name, schema, and example for each variable.
@@ -11,9 +11,9 @@ from weakref import WeakSet
11
11
  @dataclasses.dataclass
12
12
  class ProxyMeterProvider(MeterProvider):
13
13
  provider: MeterProvider
14
- meters: WeakSet[_ProxyMeter] = dataclasses.field(default_factory=WeakSet)
14
+ meters: WeakSet[_ProxyMeter] = dataclasses.field(default_factory=WeakSet['_ProxyMeter'])
15
15
  lock: Lock = dataclasses.field(default_factory=Lock)
16
- suppressed_scopes: set[str] = dataclasses.field(default_factory=set)
16
+ suppressed_scopes: set[str] = dataclasses.field(default_factory=set[str])
17
17
  def get_meter(self, name: str, version: str | None = None, schema_url: str | None = None, attributes: Attributes | None = None) -> Meter: ...
18
18
  def suppress_scopes(self, *scopes: str) -> None: ...
19
19
  def set_meter_provider(self, meter_provider: MeterProvider) -> None: ...
@@ -25,9 +25,9 @@ class ProxyTracerProvider(TracerProvider):
25
25
  """A tracer provider that wraps another internal tracer provider allowing it to be re-assigned."""
26
26
  provider: TracerProvider
27
27
  config: LogfireConfig
28
- tracers: WeakKeyDictionary[_ProxyTracer, Callable[[], Tracer]] = field(default_factory=WeakKeyDictionary)
28
+ tracers: WeakKeyDictionary[_ProxyTracer, Callable[[], Tracer]] = field(default_factory=WeakKeyDictionary['_ProxyTracer', Callable[[], Tracer]])
29
29
  lock: Lock = field(default_factory=Lock)
30
- suppressed_scopes: set[str] = field(default_factory=set)
30
+ suppressed_scopes: set[str] = field(default_factory=set[str])
31
31
  def set_provider(self, provider: SDKTracerProvider) -> None: ...
32
32
  def suppress_scopes(self, *scopes: str) -> None: ...
33
33
  def get_tracer(self, instrumenting_module_name: str, *args: Any, is_span_tracer: bool = True, **kwargs: Any) -> _ProxyTracer: ...
@@ -0,0 +1,6 @@
1
+ from logfire.variables.abstract import ResolutionReason as ResolutionReason, ResolvedVariable as ResolvedVariable, SyncMode as SyncMode, ValidationReport as ValidationReport, VariableAlreadyExistsError as VariableAlreadyExistsError, VariableNotFoundError as VariableNotFoundError, VariableWriteError as VariableWriteError
2
+ from logfire.variables.composition import ComposedReference as ComposedReference, VariableCompositionCycleError as VariableCompositionCycleError, VariableCompositionError as VariableCompositionError
3
+ from logfire.variables.config import KeyIsNotPresent as KeyIsNotPresent, KeyIsPresent as KeyIsPresent, LabelRef as LabelRef, LabeledValue as LabeledValue, LatestVersion as LatestVersion, LocalVariablesOptions as LocalVariablesOptions, Rollout as Rollout, RolloutOverride as RolloutOverride, TemplateMismatchPolicy as TemplateMismatchPolicy, ValueDoesNotEqual as ValueDoesNotEqual, ValueDoesNotMatchRegex as ValueDoesNotMatchRegex, ValueEquals as ValueEquals, ValueIsIn as ValueIsIn, ValueIsNotIn as ValueIsNotIn, ValueMatchesRegex as ValueMatchesRegex, VariableConfig as VariableConfig, VariableTypeConfig as VariableTypeConfig, VariablesConfig as VariablesConfig
4
+ from logfire.variables.variable import ResolveFunction as ResolveFunction, TemplateInputsMismatchError as TemplateInputsMismatchError, TemplateVariable as TemplateVariable, Variable as Variable, targeting_context as targeting_context
5
+
6
+ __all__ = ['Variable', 'TemplateVariable', 'ResolvedVariable', 'ResolveFunction', 'VariablesConfig', 'VariableConfig', 'VariableTypeConfig', 'LocalVariablesOptions', 'LabeledValue', 'LabelRef', 'LatestVersion', 'Rollout', 'RolloutOverride', 'KeyIsPresent', 'KeyIsNotPresent', 'ValueEquals', 'ValueDoesNotEqual', 'ValueIsIn', 'ValueIsNotIn', 'ValueMatchesRegex', 'ValueDoesNotMatchRegex', 'targeting_context', 'ComposedReference', 'ResolutionReason', 'SyncMode', 'TemplateMismatchPolicy', 'ValidationReport', 'TemplateInputsMismatchError', 'VariableAlreadyExistsError', 'VariableCompositionCycleError', 'VariableCompositionError', 'VariableNotFoundError', 'VariableWriteError']
@@ -0,0 +1,52 @@
1
+ from functools import cache
2
+ from pydantic_handlebars import CompiledTemplate, HandlebarsEnvironment
3
+
4
+ COMPOSITION_OPEN_DELIM: str
5
+ COMPOSITION_CLOSE_DELIM: str
6
+
7
+ @cache
8
+ def get_environment(strict: bool = False) -> HandlebarsEnvironment:
9
+ """Return a cached `HandlebarsEnvironment` configured for `@{...}@` composition.
10
+
11
+ Uses non-default delimiters so the composition pass leaves any
12
+ `{{...}}` runtime placeholders in the template as plain content; a
13
+ subsequent render pass with the default delimiters consumes those.
14
+
15
+ When *strict* is `True`, the environment raises `HandlebarsRuntimeError`
16
+ for any `@{ref}@` (or dotted `@{ref.field}@`) that doesn't resolve, rather
17
+ than rendering it as an empty string. Composition uses the strict
18
+ environment for provider/override values (so a missing reference triggers a
19
+ fall back to the code default) and the non-strict one for the code default
20
+ itself (the lenient last resort, where a missing reference renders empty).
21
+ """
22
+ @cache
23
+ def get_runtime_environment() -> HandlebarsEnvironment:
24
+ """Return a cached default-delimiter `HandlebarsEnvironment` for `{{...}}` rendering.
25
+
26
+ Used by `TemplateVariable.get(inputs)` to render the post-composition
27
+ serialized value against the provided inputs.
28
+ """
29
+ def compile_composition_template(source: str, strict: bool = False) -> CompiledTemplate:
30
+ """Return a cached `CompiledTemplate` for *source* under composition delimiters.
31
+
32
+ Managed-variable values are typically stable across many resolutions, so
33
+ caching the parsed program lets `Variable._resolve` skip the parse on
34
+ every `get()` call. 1024 is large enough for any realistic number of
35
+ distinct templates in a single process while staying bounded for
36
+ long-running workers. Same rationale for `compile_runtime_template`.
37
+
38
+ *strict* selects the strict vs non-strict composition environment (see
39
+ `get_environment`); the two are cached separately.
40
+ """
41
+ def compile_runtime_template(source: str) -> CompiledTemplate:
42
+ """Return a cached `CompiledTemplate` for *source* under default `{{...}}` delimiters."""
43
+ def extract_composition_dependencies(template: str) -> frozenset[str]:
44
+ """Return the top-level `@{name}@` references in *template*.
45
+
46
+ Cached because cycle / reference validation runs over the same template
47
+ strings multiple times per push or sync. The underlying delegation goes
48
+ to `pydantic_handlebars.extract_dependencies` configured for the
49
+ composition delimiters, so block helpers, dotted paths, and helper
50
+ sub-expressions are handled AST-correctly. A `frozenset` is returned so the
51
+ cached value can't be mutated by a caller and poison later lookups.
52
+ """
@@ -2,12 +2,14 @@ import logfire
2
2
  from _typeshed import Incomplete
3
3
  from abc import ABC, abstractmethod
4
4
  from collections.abc import Mapping, Sequence
5
- from dataclasses import dataclass
5
+ from dataclasses import dataclass, field
6
+ from logfire.variables.composition import ComposedReference
6
7
  from logfire.variables.config import VariableConfig, VariableTypeConfig, VariablesConfig
8
+ from logfire.variables.template_validation import TemplateFieldIssue
7
9
  from logfire.variables.variable import Variable
8
10
  from typing import Any, Generic, TypeVar
9
11
 
10
- __all__ = ['ResolvedVariable', 'ResolutionReason', 'SyncMode', 'ValidationReport', 'VariableProvider', 'NoOpVariableProvider', 'VariableWriteError', 'VariableNotFoundError', 'VariableAlreadyExistsError']
12
+ __all__ = ['ResolvedVariable', 'ResolutionReason', 'SyncMode', 'ValidationReport', 'VariableProvider', 'NoOpVariableProvider', 'VariableWriteError', 'VariableNotFoundError', 'VariableAlreadyExistsError', 'render_serialized_string']
11
13
 
12
14
  SyncMode: Incomplete
13
15
  T = TypeVar('T')
@@ -46,11 +48,27 @@ class ResolvedVariable(Generic[T_co]):
46
48
  label: str | None = ...
47
49
  version: int | None = ...
48
50
  exception: Exception | None = ...
51
+ composed_from: list[ComposedReference] = field(default_factory=list['ComposedReference'])
49
52
  reason: ResolutionReason
50
53
  def __post_init__(self) -> None: ...
51
54
  def __enter__(self): ...
52
55
  def __exit__(self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: Any) -> None: ...
53
56
 
57
+ def render_serialized_string(serialized_json: str, inputs: Any) -> str:
58
+ """Render Handlebars templates in a serialized JSON string.
59
+
60
+ Decodes the JSON, renders all string values containing `{{placeholders}}`
61
+ using the provided inputs, then re-encodes to JSON.
62
+
63
+ Args:
64
+ serialized_json: A JSON-encoded string potentially containing Handlebars templates.
65
+ inputs: Template context values. Can be a Pydantic `BaseModel`, `dict`,
66
+ `Mapping`, or `None`.
67
+
68
+ Returns:
69
+ The rendered JSON string.
70
+ """
71
+
54
72
  @dataclass
55
73
  class LabelCompatibility:
56
74
  """Result of checking a label value's compatibility with a schema."""
@@ -72,12 +90,18 @@ class VariableChange:
72
90
  local_description: str | None = ...
73
91
  server_description: str | None = ...
74
92
  description_differs: bool = ...
93
+ template_inputs_schema: dict[str, Any] | None = ...
94
+ value_schema_changed: bool = ...
95
+ inputs_schema_changed: bool = ...
75
96
 
76
97
  @dataclass
77
98
  class VariableDiff:
78
99
  """Represents the diff between local and server variables."""
79
100
  changes: list[VariableChange]
80
101
  orphaned_server_variables: list[str]
102
+ reference_errors: list[str] = field(default_factory=list[str])
103
+ reference_cycles: list[str] = field(default_factory=list[str])
104
+ template_field_issues: list[TemplateFieldIssue] = field(default_factory=list)
81
105
  @property
82
106
  def has_changes(self) -> bool:
83
107
  """Return True if there are any changes to apply."""
@@ -116,12 +140,15 @@ class ValidationReport:
116
140
  variables_checked: int
117
141
  variables_not_on_server: list[str]
118
142
  description_differences: list[DescriptionDifference]
143
+ reference_errors: list[str] = field(default_factory=list[str])
144
+ reference_cycles: list[str] = field(default_factory=list[str])
145
+ template_field_issues: list[TemplateFieldIssue] = field(default_factory=list)
119
146
  @property
120
147
  def has_errors(self) -> bool:
121
148
  """Return True if there are any validation errors."""
122
149
  @property
123
150
  def is_valid(self) -> bool:
124
- """Return False if there are any validation errors or any variables not defined in the (possibly remote) config."""
151
+ """Return False if there are validation errors, missing variables, or reference errors."""
125
152
  def format(self, *, colors: bool = True) -> str:
126
153
  """Format the validation report for human-readable output.
127
154
 
@@ -276,8 +303,10 @@ class VariableProvider(ABC):
276
303
  def push_config(self, config: VariablesConfig, *, mode: SyncMode = 'merge', dry_run: bool = False, yes: bool = False) -> bool:
277
304
  """Push a VariablesConfig to this provider.
278
305
 
279
- This method pushes a complete VariablesConfig (including labels and rollouts)
280
- to the provider. It's useful for:
306
+ This method pushes a VariablesConfig (including labels and rollouts) to the
307
+ provider. For remote providers, version records are created from label
308
+ entries with inline serialized values; `latest_version` is pull/read state
309
+ derived by the server and is not pushed directly. It's useful for:
281
310
  - Pushing configs generated or modified locally
282
311
  - Pushing configs read from files
283
312
  - Partial updates (merge mode) or full replacement (replace mode)
@@ -313,7 +342,8 @@ class VariableProvider(ABC):
313
342
  variables: Variable instances to push.
314
343
  dry_run: If True, only show what would change without applying.
315
344
  yes: If True, skip confirmation prompt.
316
- strict: If True, fail if any existing label values are incompatible with new schemas.
345
+ strict: If True, fail if any existing label values are incompatible with new schemas
346
+ or any reference errors are found.
317
347
 
318
348
  Returns:
319
349
  True if changes were applied (or would be applied in dry_run mode), False otherwise.
@@ -0,0 +1,102 @@
1
+ from collections.abc import Callable
2
+ from dataclasses import dataclass, field
3
+ from logfire.variables.abstract import ResolutionReason
4
+
5
+ __all__ = ['MAX_COMPOSITION_DEPTH', 'VariableCompositionError', 'VariableCompositionCycleError', 'ComposedReference', 'expand_references', 'find_references', 'find_references_and_errors', 'has_references']
6
+
7
+ MAX_COMPOSITION_DEPTH: int
8
+
9
+ class VariableCompositionError(Exception):
10
+ """Error during variable composition (reference expansion)."""
11
+ class VariableCompositionCycleError(VariableCompositionError):
12
+ """Circular reference detected during variable composition."""
13
+
14
+ @dataclass
15
+ class ComposedReference:
16
+ """Metadata about a single `@{reference}@` that was encountered during expansion.
17
+
18
+ This is a lightweight dataclass used to track composition results without
19
+ depending on ResolvedVariable, making it reusable from both the SDK and backend.
20
+ """
21
+ name: str
22
+ value: str | None
23
+ label: str | None
24
+ version: int | None
25
+ reason: ResolutionReason
26
+ error: str | None = ...
27
+ composed_from: list[ComposedReference] = field(default_factory=list['ComposedReference'])
28
+ fatal: bool = ...
29
+ ResolveFn = Callable[[str], tuple[str | None, str | None, int | None, ResolutionReason]]
30
+
31
+ def has_references(serialized_value: str) -> bool:
32
+ """Quick check for any `@{` in a serialized value.
33
+
34
+ Returns `True` whenever the string contains the composition open
35
+ delimiter, regardless of preceding backslashes. The actual
36
+ escape-or-real decision is made by `pydantic_handlebars` at render time
37
+ — distinguishing an escaped `\\@{x}@` from an unescaped `@{x}@` here
38
+ would require a variable-width lookbehind (the renderer counts
39
+ backslash parity to match Handlebars.js semantics) and is unnecessary:
40
+ `extract_composition_dependencies` returns an empty set for escaped-only
41
+ strings, and the renderer correctly leaves the literal text in place.
42
+ """
43
+ def expand_references(serialized_value: str, variable_name: str, resolve_fn: ResolveFn, *, strict: bool = False, _visited: tuple[str, ...] = (), _depth: int = 0) -> tuple[str, list[ComposedReference]]:
44
+ """Expand `@{var}@` references in a serialized variable value.
45
+
46
+ Uses the Handlebars engine so `@{}@` supports the full Handlebars
47
+ syntax — simple references, dotted field reads, block helpers (including
48
+ with dotted or sub-expression headers like `@{#if user.active}@`), and
49
+ helper sub-expressions — while preserving `{{runtime}}` placeholders
50
+ untouched.
51
+
52
+ Args:
53
+ serialized_value: The raw JSON-serialized variable value.
54
+ variable_name: Name of the variable being expanded (for cycle detection).
55
+ resolve_fn: Function that resolves a variable name to
56
+ (serialized_value, label, version, reason).
57
+ strict: When `True`, an unresolved `@{ref}@` / `@{ref.field}@` raises
58
+ `HandlebarsRuntimeError` instead of rendering as an empty string.
59
+ The SDK composes provider/override values strictly (so a missing
60
+ reference falls back to the code default) and the code default
61
+ non-strictly (the lenient last resort). Nested expansions inherit
62
+ this flag.
63
+ _visited: Internal - ordered variable names in the current expansion chain.
64
+ _depth: Internal - current recursion depth.
65
+
66
+ Returns:
67
+ tuple of (expanded_serialized_value, list_of_composed_references).
68
+
69
+ Raises:
70
+ VariableCompositionError: If max depth is exceeded.
71
+ VariableCompositionCycleError: If a circular reference is detected.
72
+ HandlebarsRuntimeError: Under *strict*, if a reference is unresolved.
73
+ """
74
+ def find_references(serialized_value: str) -> list[str]:
75
+ """Find all top-level `@{variable_name}@` references in a serialized value.
76
+
77
+ Walks the decoded JSON value and runs each string containing composition
78
+ syntax through `pydantic_handlebars.extract_dependencies`, so block
79
+ helpers (`@{#if var}@`), dotted paths (`@{var.field}@`), and
80
+ subexpressions (`@{lookup obj key}@`) are all picked up correctly. A string
81
+ whose `@{...}@` syntax can't be parsed is skipped (contributes no
82
+ references) so this never raises; use `find_references_and_errors` to also
83
+ surface those parse failures.
84
+
85
+ Args:
86
+ serialized_value: The raw JSON-serialized variable value to scan.
87
+
88
+ Returns:
89
+ Sorted (alphabetical) list of unique top-level variable names referenced.
90
+ """
91
+ def find_references_and_errors(serialized_value: str) -> tuple[list[str], list[str]]:
92
+ """Find references AND parse-error messages in a serialized value.
93
+
94
+ Like `find_references`, but also returns a message for every string whose
95
+ `@{...}@` syntax can't be parsed (malformed template, reserved name). Used by
96
+ push / validate so a malformed value is surfaced as a loud error rather than
97
+ silently skipped the way `find_references` does (it skips them so resolution
98
+ can degrade gracefully).
99
+
100
+ Returns:
101
+ ``(sorted unique reference names, parse-error messages)``.
102
+ """
@@ -1,12 +1,12 @@
1
1
  import re
2
2
  from collections.abc import Mapping, Sequence
3
- from logfire._internal.config import LocalVariablesOptions as LocalVariablesOptions
3
+ from logfire._internal.config import LocalVariablesOptions as LocalVariablesOptions, TemplateMismatchPolicy as TemplateMismatchPolicy
4
4
  from logfire.variables.abstract import ResolvedVariable
5
5
  from logfire.variables.variable import Variable
6
6
  from pydantic import BaseModel
7
7
  from typing import Any, Literal
8
8
 
9
- __all__ = ['KeyIsNotPresent', 'KeyIsPresent', 'LabeledValue', 'LabelRef', 'LatestVersion', 'LocalVariablesOptions', 'Rollout', 'RolloutOverride', 'ValueDoesNotEqual', 'ValueDoesNotMatchRegex', 'ValueEquals', 'ValueIsIn', 'ValueIsNotIn', 'ValueMatchesRegex', 'VariableConfig', 'VariablesConfig', 'VariableTypeConfig']
9
+ __all__ = ['KeyIsNotPresent', 'KeyIsPresent', 'LabeledValue', 'LabelRef', 'LatestVersion', 'LocalVariablesOptions', 'Rollout', 'RolloutOverride', 'TemplateMismatchPolicy', 'ValueDoesNotEqual', 'ValueDoesNotMatchRegex', 'ValueEquals', 'ValueIsIn', 'ValueIsNotIn', 'ValueMatchesRegex', 'VariableConfig', 'VariablesConfig', 'VariableTypeConfig']
10
10
 
11
11
  class ValueEquals(BaseModel):
12
12
  """Condition that matches when an attribute equals a specific value."""
@@ -76,7 +76,13 @@ class LabeledValue(BaseModel):
76
76
  serialized_value: str
77
77
 
78
78
  class LabelRef(BaseModel):
79
- """A label pointing to a version via a reference to another label, 'latest', or 'code_default'."""
79
+ """A label pointing to a version via a reference to another label, 'latest', or 'code_default'.
80
+
81
+ Note: `'latest'` and `'code_default'` are *reserved label names*. The platform rejects
82
+ user attempts to create labels with these names, so anywhere the SDK treats them as
83
+ special — `follow_ref` here, and the push-time validation that keys values by label
84
+ name — it can rely on them being unambiguous (no user-defined label can collide).
85
+ """
80
86
  version: int | None
81
87
  ref: str
82
88
 
@@ -117,6 +123,7 @@ class VariableConfig(BaseModel):
117
123
  type_name: str | None
118
124
  aliases: list[VariableName] | None
119
125
  example: str | None
126
+ template_inputs_schema: dict[str, Any] | None
120
127
  def resolve_label(self, targeting_key: str | None = None, attributes: Mapping[str, Any] | None = None) -> str | None:
121
128
  """Evaluate rollout rules and return the selected label name.
122
129
 
@@ -182,7 +189,7 @@ class VariablesConfig(BaseModel):
182
189
  Returns:
183
190
  A ResolvedVariable containing the serialized value (or None if not found).
184
191
  """
185
- def get_validation_errors(self, variables: list[Variable[Any]]) -> dict[str, dict[str | None, Exception]]:
192
+ def get_validation_errors(self, variables: Sequence[Variable[Any]]) -> dict[str, dict[str | None, Exception]]:
186
193
  """Validate that all variable label values can be deserialized to their expected types.
187
194
 
188
195
  Args:
@@ -192,7 +199,7 @@ class VariablesConfig(BaseModel):
192
199
  A dict mapping variable names to dicts of label names (or None for general errors) to exceptions.
193
200
  """
194
201
  @staticmethod
195
- def from_variables(variables: list[Variable[Any]]) -> VariablesConfig:
202
+ def from_variables(variables: Sequence[Variable[Any]]) -> VariablesConfig:
196
203
  """Create a VariablesConfig from a list of Variable instances.
197
204
 
198
205
  This creates a minimal config with just the name, schema, and example for each variable.
@@ -0,0 +1,53 @@
1
+ from collections.abc import Callable
2
+ from dataclasses import dataclass, field
3
+ from typing import Any
4
+
5
+ __all__ = ['TemplateFieldIssue', 'TemplateValidationResult', 'validate_template_composition', 'detect_composition_cycles', 'extract_template_strings']
6
+
7
+ @dataclass
8
+ class TemplateFieldIssue:
9
+ """A `{{field}}` reference that doesn't match a template variable's `template_inputs_schema`."""
10
+ field_name: str
11
+ found_in_variable: str
12
+ found_in_label: str | None
13
+ reference_path: list[str]
14
+ root_variable: str
15
+
16
+ @dataclass
17
+ class TemplateValidationResult:
18
+ """Result of template composition validation."""
19
+ issues: list[TemplateFieldIssue] = field(default_factory=list[TemplateFieldIssue])
20
+
21
+ def extract_template_strings(serialized_json: str) -> list[str]:
22
+ """Extract all string values from serialized JSON that contain `{{...}}` templates."""
23
+ def validate_template_composition(variable_name: str, template_inputs_schema: dict[str, Any], get_all_serialized_values: Callable[[str], dict[str | None, str]]) -> TemplateValidationResult:
24
+ """Validate that `{{field}}` references in a template variable match its schema.
25
+
26
+ Walks the composition graph starting from *variable_name*, collecting all
27
+ template strings from the variable's values and its `@{ref}@` dependencies,
28
+ then uses AST-based schema checking via `check_template_compatibility` to
29
+ find incompatible field references.
30
+
31
+ Args:
32
+ variable_name: Name of the template variable to validate.
33
+ template_inputs_schema: JSON Schema describing the expected template inputs.
34
+ get_all_serialized_values: Function that returns `{label_or_none: serialized_json}`
35
+ for any variable name. Each key is the label that serves that value; the
36
+ `None` key is the code default, and `'latest'` is the latest version.
37
+
38
+ Returns:
39
+ A :class:`TemplateValidationResult` with any issues found.
40
+ """
41
+ def detect_composition_cycles(variable_name: str, new_references: set[str], get_all_references: Callable[[str], set[str]]) -> list[str] | None:
42
+ """Check if adding *new_references* to *variable_name* would create a cycle.
43
+
44
+ Args:
45
+ variable_name: The variable being updated.
46
+ new_references: Set of variable names directly referenced by the new value.
47
+ get_all_references: Function that returns all variable names referenced by
48
+ any value of the given variable name.
49
+
50
+ Returns:
51
+ The cycle path (e.g., `['A', 'B', 'C', 'A']`) if a cycle is detected,
52
+ or `None` if no cycle exists.
53
+ """