axio 0.4.1__tar.gz → 0.5.1__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 (44) hide show
  1. {axio-0.4.1 → axio-0.5.1}/PKG-INFO +37 -6
  2. {axio-0.4.1 → axio-0.5.1}/README.md +36 -5
  3. {axio-0.4.1 → axio-0.5.1}/pyproject.toml +3 -1
  4. {axio-0.4.1 → axio-0.5.1}/src/axio/agent.py +7 -2
  5. axio-0.5.1/src/axio/compaction.py +67 -0
  6. {axio-0.4.1 → axio-0.5.1}/src/axio/context.py +22 -77
  7. {axio-0.4.1 → axio-0.5.1}/src/axio/models.py +12 -0
  8. {axio-0.4.1 → axio-0.5.1}/src/axio/transport.py +9 -1
  9. {axio-0.4.1 → axio-0.5.1}/tests/test_context.py +2 -1
  10. axio-0.5.1/tests/test_models.py +61 -0
  11. {axio-0.4.1 → axio-0.5.1}/.github/workflows/publish.yml +0 -0
  12. {axio-0.4.1 → axio-0.5.1}/.github/workflows/tests.yml +0 -0
  13. {axio-0.4.1 → axio-0.5.1}/.gitignore +0 -0
  14. {axio-0.4.1 → axio-0.5.1}/LICENSE +0 -0
  15. {axio-0.4.1 → axio-0.5.1}/Makefile +0 -0
  16. {axio-0.4.1 → axio-0.5.1}/src/axio/__init__.py +0 -0
  17. {axio-0.4.1 → axio-0.5.1}/src/axio/blocks.py +0 -0
  18. {axio-0.4.1 → axio-0.5.1}/src/axio/events.py +0 -0
  19. {axio-0.4.1 → axio-0.5.1}/src/axio/exceptions.py +0 -0
  20. {axio-0.4.1 → axio-0.5.1}/src/axio/messages.py +0 -0
  21. {axio-0.4.1 → axio-0.5.1}/src/axio/permission.py +0 -0
  22. {axio-0.4.1 → axio-0.5.1}/src/axio/py.typed +0 -0
  23. {axio-0.4.1 → axio-0.5.1}/src/axio/selector.py +0 -0
  24. {axio-0.4.1 → axio-0.5.1}/src/axio/stream.py +0 -0
  25. {axio-0.4.1 → axio-0.5.1}/src/axio/testing.py +0 -0
  26. {axio-0.4.1 → axio-0.5.1}/src/axio/tool.py +0 -0
  27. {axio-0.4.1 → axio-0.5.1}/src/axio/tool_args.py +0 -0
  28. {axio-0.4.1 → axio-0.5.1}/src/axio/types.py +0 -0
  29. {axio-0.4.1 → axio-0.5.1}/tests/conftest.py +0 -0
  30. {axio-0.4.1 → axio-0.5.1}/tests/test_agent_branch.py +0 -0
  31. {axio-0.4.1 → axio-0.5.1}/tests/test_agent_permission.py +0 -0
  32. {axio-0.4.1 → axio-0.5.1}/tests/test_agent_run.py +0 -0
  33. {axio-0.4.1 → axio-0.5.1}/tests/test_agent_stream.py +0 -0
  34. {axio-0.4.1 → axio-0.5.1}/tests/test_agent_tools.py +0 -0
  35. {axio-0.4.1 → axio-0.5.1}/tests/test_blocks.py +0 -0
  36. {axio-0.4.1 → axio-0.5.1}/tests/test_events.py +0 -0
  37. {axio-0.4.1 → axio-0.5.1}/tests/test_exceptions.py +0 -0
  38. {axio-0.4.1 → axio-0.5.1}/tests/test_permission.py +0 -0
  39. {axio-0.4.1 → axio-0.5.1}/tests/test_stream.py +0 -0
  40. {axio-0.4.1 → axio-0.5.1}/tests/test_tool.py +0 -0
  41. {axio-0.4.1 → axio-0.5.1}/tests/test_tool_args.py +0 -0
  42. {axio-0.4.1 → axio-0.5.1}/tests/test_transport.py +0 -0
  43. {axio-0.4.1 → axio-0.5.1}/tests/test_types.py +0 -0
  44. {axio-0.4.1 → axio-0.5.1}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: axio
