prefactor-core 0.2.1__tar.gz → 0.2.2__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.
- {prefactor_core-0.2.1 → prefactor_core-0.2.2}/PKG-INFO +2 -2
- {prefactor_core-0.2.1 → prefactor_core-0.2.2}/pyproject.toml +2 -2
- {prefactor_core-0.2.1 → prefactor_core-0.2.2}/src/prefactor_core/__init__.py +4 -0
- {prefactor_core-0.2.1 → prefactor_core-0.2.2}/src/prefactor_core/client.py +9 -3
- {prefactor_core-0.2.1 → prefactor_core-0.2.2}/src/prefactor_core/managers/agent_instance.py +15 -5
- {prefactor_core-0.2.1 → prefactor_core-0.2.2}/src/prefactor_core/managers/span.py +15 -5
- {prefactor_core-0.2.1 → prefactor_core-0.2.2}/src/prefactor_core/operations.py +2 -0
- prefactor_core-0.2.2/src/prefactor_core/utils.py +41 -0
- prefactor_core-0.2.2/tests/test_span_manager.py +85 -0
- prefactor_core-0.2.2/tests/test_utils.py +46 -0
- {prefactor_core-0.2.1 → prefactor_core-0.2.2}/.gitignore +0 -0
- {prefactor_core-0.2.1 → prefactor_core-0.2.2}/README.md +0 -0
- {prefactor_core-0.2.1 → prefactor_core-0.2.2}/examples/agent_e2e.py +0 -0
- {prefactor_core-0.2.1 → prefactor_core-0.2.2}/src/prefactor_core/config.py +0 -0
- {prefactor_core-0.2.1 → prefactor_core-0.2.2}/src/prefactor_core/context_stack.py +0 -0
- {prefactor_core-0.2.1 → prefactor_core-0.2.2}/src/prefactor_core/exceptions.py +0 -0
- {prefactor_core-0.2.1 → prefactor_core-0.2.2}/src/prefactor_core/managers/__init__.py +0 -0
- {prefactor_core-0.2.1 → prefactor_core-0.2.2}/src/prefactor_core/models.py +0 -0
- {prefactor_core-0.2.1 → prefactor_core-0.2.2}/src/prefactor_core/queue/__init__.py +0 -0
- {prefactor_core-0.2.1 → prefactor_core-0.2.2}/src/prefactor_core/queue/base.py +0 -0
- {prefactor_core-0.2.1 → prefactor_core-0.2.2}/src/prefactor_core/queue/executor.py +0 -0
- {prefactor_core-0.2.1 → prefactor_core-0.2.2}/src/prefactor_core/queue/memory.py +0 -0
- {prefactor_core-0.2.1 → prefactor_core-0.2.2}/src/prefactor_core/schema_registry.py +0 -0
- {prefactor_core-0.2.1 → prefactor_core-0.2.2}/src/prefactor_core/span_context.py +0 -0
- {prefactor_core-0.2.1 → prefactor_core-0.2.2}/tests/test_client.py +0 -0
- {prefactor_core-0.2.1 → prefactor_core-0.2.2}/tests/test_imports.py +0 -0
- {prefactor_core-0.2.1 → prefactor_core-0.2.2}/tests/test_queue.py +0 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: prefactor-core
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Core Prefactor SDK with async queue-based operations
|
|
5
5
|
Author-email: Prefactor Pty Ltd <josh@prefactor.tech>
|
|
6
6
|
License: MIT
|
|
7
7
|
Requires-Python: <4.0.0,>=3.11.0
|
|
8
|
-
Requires-Dist: prefactor-http>=0.1.
|
|
8
|
+
Requires-Dist: prefactor-http>=0.1.1
|
|
9
9
|
Requires-Dist: pydantic>=2.0.0
|
|
10
10
|
Description-Content-Type: text/markdown
|
|
11
11
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "prefactor-core"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.2"
|
|
4
4
|
description = "Core Prefactor SDK with async queue-based operations"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = { text = "MIT" }
|
|
@@ -9,7 +9,7 @@ authors = [
|
|
|
9
9
|
]
|
|
10
10
|
requires-python = ">=3.11.0, <4.0.0"
|
|
11
11
|
dependencies = [
|
|
12
|
-
"prefactor-http>=0.1.
|
|
12
|
+
"prefactor-http>=0.1.1",
|
|
13
13
|
"pydantic>=2.0.0",
|
|
14
14
|
]
|
|
15
15
|
|
|
@@ -20,6 +20,7 @@ from .operations import Operation, OperationType
|
|
|
20
20
|
from .queue import InMemoryQueue, Queue, QueueClosedError, TaskExecutor
|
|
21
21
|
from .schema_registry import SchemaRegistry
|
|
22
22
|
from .span_context import SpanContext
|
|
23
|
+
from .utils import generate_idempotency_key, validate_idempotency_key
|
|
23
24
|
|
|
24
25
|
__version__ = "0.2.1"
|
|
25
26
|
|
|
@@ -54,4 +55,7 @@ __all__ = [
|
|
|
54
55
|
"AgentInstanceHandle",
|
|
55
56
|
# Schema Registry
|
|
56
57
|
"SchemaRegistry",
|
|
58
|
+
# Utils
|
|
59
|
+
"generate_idempotency_key",
|
|
60
|
+
"validate_idempotency_key",
|
|
57
61
|
]
|
|
@@ -188,12 +188,14 @@ class PrefactorCoreClient:
|
|
|
188
188
|
await self._http.agent_instances.start(
|
|
189
189
|
agent_instance_id=operation.payload["instance_id"],
|
|
190
190
|
timestamp=operation.timestamp,
|
|
191
|
+
idempotency_key=operation.payload.get("idempotency_key"),
|
|
191
192
|
)
|
|
192
193
|
|
|
193
194
|
elif operation.type == OperationType.FINISH_AGENT_INSTANCE:
|
|
194
195
|
await self._http.agent_instances.finish(
|
|
195
196
|
agent_instance_id=operation.payload["instance_id"],
|
|
196
197
|
timestamp=operation.timestamp,
|
|
198
|
+
idempotency_key=operation.payload.get("idempotency_key"),
|
|
197
199
|
)
|
|
198
200
|
elif operation.type == OperationType.CREATE_SPAN:
|
|
199
201
|
await self._http.agent_spans.create(
|
|
@@ -211,16 +213,22 @@ class PrefactorCoreClient:
|
|
|
211
213
|
status=operation.payload.get("status", "complete"),
|
|
212
214
|
result_payload=operation.payload.get("result_payload"),
|
|
213
215
|
timestamp=operation.timestamp,
|
|
216
|
+
idempotency_key=operation.payload.get("idempotency_key"),
|
|
214
217
|
)
|
|
215
218
|
|
|
216
219
|
except Exception as e:
|
|
217
|
-
# Log error
|
|
220
|
+
# Log error and re-raise so TaskExecutor retries can run
|
|
218
221
|
logger.error(
|
|
219
222
|
f"Failed to process operation {operation.type}: {e}",
|
|
220
223
|
exc_info=True,
|
|
221
224
|
)
|
|
222
225
|
raise
|
|
223
226
|
|
|
227
|
+
@property
|
|
228
|
+
def instance_manager(self) -> AgentInstanceManager | None:
|
|
229
|
+
"""Public accessor for the agent instance manager."""
|
|
230
|
+
return self._instance_manager
|
|
231
|
+
|
|
224
232
|
async def create_agent_instance(
|
|
225
233
|
self,
|
|
226
234
|
agent_id: str,
|
|
@@ -344,7 +352,6 @@ class PrefactorCoreClient:
|
|
|
344
352
|
instance_id: str,
|
|
345
353
|
schema_name: str,
|
|
346
354
|
parent_span_id: str | None = None,
|
|
347
|
-
span_id: str | None = None,
|
|
348
355
|
payload: dict[str, Any] | None = None,
|
|
349
356
|
):
|
|
350
357
|
"""Context manager for creating and finishing a span.
|
|
@@ -366,7 +373,6 @@ class PrefactorCoreClient:
|
|
|
366
373
|
instance_id: ID of the agent instance this span belongs to.
|
|
367
374
|
schema_name: Name of the schema for this span.
|
|
368
375
|
parent_span_id: Optional explicit parent span ID.
|
|
369
|
-
span_id: Ignored (API generates IDs).
|
|
370
376
|
payload: Optional initial payload sent via auto-start on exit
|
|
371
377
|
if ``start()`` is never called explicitly.
|
|
372
378
|
|
|
@@ -10,6 +10,7 @@ from datetime import datetime, timezone
|
|
|
10
10
|
from typing import TYPE_CHECKING, Any, Awaitable, Callable
|
|
11
11
|
|
|
12
12
|
from ..operations import Operation, OperationType
|
|
13
|
+
from ..utils import generate_idempotency_key
|
|
13
14
|
|
|
14
15
|
if TYPE_CHECKING:
|
|
15
16
|
from prefactor_http.client import PrefactorHttpClient
|
|
@@ -71,7 +72,9 @@ class AgentInstanceManager:
|
|
|
71
72
|
agent_id: ID of the agent to create an instance for.
|
|
72
73
|
agent_version: Version information (name, external_identifier, etc.).
|
|
73
74
|
agent_schema_version: Schema version information.
|
|
74
|
-
instance_id:
|
|
75
|
+
instance_id: Optional ID to forward to the API as ``id``. When
|
|
76
|
+
provided, the API uses it as the instance ID; when omitted,
|
|
77
|
+
the API generates one.
|
|
75
78
|
|
|
76
79
|
Returns:
|
|
77
80
|
The instance ID (API-generated).
|
|
@@ -81,6 +84,7 @@ class AgentInstanceManager:
|
|
|
81
84
|
agent_version=agent_version,
|
|
82
85
|
agent_schema_version=agent_schema_version,
|
|
83
86
|
id=instance_id,
|
|
87
|
+
idempotency_key=generate_idempotency_key(),
|
|
84
88
|
)
|
|
85
89
|
return result.id
|
|
86
90
|
|
|
@@ -94,7 +98,10 @@ class AgentInstanceManager:
|
|
|
94
98
|
"""
|
|
95
99
|
operation = Operation(
|
|
96
100
|
type=OperationType.START_AGENT_INSTANCE,
|
|
97
|
-
payload={
|
|
101
|
+
payload={
|
|
102
|
+
"instance_id": instance_id,
|
|
103
|
+
"idempotency_key": generate_idempotency_key(),
|
|
104
|
+
},
|
|
98
105
|
timestamp=datetime.now(timezone.utc),
|
|
99
106
|
)
|
|
100
107
|
|
|
@@ -110,7 +117,10 @@ class AgentInstanceManager:
|
|
|
110
117
|
"""
|
|
111
118
|
operation = Operation(
|
|
112
119
|
type=OperationType.FINISH_AGENT_INSTANCE,
|
|
113
|
-
payload={
|
|
120
|
+
payload={
|
|
121
|
+
"instance_id": instance_id,
|
|
122
|
+
"idempotency_key": generate_idempotency_key(),
|
|
123
|
+
},
|
|
114
124
|
timestamp=datetime.now(timezone.utc),
|
|
115
125
|
)
|
|
116
126
|
|
|
@@ -169,7 +179,7 @@ class AgentInstanceHandle:
|
|
|
169
179
|
if self._started:
|
|
170
180
|
return
|
|
171
181
|
|
|
172
|
-
manager = self._client.
|
|
182
|
+
manager = self._client.instance_manager
|
|
173
183
|
assert manager is not None
|
|
174
184
|
await manager.start(self._instance_id)
|
|
175
185
|
self._started = True
|
|
@@ -182,7 +192,7 @@ class AgentInstanceHandle:
|
|
|
182
192
|
if self._finished:
|
|
183
193
|
return
|
|
184
194
|
|
|
185
|
-
manager = self._client.
|
|
195
|
+
manager = self._client.instance_manager
|
|
186
196
|
assert manager is not None
|
|
187
197
|
await manager.finish(self._instance_id)
|
|
188
198
|
self._finished = True
|
|
@@ -5,13 +5,13 @@ calls into Operation objects that are queued for processing. It also manages
|
|
|
5
5
|
the span stack for automatic parent detection.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
import uuid
|
|
9
8
|
from datetime import datetime, timezone
|
|
10
9
|
from typing import TYPE_CHECKING, Any, Awaitable, Callable
|
|
11
10
|
|
|
12
11
|
from ..context_stack import SpanContextStack
|
|
13
12
|
from ..models import Span
|
|
14
13
|
from ..operations import Operation, OperationType
|
|
14
|
+
from ..utils import generate_idempotency_key
|
|
15
15
|
|
|
16
16
|
if TYPE_CHECKING:
|
|
17
17
|
from prefactor_http.client import PrefactorHttpClient
|
|
@@ -90,7 +90,7 @@ class SpanManager:
|
|
|
90
90
|
if parent_span_id is None:
|
|
91
91
|
parent_span_id = SpanContextStack.peek()
|
|
92
92
|
|
|
93
|
-
temp_id =
|
|
93
|
+
temp_id = generate_idempotency_key()
|
|
94
94
|
|
|
95
95
|
span = Span(
|
|
96
96
|
id=temp_id,
|
|
@@ -139,6 +139,7 @@ class SpanManager:
|
|
|
139
139
|
status="active",
|
|
140
140
|
payload=payload or {},
|
|
141
141
|
parent_span_id=span.parent_span_id,
|
|
142
|
+
idempotency_key=generate_idempotency_key(),
|
|
142
143
|
)
|
|
143
144
|
|
|
144
145
|
api_id = result.id
|
|
@@ -185,12 +186,14 @@ class SpanManager:
|
|
|
185
186
|
status="pending",
|
|
186
187
|
payload={},
|
|
187
188
|
parent_span_id=span.parent_span_id,
|
|
189
|
+
idempotency_key=generate_idempotency_key(),
|
|
188
190
|
)
|
|
189
191
|
api_id = result.id
|
|
190
192
|
|
|
191
193
|
await self._http.agent_spans.finish(
|
|
192
194
|
agent_span_id=api_id,
|
|
193
195
|
status="cancelled",
|
|
196
|
+
idempotency_key=generate_idempotency_key(),
|
|
194
197
|
)
|
|
195
198
|
|
|
196
199
|
span.status = "cancelled"
|
|
@@ -261,10 +264,17 @@ class SpanManager:
|
|
|
261
264
|
self._spans[span_id].status = status
|
|
262
265
|
self._spans[span_id].finished_at = datetime.now(timezone.utc)
|
|
263
266
|
|
|
264
|
-
|
|
265
|
-
|
|
267
|
+
stack = SpanContextStack.get_stack()
|
|
268
|
+
if span_id in stack:
|
|
269
|
+
from ..context_stack import _current_span_stack
|
|
270
|
+
|
|
271
|
+
_current_span_stack.set([s for s in stack if s != span_id])
|
|
266
272
|
|
|
267
|
-
op_payload: dict[str, Any] = {
|
|
273
|
+
op_payload: dict[str, Any] = {
|
|
274
|
+
"span_id": span_id,
|
|
275
|
+
"status": status,
|
|
276
|
+
"idempotency_key": generate_idempotency_key(),
|
|
277
|
+
}
|
|
268
278
|
if result_payload is not None:
|
|
269
279
|
op_payload["result_payload"] = result_payload
|
|
270
280
|
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Utility functions for prefactor-core."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import uuid
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def generate_idempotency_key() -> str:
|
|
9
|
+
"""Generate a new UUID-based idempotency key.
|
|
10
|
+
|
|
11
|
+
The returned key is a UUID4 string (36 characters), always within the
|
|
12
|
+
64-character API limit.
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
A unique idempotency key string.
|
|
16
|
+
"""
|
|
17
|
+
return str(uuid.uuid4())
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def validate_idempotency_key(key: str) -> str:
|
|
21
|
+
"""Validate that an idempotency key is a non-empty string of at most 64 characters.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
key: The idempotency key to validate.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
The key unchanged if valid.
|
|
28
|
+
|
|
29
|
+
Raises:
|
|
30
|
+
ValueError: If the key is empty or exceeds 64 characters.
|
|
31
|
+
"""
|
|
32
|
+
if not key:
|
|
33
|
+
raise ValueError("Idempotency key must not be empty")
|
|
34
|
+
if len(key) > 64:
|
|
35
|
+
raise ValueError(
|
|
36
|
+
f"Idempotency key must be at most 64 characters, got {len(key)}"
|
|
37
|
+
)
|
|
38
|
+
return key
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
__all__ = ["generate_idempotency_key", "validate_idempotency_key"]
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Tests for SpanManager idempotency key auto-generation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import uuid
|
|
6
|
+
from unittest.mock import AsyncMock, MagicMock
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
from prefactor_core.managers.span import SpanManager
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _make_span_result(span_id: str = "api-span-id") -> MagicMock:
|
|
13
|
+
result = MagicMock()
|
|
14
|
+
result.id = span_id
|
|
15
|
+
return result
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@pytest.fixture
|
|
19
|
+
def http_client():
|
|
20
|
+
"""Return a mock HTTP client with stubbed agent_spans create/finish methods."""
|
|
21
|
+
client = MagicMock()
|
|
22
|
+
client.agent_spans = MagicMock()
|
|
23
|
+
client.agent_spans.create = AsyncMock(return_value=_make_span_result())
|
|
24
|
+
client.agent_spans.finish = AsyncMock(return_value=_make_span_result())
|
|
25
|
+
return client
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@pytest.fixture
|
|
29
|
+
def enqueue():
|
|
30
|
+
"""Return an async mock for the enqueue callback."""
|
|
31
|
+
return AsyncMock()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@pytest.fixture
|
|
35
|
+
def manager(http_client, enqueue):
|
|
36
|
+
"""Return a SpanManager wired to the mock http_client and enqueue fixtures."""
|
|
37
|
+
return SpanManager(http_client, enqueue)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class TestSpanManagerIdempotencyKeys:
|
|
41
|
+
"""Tests that SpanManager generates and propagates idempotency keys correctly."""
|
|
42
|
+
|
|
43
|
+
async def test_start_passes_idempotency_key(self, manager, http_client):
|
|
44
|
+
"""start() should pass a valid UUID idempotency key to agent_spans.create."""
|
|
45
|
+
temp_id = manager.prepare(instance_id="inst-1", schema_name="agent:llm")
|
|
46
|
+
await manager.start(temp_id)
|
|
47
|
+
|
|
48
|
+
call_kwargs = http_client.agent_spans.create.call_args.kwargs
|
|
49
|
+
key = call_kwargs.get("idempotency_key")
|
|
50
|
+
assert key is not None
|
|
51
|
+
assert len(key) <= 64
|
|
52
|
+
uuid.UUID(key) # must be valid UUID
|
|
53
|
+
|
|
54
|
+
async def test_cancel_unstarted_passes_distinct_keys(self, manager, http_client):
|
|
55
|
+
"""cancel_unstarted() should use distinct keys for create and finish calls."""
|
|
56
|
+
temp_id = manager.prepare(instance_id="inst-1", schema_name="agent:llm")
|
|
57
|
+
await manager.cancel_unstarted(temp_id)
|
|
58
|
+
|
|
59
|
+
create_key = http_client.agent_spans.create.call_args.kwargs.get(
|
|
60
|
+
"idempotency_key"
|
|
61
|
+
)
|
|
62
|
+
finish_key = http_client.agent_spans.finish.call_args.kwargs.get(
|
|
63
|
+
"idempotency_key"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
assert create_key is not None
|
|
67
|
+
assert finish_key is not None
|
|
68
|
+
assert create_key != finish_key
|
|
69
|
+
|
|
70
|
+
async def test_finish_includes_idempotency_key_in_operation(
|
|
71
|
+
self, manager, http_client, enqueue
|
|
72
|
+
):
|
|
73
|
+
"""finish() should enqueue an operation with a valid UUID idempotency key."""
|
|
74
|
+
# Start a span first to get a real API id
|
|
75
|
+
temp_id = manager.prepare(instance_id="inst-1", schema_name="agent:llm")
|
|
76
|
+
api_id = await manager.start(temp_id)
|
|
77
|
+
|
|
78
|
+
await manager.finish(api_id)
|
|
79
|
+
|
|
80
|
+
enqueue.assert_called_once()
|
|
81
|
+
operation = enqueue.call_args.args[0]
|
|
82
|
+
key = operation.payload.get("idempotency_key")
|
|
83
|
+
assert key is not None
|
|
84
|
+
assert len(key) <= 64
|
|
85
|
+
uuid.UUID(key)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Tests for prefactor_core utility functions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import uuid
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
from prefactor_core.utils import generate_idempotency_key, validate_idempotency_key
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TestGenerateIdempotencyKey:
|
|
12
|
+
def test_returns_valid_uuid(self):
|
|
13
|
+
key = generate_idempotency_key()
|
|
14
|
+
parsed = uuid.UUID(key)
|
|
15
|
+
assert parsed.version == 4
|
|
16
|
+
|
|
17
|
+
def test_max_length(self):
|
|
18
|
+
key = generate_idempotency_key()
|
|
19
|
+
assert len(key) <= 64
|
|
20
|
+
|
|
21
|
+
def test_unique_per_call(self):
|
|
22
|
+
keys = {generate_idempotency_key() for _ in range(10)}
|
|
23
|
+
assert len(keys) == 10
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TestValidateIdempotencyKey:
|
|
27
|
+
def test_accepts_valid_key(self):
|
|
28
|
+
key = "a" * 64
|
|
29
|
+
assert validate_idempotency_key(key) == key
|
|
30
|
+
|
|
31
|
+
def test_accepts_short_key(self):
|
|
32
|
+
key = "abc"
|
|
33
|
+
assert validate_idempotency_key(key) == key
|
|
34
|
+
|
|
35
|
+
def test_rejects_empty_key(self):
|
|
36
|
+
with pytest.raises(ValueError, match="must not be empty"):
|
|
37
|
+
validate_idempotency_key("")
|
|
38
|
+
|
|
39
|
+
def test_rejects_oversized_key(self):
|
|
40
|
+
key = "a" * 65
|
|
41
|
+
with pytest.raises(ValueError, match="64 characters"):
|
|
42
|
+
validate_idempotency_key(key)
|
|
43
|
+
|
|
44
|
+
def test_accepts_uuid_key(self):
|
|
45
|
+
key = str(uuid.uuid4())
|
|
46
|
+
assert validate_idempotency_key(key) == key
|
|
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
|