gumloop 0.3.4__tar.gz → 0.3.5__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.
- {gumloop-0.3.4 → gumloop-0.3.5}/PKG-INFO +1 -1
- {gumloop-0.3.4 → gumloop-0.3.5}/pyproject.toml +1 -1
- {gumloop-0.3.4 → gumloop-0.3.5}/src/gumloop/__init__.py +1 -1
- gumloop-0.3.5/src/gumloop/_auth.py +35 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/src/gumloop/_client.py +3 -2
- {gumloop-0.3.4 → gumloop-0.3.5}/src/gumloop/_http.py +9 -18
- {gumloop-0.3.4 → gumloop-0.3.5}/tests/sdk/test_client.py +69 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/uv.lock +1 -1
- {gumloop-0.3.4 → gumloop-0.3.5}/.env.example +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/.gitignore +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/.pre-commit-config.yaml +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/.python-version +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/LICENSE +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/README.md +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/scripts/check +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/scripts/fmt +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/scripts/sdk_examples.py +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/src/gumloop/cli/__init__.py +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/src/gumloop/cli/commands/__init__.py +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/src/gumloop/cli/commands/_args_input.py +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/src/gumloop/cli/commands/_downloads.py +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/src/gumloop/cli/commands/agents.py +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/src/gumloop/cli/commands/artifacts.py +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/src/gumloop/cli/commands/auth.py +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/src/gumloop/cli/commands/brain.py +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/src/gumloop/cli/commands/chat.py +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/src/gumloop/cli/commands/mcp.py +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/src/gumloop/cli/commands/sessions.py +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/src/gumloop/cli/commands/skills.py +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/src/gumloop/cli/console.py +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/src/gumloop/cli/context.py +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/src/gumloop/cli/credentials.py +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/src/gumloop/cli/errors.py +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/src/gumloop/cli/main.py +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/src/gumloop/cli/oauth.py +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/src/gumloop/errors.py +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/src/gumloop/oauth.py +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/src/gumloop/py.typed +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/src/gumloop/resources/__init__.py +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/src/gumloop/resources/agents.py +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/src/gumloop/resources/artifacts.py +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/src/gumloop/resources/brain.py +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/src/gumloop/resources/chat.py +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/src/gumloop/resources/mcp.py +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/src/gumloop/resources/sessions.py +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/src/gumloop/resources/skills.py +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/src/gumloop/resources/teams.py +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/src/gumloop/spec/__init__.py +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/src/gumloop/spec/_compat.py +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/src/gumloop/spec/_extensions.py +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/src/gumloop/types.py +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/tests/__init__.py +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/tests/cli/__init__.py +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/tests/cli/conftest.py +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/tests/cli/test_agents.py +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/tests/cli/test_args_input.py +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/tests/cli/test_artifacts.py +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/tests/cli/test_brain.py +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/tests/cli/test_chat.py +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/tests/cli/test_context.py +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/tests/cli/test_credentials.py +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/tests/cli/test_login.py +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/tests/cli/test_main.py +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/tests/cli/test_mcp.py +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/tests/cli/test_sessions.py +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/tests/cli/test_skills.py +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/tests/conftest.py +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/tests/integration/conftest.py +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/tests/integration/test_live.py +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/tests/sdk/__init__.py +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/tests/sdk/conftest.py +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/tests/sdk/helpers.py +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/tests/sdk/test_agents.py +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/tests/sdk/test_artifacts.py +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/tests/sdk/test_brain.py +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/tests/sdk/test_chat.py +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/tests/sdk/test_mcp.py +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/tests/sdk/test_oauth.py +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/tests/sdk/test_sessions.py +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/tests/sdk/test_skills.py +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/tests/sdk/test_teams.py +0 -0
- {gumloop-0.3.4 → gumloop-0.3.5}/tests/test_client.py +0 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Credential resolution shared by the sync and async transports."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
from gumloop.errors import AuthenticationError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class EnvToken(str):
|
|
11
|
+
"""Access token sourced from ``GUMLOOP_ACCESS_TOKEN``.
|
|
12
|
+
|
|
13
|
+
Stays live: ``current()`` returns the env var's present value, so
|
|
14
|
+
platform-rotated tokens (e.g. per-call sandbox credentials) are picked up
|
|
15
|
+
on every request. Explicit tokens are plain strings and never change.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
@classmethod
|
|
19
|
+
def from_env(cls) -> EnvToken | None:
|
|
20
|
+
token = os.environ.get("GUMLOOP_ACCESS_TOKEN")
|
|
21
|
+
return cls(token) if token else None
|
|
22
|
+
|
|
23
|
+
def current(self) -> str:
|
|
24
|
+
return os.environ.get("GUMLOOP_ACCESS_TOKEN") or str(self)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def auth_headers(access_token: str | None, user_id: str | None) -> dict[str, str]:
|
|
28
|
+
if isinstance(access_token, EnvToken):
|
|
29
|
+
access_token = access_token.current()
|
|
30
|
+
if not access_token:
|
|
31
|
+
raise AuthenticationError("access_token, api_key, or GUMLOOP_ACCESS_TOKEN is required")
|
|
32
|
+
headers = {"Authorization": f"Bearer {access_token}"}
|
|
33
|
+
if user_id:
|
|
34
|
+
headers["x-auth-key"] = user_id
|
|
35
|
+
return headers
|
|
@@ -7,6 +7,7 @@ from typing import Any
|
|
|
7
7
|
|
|
8
8
|
import httpx
|
|
9
9
|
|
|
10
|
+
from gumloop._auth import EnvToken
|
|
10
11
|
from gumloop._http import AsyncHttpClient
|
|
11
12
|
from gumloop._http import HttpClient
|
|
12
13
|
from gumloop.oauth import OAuth
|
|
@@ -59,7 +60,7 @@ class Gumloop:
|
|
|
59
60
|
stream_timeout: float | None = DEFAULT_STREAM_TIMEOUT,
|
|
60
61
|
) -> None:
|
|
61
62
|
self.api_key = api_key
|
|
62
|
-
self.access_token = access_token or api_key or
|
|
63
|
+
self.access_token = access_token or api_key or EnvToken.from_env()
|
|
63
64
|
# Personal Gumloop API keys are user-scoped; the backend uses the
|
|
64
65
|
# x-auth-key header to look up the owner's stored secret. OAuth
|
|
65
66
|
# callers omit it because the bearer token already identifies them.
|
|
@@ -115,7 +116,7 @@ class AsyncGumloop:
|
|
|
115
116
|
stream_timeout: float | None = DEFAULT_STREAM_TIMEOUT,
|
|
116
117
|
) -> None:
|
|
117
118
|
self.api_key = api_key
|
|
118
|
-
self.access_token = access_token or api_key or
|
|
119
|
+
self.access_token = access_token or api_key or EnvToken.from_env()
|
|
119
120
|
self.user_id = user_id or os.environ.get("GUMLOOP_USER_ID")
|
|
120
121
|
# Defaults to production; GUMLOOP_BASE_URL is an optional override.
|
|
121
122
|
self.base_url = (base_url or os.environ.get("GUMLOOP_BASE_URL") or DEFAULT_BASE_URL).rstrip("/")
|
|
@@ -13,7 +13,7 @@ from httpx_sse import ServerSentEvent
|
|
|
13
13
|
from pydantic import BaseModel
|
|
14
14
|
from pydantic import ValidationError
|
|
15
15
|
|
|
16
|
-
from gumloop.
|
|
16
|
+
from gumloop._auth import auth_headers
|
|
17
17
|
from gumloop.errors import to_api_error
|
|
18
18
|
from gumloop.types import StreamEvent
|
|
19
19
|
|
|
@@ -23,15 +23,6 @@ _DONE_SENTINEL = "[DONE]"
|
|
|
23
23
|
_T = TypeVar("_T", bound=BaseModel)
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
def _auth_headers(access_token: str | None, user_id: str | None) -> dict[str, str]:
|
|
27
|
-
if not access_token:
|
|
28
|
-
raise AuthenticationError("access_token, api_key, or GUMLOOP_ACCESS_TOKEN is required")
|
|
29
|
-
headers = {"Authorization": f"Bearer {access_token}"}
|
|
30
|
-
if user_id:
|
|
31
|
-
headers["x-auth-key"] = user_id
|
|
32
|
-
return headers
|
|
33
|
-
|
|
34
|
-
|
|
35
26
|
def _omit_none_params(params: Mapping[str, Any] | None) -> dict[str, Any] | None:
|
|
36
27
|
# Backend treats absent ``?foo`` and ``?foo=`` as different signals
|
|
37
28
|
# ("not provided" vs "empty string"); drop None values so the wire URL
|
|
@@ -120,7 +111,7 @@ class HttpClient:
|
|
|
120
111
|
# Endpoints whose streaming variant lives on the stream host (e.g. chat
|
|
121
112
|
# completions) must accept their unary counterparts at the same host —
|
|
122
113
|
# the api host has no handler for them.
|
|
123
|
-
headers =
|
|
114
|
+
headers = auth_headers(self.access_token, self.user_id)
|
|
124
115
|
headers["Content-Type"] = "application/json"
|
|
125
116
|
response = self._client.post(
|
|
126
117
|
f"{self._stream_base_url}/{path.lstrip('/')}",
|
|
@@ -140,7 +131,7 @@ class HttpClient:
|
|
|
140
131
|
json: Any = None,
|
|
141
132
|
params: Mapping[str, Any] | None = None,
|
|
142
133
|
) -> Iterator[StreamEvent]:
|
|
143
|
-
headers = {**
|
|
134
|
+
headers = {**auth_headers(self.access_token, self.user_id), "Accept": "text/event-stream"}
|
|
144
135
|
with self._client.stream(
|
|
145
136
|
method,
|
|
146
137
|
f"{self._stream_base_url}/{path.lstrip('/')}",
|
|
@@ -166,7 +157,7 @@ class HttpClient:
|
|
|
166
157
|
) -> Iterator[_T]:
|
|
167
158
|
# Skips the StreamEvent envelope and honors OpenRouter's `data: [DONE]`
|
|
168
159
|
# terminator. Unparseable events (keep-alives, comments) are skipped.
|
|
169
|
-
headers = {**
|
|
160
|
+
headers = {**auth_headers(self.access_token, self.user_id), "Accept": "text/event-stream"}
|
|
170
161
|
with self._client.stream(
|
|
171
162
|
method,
|
|
172
163
|
f"{self._stream_base_url}/{path.lstrip('/')}",
|
|
@@ -194,7 +185,7 @@ class HttpClient:
|
|
|
194
185
|
def _request(self, method: str, path: str, **kwargs: Any) -> Any:
|
|
195
186
|
# Headers are rebuilt per request so ``access_token`` / ``user_id``
|
|
196
187
|
# can be rotated on a live client without reconstructing it.
|
|
197
|
-
headers =
|
|
188
|
+
headers = auth_headers(self.access_token, self.user_id)
|
|
198
189
|
if not kwargs.get("files"):
|
|
199
190
|
headers["Content-Type"] = "application/json"
|
|
200
191
|
response = self._client.request(method, path, headers=headers, **kwargs)
|
|
@@ -261,7 +252,7 @@ class AsyncHttpClient:
|
|
|
261
252
|
return await self._request("DELETE", path)
|
|
262
253
|
|
|
263
254
|
async def post_to_stream_host(self, path: str, *, json: Any = None) -> Any:
|
|
264
|
-
headers =
|
|
255
|
+
headers = auth_headers(self.access_token, self.user_id)
|
|
265
256
|
headers["Content-Type"] = "application/json"
|
|
266
257
|
response = await self._client.post(
|
|
267
258
|
f"{self._stream_base_url}/{path.lstrip('/')}",
|
|
@@ -281,7 +272,7 @@ class AsyncHttpClient:
|
|
|
281
272
|
json: Any = None,
|
|
282
273
|
params: Mapping[str, Any] | None = None,
|
|
283
274
|
) -> AsyncIterator[StreamEvent]:
|
|
284
|
-
headers = {**
|
|
275
|
+
headers = {**auth_headers(self.access_token, self.user_id), "Accept": "text/event-stream"}
|
|
285
276
|
async with self._client.stream(
|
|
286
277
|
method,
|
|
287
278
|
f"{self._stream_base_url}/{path.lstrip('/')}",
|
|
@@ -305,7 +296,7 @@ class AsyncHttpClient:
|
|
|
305
296
|
json: Any = None,
|
|
306
297
|
params: Mapping[str, Any] | None = None,
|
|
307
298
|
) -> AsyncIterator[_T]:
|
|
308
|
-
headers = {**
|
|
299
|
+
headers = {**auth_headers(self.access_token, self.user_id), "Accept": "text/event-stream"}
|
|
309
300
|
async with self._client.stream(
|
|
310
301
|
method,
|
|
311
302
|
f"{self._stream_base_url}/{path.lstrip('/')}",
|
|
@@ -331,7 +322,7 @@ class AsyncHttpClient:
|
|
|
331
322
|
continue
|
|
332
323
|
|
|
333
324
|
async def _request(self, method: str, path: str, **kwargs: Any) -> Any:
|
|
334
|
-
headers =
|
|
325
|
+
headers = auth_headers(self.access_token, self.user_id)
|
|
335
326
|
if not kwargs.get("files"):
|
|
336
327
|
headers["Content-Type"] = "application/json"
|
|
337
328
|
response = await self._client.request(method, path, headers=headers, **kwargs)
|
|
@@ -60,6 +60,75 @@ def test_missing_credential_raises_clear_auth_error(monkeypatch: pytest.MonkeyPa
|
|
|
60
60
|
Gumloop().models.list()
|
|
61
61
|
|
|
62
62
|
|
|
63
|
+
@respx.mock
|
|
64
|
+
def test_env_sourced_token_follows_env_rotation(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
65
|
+
"""A client built from GUMLOOP_ACCESS_TOKEN must send the CURRENT env
|
|
66
|
+
value per request: platforms (e.g. the agent sandbox) rotate the token
|
|
67
|
+
between requests on a live client."""
|
|
68
|
+
monkeypatch.setenv("GUMLOOP_ACCESS_TOKEN", "tok-1")
|
|
69
|
+
route = respx.get(f"{API_BASE}/models").mock(return_value=httpx.Response(200, json={"model_groups": []}))
|
|
70
|
+
client = Gumloop()
|
|
71
|
+
|
|
72
|
+
client.models.list()
|
|
73
|
+
monkeypatch.setenv("GUMLOOP_ACCESS_TOKEN", "tok-2")
|
|
74
|
+
client.models.list()
|
|
75
|
+
|
|
76
|
+
assert auth_header(route.calls[0].request) == "Bearer tok-1"
|
|
77
|
+
assert auth_header(route.calls[1].request) == "Bearer tok-2"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@respx.mock
|
|
81
|
+
def test_env_sourced_token_falls_back_to_snapshot_when_env_cleared(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
82
|
+
monkeypatch.setenv("GUMLOOP_ACCESS_TOKEN", "tok-1")
|
|
83
|
+
route = respx.get(f"{API_BASE}/models").mock(return_value=httpx.Response(200, json={"model_groups": []}))
|
|
84
|
+
client = Gumloop()
|
|
85
|
+
|
|
86
|
+
monkeypatch.delenv("GUMLOOP_ACCESS_TOKEN")
|
|
87
|
+
client.models.list()
|
|
88
|
+
|
|
89
|
+
assert auth_header(route.calls[0].request) == "Bearer tok-1"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@pytest.mark.parametrize(
|
|
93
|
+
"kwargs, expected_header",
|
|
94
|
+
[
|
|
95
|
+
({"access_token": "explicit-tok"}, "Bearer explicit-tok"),
|
|
96
|
+
({"api_key": "gum_xxx"}, "Bearer gum_xxx"),
|
|
97
|
+
],
|
|
98
|
+
)
|
|
99
|
+
@respx.mock
|
|
100
|
+
def test_explicit_credential_ignores_env_rotation(
|
|
101
|
+
monkeypatch: pytest.MonkeyPatch, kwargs: dict[str, str], expected_header: str
|
|
102
|
+
) -> None:
|
|
103
|
+
"""Explicitly passed credentials are immutable — a env var appearing or
|
|
104
|
+
rotating later must never hijack the client's identity."""
|
|
105
|
+
monkeypatch.setenv("GUMLOOP_ACCESS_TOKEN", "env-tok")
|
|
106
|
+
route = respx.get(f"{API_BASE}/models").mock(return_value=httpx.Response(200, json={"model_groups": []}))
|
|
107
|
+
client = Gumloop(**kwargs)
|
|
108
|
+
|
|
109
|
+
monkeypatch.setenv("GUMLOOP_ACCESS_TOKEN", "env-tok-2")
|
|
110
|
+
client.models.list()
|
|
111
|
+
|
|
112
|
+
assert auth_header(route.calls[0].request) == expected_header
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@respx.mock
|
|
116
|
+
def test_async_env_sourced_token_follows_env_rotation(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
117
|
+
monkeypatch.setenv("GUMLOOP_ACCESS_TOKEN", "tok-1")
|
|
118
|
+
route = respx.get(f"{API_BASE}/models").mock(return_value=httpx.Response(200, json={"model_groups": []}))
|
|
119
|
+
|
|
120
|
+
async def run() -> None:
|
|
121
|
+
async with AsyncGumloop() as client:
|
|
122
|
+
await client.models.list()
|
|
123
|
+
monkeypatch.setenv("GUMLOOP_ACCESS_TOKEN", "tok-2")
|
|
124
|
+
await client.models.list()
|
|
125
|
+
|
|
126
|
+
asyncio.run(run())
|
|
127
|
+
|
|
128
|
+
assert auth_header(route.calls[0].request) == "Bearer tok-1"
|
|
129
|
+
assert auth_header(route.calls[1].request) == "Bearer tok-2"
|
|
130
|
+
|
|
131
|
+
|
|
63
132
|
@respx.mock
|
|
64
133
|
def test_non_success_response_raises_gumloop_status_error(client: Gumloop) -> None:
|
|
65
134
|
error = {
|
|
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
|
|
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
|