3
- Version: 0.4.1
3
+ Version: 0.5.1
4
4
  Summary: Minimal, streaming-first, protocol-driven foundation for LLM-powered agents
5
5
  Project-URL: Homepage, https://github.com/axio-agent/axio
6
6
  Project-URL: Repository, https://github.com/axio-agent/axio
@@ -38,9 +38,24 @@ pip install axio
38
38
 
39
39
  ## Quick start
40
40
 
41
+ <!--
42
+ name: test_readme_quick_start
43
+ ```python
44
+ import sys, types
45
+ from axio.testing import StubTransport, make_text_response
46
+
47
+ _m = types.ModuleType("axio_transport_openai")
48
+ class OpenAITransport(StubTransport):
49
+ def __init__(self, api_key: str = "", model: str = "") -> None:
50
+ super().__init__([make_text_response("Hello, Alice!")])
51
+ _m.OpenAITransport = OpenAITransport # type: ignore[attr-defined]
52
+ sys.modules["axio_transport_openai"] = _m
53
+ ```
54
+ -->
55
+ <!-- name: test_readme_quick_start -->
41
56
  ```python
42
57
  import asyncio
43
- from axio import Agent
58
+ from axio.agent import Agent
44
59
  from axio.context import MemoryContextStore
45
60
  from axio.tool import Tool, ToolHandler
46
61
 
@@ -97,6 +112,7 @@ asyncio.run(main())
97
112
 
98
113
  ### CompletionTransport
99
114
 
115
+ <!-- name: test_readme_completion_transport -->
100
116
  ```python
101
117
  from typing import Protocol, runtime_checkable
102
118
  from collections.abc import AsyncIterator
@@ -113,18 +129,30 @@ class CompletionTransport(Protocol):
113
129
 
114
130
  ### ContextStore
115
131
 
132
+ <!-- name: test_readme_context_store -->
116
133
  ```python
117
- class ContextStore(ABC):
134
+ from abc import ABC, abstractmethod
135
+ from axio.context import ContextStore
136
+ from axio.messages import Message
137
+
138
+ class MyContextStore(ContextStore, ABC):
139
+ @abstractmethod
118
140
  async def append(self, message: Message) -> None: ...
141
+
142
+ @abstractmethod
119
143
  async def get_history(self) -> list[Message]: ...
120
- async def fork(self) -> ContextStore: ... # branch conversation
121
- async def clear(self) -> None: ...
122
- async def close(self) -> None: ...
144
+
145
+ # Everything else — session_id, close(), fork(), clear(),
146
+ # get/set_context_tokens() has a default implementation.
123
147
  ```
124
148
 
125
149
  ### PermissionGuard
126
150
 
151
+ <!-- name: test_readme_permission_guard -->
127
152
  ```python
153
+ from typing import Protocol, runtime_checkable
154
+ from axio.tool import ToolHandler
155
+
128
156
  @runtime_checkable
129
157
  class PermissionGuard(Protocol):
130
158
  async def check(self, handler: ToolHandler) -> ToolHandler: ...
@@ -144,6 +172,7 @@ class PermissionGuard(Protocol):
144
172
 
145
173
  ## Tools
146
174
 
175
+ <!-- name: test_readme_tools -->
147
176
  ```python
148
177
  from axio.tool import Tool, ToolHandler
149
178
 
