trodo-python 2.23.1__tar.gz → 2.23.3__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.
- {trodo_python-2.23.1 → trodo_python-2.23.3}/PKG-INFO +1 -1
- {trodo_python-2.23.1 → trodo_python-2.23.3}/pyproject.toml +1 -1
- trodo_python-2.23.3/tests/test_framework_deference.py +41 -0
- {trodo_python-2.23.1 → trodo_python-2.23.3}/tests/test_prompt_trace.py +77 -0
- {trodo_python-2.23.1 → trodo_python-2.23.3}/trodo/otel/auto_instrument.py +45 -0
- {trodo_python-2.23.1 → trodo_python-2.23.3}/trodo/otel/prompt_trace.py +25 -3
- {trodo_python-2.23.1 → trodo_python-2.23.3}/trodo/otel/wrap_agent.py +4 -2
- {trodo_python-2.23.1 → trodo_python-2.23.3}/trodo_python.egg-info/PKG-INFO +1 -1
- {trodo_python-2.23.1 → trodo_python-2.23.3}/trodo_python.egg-info/SOURCES.txt +1 -0
- {trodo_python-2.23.1 → trodo_python-2.23.3}/README.md +0 -0
- {trodo_python-2.23.1 → trodo_python-2.23.3}/setup.cfg +0 -0
- {trodo_python-2.23.1 → trodo_python-2.23.3}/tests/test_anon_distinct_id.py +0 -0
- {trodo_python-2.23.1 → trodo_python-2.23.3}/tests/test_auto_instrument_fixes.py +0 -0
- {trodo_python-2.23.1 → trodo_python-2.23.3}/tests/test_cross_process_session.py +0 -0
- {trodo_python-2.23.1 → trodo_python-2.23.3}/tests/test_datasets.py +0 -0
- {trodo_python-2.23.1 → trodo_python-2.23.3}/tests/test_end_run.py +0 -0
- {trodo_python-2.23.1 → trodo_python-2.23.3}/tests/test_error_enrichment.py +0 -0
- {trodo_python-2.23.1 → trodo_python-2.23.3}/tests/test_experiments.py +0 -0
- {trodo_python-2.23.1 → trodo_python-2.23.3}/tests/test_get_prompt_version_types.py +0 -0
- {trodo_python-2.23.1 → trodo_python-2.23.3}/tests/test_instrumentor_resolution.py +0 -0
- {trodo_python-2.23.1 → trodo_python-2.23.3}/tests/test_llm_usage_cost.py +0 -0
- {trodo_python-2.23.1 → trodo_python-2.23.3}/tests/test_multi_agent_shape.py +0 -0
- {trodo_python-2.23.1 → trodo_python-2.23.3}/tests/test_processor_methods.py +0 -0
- {trodo_python-2.23.1 → trodo_python-2.23.3}/tests/test_prompts.py +0 -0
- {trodo_python-2.23.1 → trodo_python-2.23.3}/tests/test_register_otel.py +0 -0
- {trodo_python-2.23.1 → trodo_python-2.23.3}/tests/test_start_run.py +0 -0
- {trodo_python-2.23.1 → trodo_python-2.23.3}/tests/test_user_traits.py +0 -0
- {trodo_python-2.23.1 → trodo_python-2.23.3}/tests/test_wrap_agent_unchanged.py +0 -0
- {trodo_python-2.23.1 → trodo_python-2.23.3}/trodo/__init__.py +0 -0
- {trodo_python-2.23.1 → trodo_python-2.23.3}/trodo/api/__init__.py +0 -0
- {trodo_python-2.23.1 → trodo_python-2.23.3}/trodo/api/async_client.py +0 -0
- {trodo_python-2.23.1 → trodo_python-2.23.3}/trodo/api/endpoints.py +0 -0
- {trodo_python-2.23.1 → trodo_python-2.23.3}/trodo/api/http_client.py +0 -0
- {trodo_python-2.23.1 → trodo_python-2.23.3}/trodo/auto/__init__.py +0 -0
- {trodo_python-2.23.1 → trodo_python-2.23.3}/trodo/auto/auto_event_manager.py +0 -0
- {trodo_python-2.23.1 → trodo_python-2.23.3}/trodo/client.py +0 -0
- {trodo_python-2.23.1 → trodo_python-2.23.3}/trodo/managers/__init__.py +0 -0
- {trodo_python-2.23.1 → trodo_python-2.23.3}/trodo/managers/dataset_manager.py +0 -0
- {trodo_python-2.23.1 → trodo_python-2.23.3}/trodo/managers/experiment_manager.py +0 -0
- {trodo_python-2.23.1 → trodo_python-2.23.3}/trodo/managers/group_manager.py +0 -0
- {trodo_python-2.23.1 → trodo_python-2.23.3}/trodo/managers/people_manager.py +0 -0
- {trodo_python-2.23.1 → trodo_python-2.23.3}/trodo/managers/prompt_manager.py +0 -0
- {trodo_python-2.23.1 → trodo_python-2.23.3}/trodo/managers/user_manager.py +0 -0
- {trodo_python-2.23.1 → trodo_python-2.23.3}/trodo/otel/__init__.py +0 -0
- {trodo_python-2.23.1 → trodo_python-2.23.3}/trodo/otel/context.py +0 -0
- {trodo_python-2.23.1 → trodo_python-2.23.3}/trodo/otel/helpers.py +0 -0
- {trodo_python-2.23.1 → trodo_python-2.23.3}/trodo/otel/processor.py +0 -0
- {trodo_python-2.23.1 → trodo_python-2.23.3}/trodo/otel/register.py +0 -0
- {trodo_python-2.23.1 → trodo_python-2.23.3}/trodo/otel/transport.py +0 -0
- {trodo_python-2.23.1 → trodo_python-2.23.3}/trodo/prompts/__init__.py +0 -0
- {trodo_python-2.23.1 → trodo_python-2.23.3}/trodo/prompts/compile.py +0 -0
- {trodo_python-2.23.1 → trodo_python-2.23.3}/trodo/prompts/template.py +0 -0
- {trodo_python-2.23.1 → trodo_python-2.23.3}/trodo/prompts/types.py +0 -0
- {trodo_python-2.23.1 → trodo_python-2.23.3}/trodo/queue/__init__.py +0 -0
- {trodo_python-2.23.1 → trodo_python-2.23.3}/trodo/queue/batch_flusher.py +0 -0
- {trodo_python-2.23.1 → trodo_python-2.23.3}/trodo/queue/event_queue.py +0 -0
- {trodo_python-2.23.1 → trodo_python-2.23.3}/trodo/session/__init__.py +0 -0
- {trodo_python-2.23.1 → trodo_python-2.23.3}/trodo/session/server_session.py +0 -0
- {trodo_python-2.23.1 → trodo_python-2.23.3}/trodo/session/session_manager.py +0 -0
- {trodo_python-2.23.1 → trodo_python-2.23.3}/trodo/types.py +0 -0
- {trodo_python-2.23.1 → trodo_python-2.23.3}/trodo/user_context.py +0 -0
- {trodo_python-2.23.1 → trodo_python-2.23.3}/trodo/util/__init__.py +0 -0
- {trodo_python-2.23.1 → trodo_python-2.23.3}/trodo/util/lru.py +0 -0
- {trodo_python-2.23.1 → trodo_python-2.23.3}/trodo_python.egg-info/dependency_links.txt +0 -0
- {trodo_python-2.23.1 → trodo_python-2.23.3}/trodo_python.egg-info/requires.txt +0 -0
- {trodo_python-2.23.1 → trodo_python-2.23.3}/trodo_python.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""One model call must produce one llm span.
|
|
2
|
+
|
|
3
|
+
langchain-openai depends on the `openai` package, so a chain invocation IS a
|
|
4
|
+
provider client call. With both instrumentors registered that single call emits
|
|
5
|
+
two sibling llm spans carrying identical tokens, and the run's totals double.
|
|
6
|
+
Measured on the Node side against real traffic: a 45/2 call reported 90/4. Cost
|
|
7
|
+
escaped doubling only by luck -- the framework's duplicate reported
|
|
8
|
+
model=unknown and so could not be priced at all.
|
|
9
|
+
|
|
10
|
+
The framework instrumentor therefore defers to the provider one, which knows the
|
|
11
|
+
real model id and can be priced.
|
|
12
|
+
"""
|
|
13
|
+
from trodo.otel.auto_instrument import _superseding_providers
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_langchain_defers_to_openai():
|
|
17
|
+
assert "openai" in _superseding_providers("langchain", ["anthropic", "openai"])
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_langchain_defers_to_anthropic():
|
|
21
|
+
assert "anthropic" in _superseding_providers("langchain", ["anthropic"])
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_langchain_registers_when_no_provider_it_routes_through_is_active():
|
|
25
|
+
# LangChain over a provider we do not instrument: its span is then the ONLY
|
|
26
|
+
# capture, and suppressing it would lose the call entirely.
|
|
27
|
+
assert _superseding_providers("langchain", ["google_generativeai"]) == []
|
|
28
|
+
assert _superseding_providers("langchain", []) == []
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_providers_never_defer_to_each_other():
|
|
32
|
+
# Two providers are two different clients; there is nothing to double.
|
|
33
|
+
assert _superseding_providers("openai", ["anthropic"]) == []
|
|
34
|
+
assert _superseding_providers("anthropic", ["openai"]) == []
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_only_lists_providers_registered_before_the_framework():
|
|
38
|
+
# The check reads `active`, built in registration order (anthropic, openai,
|
|
39
|
+
# openai_v2, langchain, ...). Listing bedrock would read right and never fire.
|
|
40
|
+
got = _superseding_providers("langchain", ["openai", "anthropic", "bedrock", "cohere"])
|
|
41
|
+
assert "bedrock" not in got and "cohere" not in got
|
|
@@ -259,3 +259,80 @@ def test_run_set_input_with_ordinary_input_records_nothing(processor, http):
|
|
|
259
259
|
|
|
260
260
|
attrs = http.run_ingest[0]["run"].get("attributes") or {}
|
|
261
261
|
assert "trodo.prompts" not in attrs
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
# ── Prompts are SPAN-level, not run-level ───────────────────────────────────
|
|
265
|
+
#
|
|
266
|
+
# One run's spans routinely use different prompts, different versions, or none.
|
|
267
|
+
# The run's "most recently compiled" prompt is a fallback, and applying it to
|
|
268
|
+
# every span meant compiling anywhere in a run attributed that prompt to every
|
|
269
|
+
# LATER span. Caught in production: a span named 'no-prompt' was labelled with
|
|
270
|
+
# whichever prompt had been compiled last.
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def test_does_not_leak_last_prompt_onto_a_later_unrelated_span(processor, http):
|
|
274
|
+
mgr = PromptManager(PromptHttp(prompt=PROMPT_WIRE))
|
|
275
|
+
with wrap_agent(processor=processor, team_site_id="site-x", agent_name="chat"):
|
|
276
|
+
p = mgr.get("refund-agent", cache_ttl_seconds=0)
|
|
277
|
+
with span("used-it", kind="llm"):
|
|
278
|
+
p.compile(q="x")
|
|
279
|
+
with span("unrelated"):
|
|
280
|
+
pass
|
|
281
|
+
|
|
282
|
+
spans = http.run_ingest[0]["spans"]
|
|
283
|
+
attrs = next(s for s in spans if s["name"] == "unrelated").get("attributes") or {}
|
|
284
|
+
assert "trodo.prompt.name" not in attrs
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def test_still_labels_a_model_span_following_a_compile(processor, http):
|
|
288
|
+
mgr = PromptManager(PromptHttp(prompt=PROMPT_WIRE))
|
|
289
|
+
with wrap_agent(processor=processor, team_site_id="site-x", agent_name="chat"):
|
|
290
|
+
p = mgr.get("refund-agent", cache_ttl_seconds=0)
|
|
291
|
+
p.compile(q="x")
|
|
292
|
+
with span("model-call", kind="llm"):
|
|
293
|
+
pass
|
|
294
|
+
|
|
295
|
+
spans = http.run_ingest[0]["spans"]
|
|
296
|
+
attrs = next(s for s in spans if s["name"] == "model-call")["attributes"]
|
|
297
|
+
assert attrs["trodo.prompt.name"] == "refund-agent"
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def test_labels_the_span_the_prompt_was_compiled_inside(processor, http):
|
|
301
|
+
"""A plain span wrapping a hand-rolled model call plainly used the prompt
|
|
302
|
+
it compiled; gating purely on kind would have lost it."""
|
|
303
|
+
mgr = PromptManager(PromptHttp(prompt=PROMPT_WIRE))
|
|
304
|
+
with wrap_agent(processor=processor, team_site_id="site-x", agent_name="chat"):
|
|
305
|
+
p = mgr.get("refund-agent", cache_ttl_seconds=0)
|
|
306
|
+
with span("hand-rolled"):
|
|
307
|
+
p.compile(q="x")
|
|
308
|
+
|
|
309
|
+
spans = http.run_ingest[0]["spans"]
|
|
310
|
+
attrs = next(s for s in spans if s["name"] == "hand-rolled")["attributes"]
|
|
311
|
+
assert attrs["trodo.prompt.name"] == "refund-agent"
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def test_each_span_gets_its_own_prompt_and_the_run_rolls_up_both(processor, http):
|
|
315
|
+
other = dict(PROMPT_WIRE, name="other-agent", version_hash="bbbb2222")
|
|
316
|
+
|
|
317
|
+
class Two:
|
|
318
|
+
def get_prompt(self, name, label=None, version=None):
|
|
319
|
+
return {"prompt": dict(other if name == "other-agent" else PROMPT_WIRE)}
|
|
320
|
+
|
|
321
|
+
mgr = PromptManager(Two())
|
|
322
|
+
with wrap_agent(processor=processor, team_site_id="site-x", agent_name="chat"):
|
|
323
|
+
a = mgr.get("refund-agent", cache_ttl_seconds=0)
|
|
324
|
+
b = mgr.get("other-agent", cache_ttl_seconds=0)
|
|
325
|
+
with span("span-a", kind="llm"):
|
|
326
|
+
a.compile(q="x")
|
|
327
|
+
with span("span-b", kind="llm"):
|
|
328
|
+
b.compile(q="y")
|
|
329
|
+
with span("span-c"):
|
|
330
|
+
pass
|
|
331
|
+
|
|
332
|
+
spans = http.run_ingest[0]["spans"]
|
|
333
|
+
at = lambda n: (next(s for s in spans if s["name"] == n).get("attributes") or {})
|
|
334
|
+
assert at("span-a")["trodo.prompt.name"] == "refund-agent"
|
|
335
|
+
assert at("span-b")["trodo.prompt.name"] == "other-agent"
|
|
336
|
+
assert "trodo.prompt.name" not in at("span-c")
|
|
337
|
+
used = http.run_ingest[0]["run"]["attributes"]["trodo.prompts"]
|
|
338
|
+
assert sorted(r["name"] for r in used) == ["other-agent", "refund-agent"]
|
|
@@ -554,6 +554,33 @@ def _register_instrumentors() -> None:
|
|
|
554
554
|
]
|
|
555
555
|
|
|
556
556
|
|
|
557
|
+
# Framework instrumentors that call a provider client we ALSO instrument.
|
|
558
|
+
#
|
|
559
|
+
# langchain-openai depends on the `openai` package, langchain-anthropic on
|
|
560
|
+
# `anthropic` -- so one chain invocation is also a genuine provider client call.
|
|
561
|
+
# With both instrumentors registered that single call emits TWO sibling llm
|
|
562
|
+
# spans carrying the same tokens, and the run's totals come out doubled.
|
|
563
|
+
# Measured on the Node side against real traffic: a 45/2 call reported 90/4.
|
|
564
|
+
#
|
|
565
|
+
# The provider span is the better of the two: it carries the real model id, so
|
|
566
|
+
# it can be priced, while the framework's reported model=unknown and could not
|
|
567
|
+
# be. So the framework instrumentor defers.
|
|
568
|
+
#
|
|
569
|
+
# Only providers registered BEFORE the framework in _INSTRUMENTORS may appear
|
|
570
|
+
# here -- the check reads `active`, which is built in registration order. That
|
|
571
|
+
# order is anthropic, openai, openai_v2, langchain, ... so those are covered;
|
|
572
|
+
# listing bedrock or cohere would read correctly and never fire.
|
|
573
|
+
_ROUTES_THROUGH: dict[str, tuple[str, ...]] = {
|
|
574
|
+
"langchain": ("openai", "openai_v2", "anthropic"),
|
|
575
|
+
"llama_index": ("openai", "openai_v2", "anthropic"),
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def _superseding_providers(name: str, active: List[str]) -> List[str]:
|
|
580
|
+
"""Provider instrumentors already active that `name` would double-count."""
|
|
581
|
+
return [p for p in _ROUTES_THROUGH.get(name, ()) if p in active]
|
|
582
|
+
|
|
583
|
+
|
|
557
584
|
def enable_auto_instrument(
|
|
558
585
|
processor: TrodoSpanProcessor,
|
|
559
586
|
disable: Optional[Iterable[str]] = None,
|
|
@@ -590,6 +617,24 @@ def enable_auto_instrument(
|
|
|
590
617
|
for name, register in _INSTRUMENTORS:
|
|
591
618
|
if name in disabled:
|
|
592
619
|
continue
|
|
620
|
+
# BEFORE registering: a framework instrumentor whose model calls are
|
|
621
|
+
# already captured by an active provider instrumentor would double every
|
|
622
|
+
# token. Checked here rather than after, because instrumentors patch on
|
|
623
|
+
# instrument() and undoing it afterwards is too late.
|
|
624
|
+
superseded = _superseding_providers(name, active)
|
|
625
|
+
if superseded:
|
|
626
|
+
_warn_once(
|
|
627
|
+
f"superseded-{name}",
|
|
628
|
+
f"auto-instrument: '{name}' was NOT loaded because "
|
|
629
|
+
f"{', '.join(superseded)} instrumentation is already active and "
|
|
630
|
+
f"{name} calls those clients directly -- running both makes one "
|
|
631
|
+
f"model call emit two llm spans and doubles its tokens and cost. "
|
|
632
|
+
f"Your {name} calls are still captured, by the provider "
|
|
633
|
+
f"instrumentation, which also records the real model id so the "
|
|
634
|
+
f"span can be priced. To choose the other way round, pass "
|
|
635
|
+
f"disable=['{superseded[0]}'].",
|
|
636
|
+
)
|
|
637
|
+
continue
|
|
593
638
|
try:
|
|
594
639
|
register()
|
|
595
640
|
active.append(name)
|
|
@@ -25,7 +25,7 @@ PromptRef = Dict[str, Any]
|
|
|
25
25
|
|
|
26
26
|
def new_prompt_state() -> Dict[str, Any]:
|
|
27
27
|
"""Per-run accumulator, shared by reference across the run's span tree."""
|
|
28
|
-
return {"current": None, "all": []}
|
|
28
|
+
return {"current": None, "current_span_id": None, "all": []}
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
# Compiled prompt (or its messages list) -> the version it came from.
|
|
@@ -93,6 +93,7 @@ def record_compiled_prompt(ref: PromptRef) -> None:
|
|
|
93
93
|
state = getattr(active, "prompt_state", None) if active is not None else None
|
|
94
94
|
if isinstance(state, dict):
|
|
95
95
|
state["current"] = ref
|
|
96
|
+
state["current_span_id"] = getattr(active, "span_id", None)
|
|
96
97
|
key = _ref_key(ref)
|
|
97
98
|
used: List[PromptRef] = state["all"]
|
|
98
99
|
if not any(_ref_key(r) == key for r in used):
|
|
@@ -116,7 +117,11 @@ def prompt_attributes(ref: Optional[PromptRef]) -> Dict[str, str]:
|
|
|
116
117
|
|
|
117
118
|
|
|
118
119
|
def merge_prompt_attrs(
|
|
119
|
-
active: Any,
|
|
120
|
+
active: Any,
|
|
121
|
+
attrs: Optional[Dict[str, Any]],
|
|
122
|
+
from_input: Optional[PromptRef] = None,
|
|
123
|
+
kind: Optional[str] = None,
|
|
124
|
+
span_id: Optional[str] = None,
|
|
120
125
|
) -> Optional[Dict[str, Any]]:
|
|
121
126
|
"""Merge the prompt this span used into its attributes.
|
|
122
127
|
|
|
@@ -134,7 +139,24 @@ def merge_prompt_attrs(
|
|
|
134
139
|
remember_prompt(state, from_input)
|
|
135
140
|
merged.update(prompt_attributes(from_input))
|
|
136
141
|
elif isinstance(state, dict):
|
|
137
|
-
|
|
142
|
+
# The run's "most recently compiled" prompt is a FALLBACK, used only
|
|
143
|
+
# where it is defensible. Applying it to every span meant compiling a
|
|
144
|
+
# prompt anywhere in a run attributed it to every LATER span -- a tool,
|
|
145
|
+
# a retrieval, a bit of plain work all claiming a prompt they never ran.
|
|
146
|
+
# Prompts belong to SPANS: one run's spans routinely use different
|
|
147
|
+
# prompts, different versions, or none at all.
|
|
148
|
+
#
|
|
149
|
+
# Two cases survive: a MODEL call (kind 'llm'), the only thing that runs
|
|
150
|
+
# a prompt and the one shape whose input we cannot read when it comes
|
|
151
|
+
# from a provider instrumentation; and the very span the prompt was
|
|
152
|
+
# compiled INSIDE, which plainly used it whatever its kind.
|
|
153
|
+
compiled_here = (
|
|
154
|
+
state.get("current") is not None
|
|
155
|
+
and span_id is not None
|
|
156
|
+
and state.get("current_span_id") == span_id
|
|
157
|
+
)
|
|
158
|
+
if kind == "llm" or compiled_here:
|
|
159
|
+
merged.update(prompt_attributes(state.get("current")))
|
|
138
160
|
return merged or None
|
|
139
161
|
|
|
140
162
|
|
|
@@ -876,7 +876,8 @@ class join_run:
|
|
|
876
876
|
temperature=self.handle.temperature,
|
|
877
877
|
tool_name=self.handle.tool_name,
|
|
878
878
|
attributes=merge_prompt_attrs(
|
|
879
|
-
self._ctx, self.handle.attributes, self.handle.prompt_ref
|
|
879
|
+
self._ctx, self.handle.attributes, self.handle.prompt_ref,
|
|
880
|
+
self._kind, self._span_id,
|
|
880
881
|
),
|
|
881
882
|
)
|
|
882
883
|
try:
|
|
@@ -988,7 +989,8 @@ class span:
|
|
|
988
989
|
temperature=self.handle.temperature,
|
|
989
990
|
tool_name=self.handle.tool_name,
|
|
990
991
|
attributes=merge_prompt_attrs(
|
|
991
|
-
self._active, self.handle.attributes, self.handle.prompt_ref
|
|
992
|
+
self._active, self.handle.attributes, self.handle.prompt_ref,
|
|
993
|
+
self._kind, self._span_id,
|
|
992
994
|
),
|
|
993
995
|
)
|
|
994
996
|
processor: TrodoSpanProcessor = self._active.processor # type: ignore[assignment]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|