@@ -166,7 +195,9 @@ tool = Tool(
166
195
 
167
196
  ## Testing
168
197
 
198
+ <!-- name: test_readme_testing -->
169
199
  ```python
200
+ from axio.agent import Agent
170
201
  from axio.testing import (
171
202
  StubTransport,
172
203
  make_tool_use_response,
@@ -25,9 +25,24 @@ pip install axio
25
25
 
26
26
  ## Quick start
27
27
 
28
+ <!--
29
+ name: test_readme_quick_start
30
+ ```python
31
+ import sys, types
32
+ from axio.testing import StubTransport, make_text_response
33
+
34
+ _m = types.ModuleType("axio_transport_openai")
35
+ class OpenAITransport(StubTransport):
36
+ def __init__(self, api_key: str = "", model: str = "") -> None:
37
+ super().__init__([make_text_response("Hello, Alice!")])
38
+ _m.OpenAITransport = OpenAITransport # type: ignore[attr-defined]
39
+ sys.modules["axio_transport_openai"] = _m
40
+ ```
41
+ -->
42
+ <!-- name: test_readme_quick_start -->
28
43
  ```python
29
44
  import asyncio
30
- from axio import Agent
45
+ from axio.agent import Agent
31
46
  from axio.context import MemoryContextStore
32
47
  from axio.tool import Tool, ToolHandler
33
48
 
@@ -84,6 +99,7 @@ asyncio.run(main())
84
99
 
85
100
  ### CompletionTransport
86
101
 
102
+ <!-- name: test_readme_completion_transport -->
87
103
  ```python
88
104
  from typing import Protocol, runtime_checkable
89
105
  from collections.abc import AsyncIterator
@@ -100,18 +116,30 @@ class CompletionTransport(Protocol):
100
116
 
101
117
  ### ContextStore
102
118
 
119
+ <!-- name: test_readme_context_store -->
103
120
  ```python
104
- class ContextStore(ABC):
121
+ from abc import ABC, abstractmethod
122
+ from axio.context import ContextStore
123
+ from axio.messages import Message
124
+
125
+ class MyContextStore(ContextStore, ABC):
126
+ @abstractmethod
105
127
  async def append(self, message: Message) -> None: ...
128
+
129
+ @abstractmethod
106
130
  async def get_history(self) -> list[Message]: ...
107
- async def fork(self) -> ContextStore: ... # branch conversation
108
- async def clear(self) -> None: ...
109
- async def close(self) -> None: ...
131
+
132
+ # Everything else — session_id, close(), fork(), clear(),
133
+ # get/set_context_tokens() has a default implementation.
110
134
  ```
111
135
 
112
136
  ### PermissionGuard
113
137
 
138
+ <!-- name: test_readme_permission_guard -->
114
139
  ```python
140
+ from typing import Protocol, runtime_checkable
141
+ from axio.tool import ToolHandler
142
+
115
143
  @runtime_checkable
116
144
  class PermissionGuard(Protocol):
117
145
  async def check(self, handler: ToolHandler) -> ToolHandler: ...
@@ -131,6 +159,7 @@ class PermissionGuard(Protocol):
131
159
 
132
160
  ## Tools
133
161
 
162
+ <!-- name: test_readme_tools -->
134
163
  ```python
135
164
  from axio.tool import Tool, ToolHandler
136
165
 
@@ -153,7 +182,9 @@ tool = Tool(
153
182
 
154
183
  ## Testing
155
184
 
185
+ <!-- name: test_readme_testing -->
156
186
  ```python
187
+ from axio.agent import Agent
157
188
  from axio.testing import (
158
189
  StubTransport,
159
190
  make_tool_use_response,
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "axio"
3
- version = "0.4.1"
3
+ version = "0.5.1"
4
4
  description = "Minimal, streaming-first, protocol-driven foundation for LLM-powered agents"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -21,6 +21,7 @@ packages = ["src/axio"]
21
21
 
22
22
  [tool.pytest.ini_options]
23
23
  asyncio_mode = "auto"
24
+ testpaths = ["tests", "README.md"]
24
25
 
25
26
  [tool.ruff]
26
27
  line-length = 119
@@ -40,4 +41,5 @@ dev = [
40
41
  "mypy>=1.14",
41
42
  "ruff>=0.9",
42
43
  "pytest-cov>=7.1.0",
44
+ "markdown-pytest>=0.6.0",
43
45
  ]
@@ -3,11 +3,12 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
+ import dataclasses
6
7
  import json
7
8
  import logging
8
9
  from collections.abc import AsyncGenerator, Iterable
9
10
  from dataclasses import dataclass, field
10
- from typing import Any
11
+ from typing import Any, Self
11
12
 
12
13
  from axio.blocks import TextBlock, ToolResultBlock, ToolUseBlock
13
14
  from axio.context import ContextStore
@@ -34,11 +35,15 @@ logger = logging.getLogger(__name__)
34
35
  @dataclass(slots=True)
35
36
  class Agent:
36
37
  system: str
37
- tools: list[Tool]
38
38
  transport: CompletionTransport
39
+ tools: list[Tool] = field(default_factory=list)
39
40
  selector: ToolSelector | None = field(default=None)
40
41
  max_iterations: int = field(default=50)
41
42
 
43
+ def copy(self, **overrides: Any) -> Self:
44
+ """Return a new Agent with *overrides* applied."""
45
+ return dataclasses.replace(self, **overrides)
46
+
42
47
  def run_stream(self, user_message: str, context: ContextStore) -> AgentStream:
43
48
  return AgentStream(self._run_loop(user_message, context))
44
49
 
@@ -0,0 +1,67 @@
1
+ """Context compaction: summarise old history to stay within token limits."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+
7
+ from axio.agent import Agent
8
+ from axio.blocks import TextBlock, ToolResultBlock
9
+ from axio.context import ContextStore, MemoryContextStore
10
+ from axio.messages import Message
11
+ from axio.transport import CompletionTransport
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ _DEFAULT_COMPACTION_PROMPT = (
16
+ "You are a conversation summarizer. You will see a conversation between"
17
+ " a user and an AI assistant, including tool calls and their results."
18
+ " Produce a concise summary preserving: user goals, decisions made,"
19
+ " key facts, tool outcomes, and state changes. Write as narrative prose,"
20
+ " not as a transcript."
21
+ )
22
+
23
+
24
+ async def compact_context(
25
+ context: ContextStore,
26
+ transport: CompletionTransport,
27
+ *,
28
+ max_messages: int = 20,
29
+ keep_recent: int = 6,
30
+ system_prompt: str = _DEFAULT_COMPACTION_PROMPT,
31
+ ) -> list[Message] | None:
32
+ """Summarize old messages from *context*, keeping recent ones verbatim.
33
+
34
+ Returns a compacted message list ready to populate a fresh store,
35
+ or ``None`` when no compaction is needed.
36
+ """
37
+ history = await context.get_history()
38
+ if len(history) <= max_messages:
39
+ return None
40
+
41
+ split = _find_safe_boundary(history, keep_recent)
42
+ if split <= 0:
43
+ return None
44
+
45
+ old, recent = history[:split], history[split:]
46
+
47
+ summary_ctx = MemoryContextStore(old)
48
+ agent = Agent(system=system_prompt, transport=transport, max_iterations=1)
49
+ try:
50
+ summary = await agent.run("Summarize the conversation above.", summary_ctx)
51
+ except Exception:
52
+ logger.warning("Context compaction failed, keeping original history", exc_info=True)
53
+ return None
54
+
55
+ return [
56
+ Message(role="user", content=[TextBlock(text=summary)]),
57
+ Message(role="assistant", content=[TextBlock(text="Understood, context restored.")]),
58
+ *recent,
59
+ ]
60
+
61
+
62
+ def _find_safe_boundary(history: list[Message], keep_recent: int) -> int:
63
+ """Return a split index that never separates a tool_use from its tool_result."""
64
+ split = len(history) - keep_recent
65
+ while split > 0 and any(isinstance(b, ToolResultBlock) for b in history[split].content):
66
+ split -= 1
67
+ return split
@@ -9,9 +9,8 @@ from dataclasses import dataclass
9
9
  from typing import Self
10
10
  from uuid import uuid4
11
11
 
12
- from axio.blocks import TextBlock, ToolResultBlock
12
+ from axio.blocks import TextBlock
13
13
  from axio.messages import Message
14
- from axio.transport import CompletionTransport
15
14
 
16
15
  logger = logging.getLogger(__name__)
17
16
 
@@ -28,8 +27,11 @@ class SessionInfo:
28
27
 
29
28
  class ContextStore(ABC):
30
29
  @property
31
- @abstractmethod
32
- def session_id(self) -> str: ...
30
+ def session_id(self) -> str:
31
+ """Lazy-init UUID hex; works without calling super().__init__()."""
32
+ if "_session_id" not in self.__dict__:
33
+ self.__dict__["_session_id"] = uuid4().hex
34
+ return str(self.__dict__["_session_id"])
33
35
 
34
36
  @abstractmethod
35
37
  async def append(self, message: Message) -> None: ...
@@ -37,20 +39,26 @@ class ContextStore(ABC):
37
39
  @abstractmethod
38
40
  async def get_history(self) -> list[Message]: ...
39
41
 
40
- @abstractmethod
41
- async def clear(self) -> None: ...
42
+ async def clear(self) -> None:
43
+ raise NotImplementedError(f"{type(self).__name__} does not support clear()")
42
44
 
43
- @abstractmethod
44
- async def fork(self) -> ContextStore: ...
45
+ async def fork(self) -> ContextStore:
46
+ """Returns a MemoryContextStore deep copy by default."""
47
+ messages = copy.deepcopy(await self.get_history())
48
+ in_tok, out_tok = await self.get_context_tokens()
49
+ store = MemoryContextStore(messages)
50
+ await store.set_context_tokens(in_tok, out_tok)
51
+ return store
45
52
 
46
- @abstractmethod
47
- async def set_context_tokens(self, input_tokens: int, output_tokens: int) -> None: ...
53
+ async def set_context_tokens(self, input_tokens: int, output_tokens: int) -> None:
54
+ """No-op by default; tokens are silently dropped."""
48
55
 
49
- @abstractmethod
50
- async def get_context_tokens(self) -> tuple[int, int]: ...
56
+ async def get_context_tokens(self) -> tuple[int, int]:
57
+ """Returns (0, 0) by default."""
58
+ return 0, 0
51
59
 
52
- @abstractmethod
53
- async def close(self) -> None: ...
60
+ async def close(self) -> None:
61
+ """No-op by default."""
54
62
 
55
63
  async def list_sessions(self) -> list[SessionInfo]:
56
64
  """List available sessions. Default: returns a single entry for the current session."""
@@ -132,66 +140,3 @@ class MemoryContextStore(ContextStore):
132
140
 
133
141
  async def close(self) -> None:
134
142
  pass
135
-
136
-
137
- _DEFAULT_COMPACTION_PROMPT = (
138
- "You are a conversation summarizer. You will see a conversation between"
139
- " a user and an AI assistant, including tool calls and their results."
140
- " Produce a concise summary preserving: user goals, decisions made,"
141
- " key facts, tool outcomes, and state changes. Write as narrative prose,"
142
- " not as a transcript."
143
- )
144
-
145
-
146
- async def compact_context(
147
- context: ContextStore,
148
- transport: CompletionTransport,
149
- *,
150
- max_messages: int = 20,
151
- keep_recent: int = 6,
152
- system_prompt: str | None = None,
153
- ) -> list[Message] | None:
154
- """Summarize old messages from *context*, keeping recent ones verbatim.
155
-
156
- Returns a compacted message list ready to populate a fresh store,
157
- or ``None`` when no compaction is needed.
158
- """
159
- history = await context.get_history()
160
- if len(history) <= max_messages:
161
- return None
162
-
163
- split = _find_safe_boundary(history, keep_recent)
164
- if split <= 0:
165
- return None
166
-
167
- old, recent = history[:split], history[split:]
168
-
169
- # Deferred import to avoid circular dependency (context ↔ agent)
170
- from axio.agent import Agent
171
-
172
- summary_ctx = MemoryContextStore(old)
173
- agent = Agent(
174
- system=system_prompt or _DEFAULT_COMPACTION_PROMPT,
175
- tools=[],
176
- transport=transport,
177
- max_iterations=1,
178
- )
179
- try:
180
- summary = await agent.run("Summarize the conversation above.", summary_ctx)
181
- except Exception:
182
- logger.warning("Context compaction failed, keeping original history", exc_info=True)
183
- return None
184
-
185
- return [
186
- Message(role="user", content=[TextBlock(text=summary)]),
187
- Message(role="assistant", content=[TextBlock(text="Understood, context restored.")]),
188
- *recent,
189
- ]
190
-
191
-
192
- def _find_safe_boundary(history: list[Message], keep_recent: int) -> int:
193
- """Return a split index that never separates a tool_use from its tool_result."""
194
- split = len(history) - keep_recent
195
- while split > 0 and any(isinstance(b, ToolResultBlock) for b in history[split].content):
196
- split -= 1
197
- return split
@@ -91,3 +91,15 @@ class ModelRegistry(MutableMapping[str, ModelSpec]):
91
91
 
92
92
  def ids(self) -> list[str]:
93
93
  return list(self._models)
94
+
95
+ def _get_model_by_index(self, index: int) -> ModelSpec:
96
+ vals = list(iter(self._models.values()))
97
+ if len(vals) >= 1:
98
+ return vals[index]
99
+ raise IndexError("ModelRegistry is empty")
100
+
101
+ def first(self) -> ModelSpec:
102
+ return self._get_model_by_index(0)
103
+
104
+ def last(self) -> ModelSpec:
105
+ return self._get_model_by_index(-1)
@@ -1,4 +1,12 @@
1
- """Transport protocols: completion, image gen, TTS, STT."""
1
+ """Transport protocols: completion, image gen, TTS, STT.
2
+
3
+ Transports should be stateless — all request state lives in the arguments
4
+ passed to each method call. This allows multiple agents to share a single
5
+ transport instance and call it concurrently without interference.
6
+
7
+ The one allowed exception is a reusable connection pool (e.g. an
8
+ ``aiohttp.ClientSession``), which is safe to share across concurrent calls.
9
+ """
2
10
 
3
11
  from __future__ import annotations
4
12
 
@@ -6,7 +6,8 @@ from collections.abc import AsyncIterator
6
6
  from typing import Literal
7
7
 
8
8
  from axio.blocks import TextBlock, ToolResultBlock
9
- from axio.context import ContextStore, MemoryContextStore, _find_safe_boundary, compact_context
9
+ from axio.compaction import _find_safe_boundary, compact_context
10
+ from axio.context import ContextStore, MemoryContextStore
10
11
  from axio.events import StreamEvent
11
12
  from axio.messages import Message
12
13
  from axio.testing import StubTransport, make_text_response
@@ -0,0 +1,61 @@
1
+ """Tests for ModelRegistry.first() and ModelRegistry.last()."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import pytest
6
+
7
+ from axio.models import ModelRegistry, ModelSpec
8
+
9
+ A = ModelSpec(id="a", input_cost=1.0)
10
+ B = ModelSpec(id="b", input_cost=2.0)
11
+ C = ModelSpec(id="c", input_cost=3.0)
12
+
13
+
14
+ def test_first_single() -> None:
15
+ reg = ModelRegistry([A])
16
+ assert reg.first() == A
17
+
18
+
19
+ def test_last_single() -> None:
20
+ reg = ModelRegistry([A])
21
+ assert reg.last() == A
22
+
23
+
24
+ def test_first_last_same_for_single_element() -> None:
25
+ reg = ModelRegistry([A])
26
+ assert reg.first() == reg.last()
27
+
28
+
29
+ def test_first_multi() -> None:
30
+ reg = ModelRegistry([A, B, C])
31
+ assert reg.first() == A
32
+
33
+
34
+ def test_last_multi() -> None:
35
+ reg = ModelRegistry([A, B, C])
36
+ assert reg.last() == C
37
+
38
+
39
+ def test_first_empty_raises() -> None:
40
+ reg = ModelRegistry()
41
+ with pytest.raises(IndexError, match="ModelRegistry is empty"):
42
+ reg.first()
43
+
44
+
45
+ def test_last_empty_raises() -> None:
46
+ reg = ModelRegistry()
47
+ with pytest.raises(IndexError, match="ModelRegistry is empty"):
48
+ reg.last()
49
+
50
+
51
+ def test_first_after_by_cost() -> None:
52
+ reg = ModelRegistry([C, A, B])
53
+ cheapest = reg.by_cost()
54
+ assert cheapest.first() == A
55
+
56
+
57
+ def test_last_after_by_cost() -> None:
58
+ reg = ModelRegistry([C, A, B])
59
+ priciest = reg.by_cost(desc=True)
60
+ assert priciest.first() == C
61
+ assert priciest.last() == A
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