fleet-python 0.2.94__tar.gz → 0.2.96__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.
- {fleet_python-0.2.94/fleet_python.egg-info → fleet_python-0.2.96}/PKG-INFO +1 -1
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/__init__.py +1 -1
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/_async/__init__.py +1 -1
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/_async/base.py +1 -1
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/_async/client.py +19 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/_async/instance/client.py +26 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/_async/models.py +1 -0
- fleet_python-0.2.96/fleet/_async/resources/api.py +200 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/_async/resources/sqlite.py +29 -3
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/base.py +1 -1
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/client.py +19 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/instance/client.py +26 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/instance/models.py +1 -0
- fleet_python-0.2.96/fleet/resources/api.py +200 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/resources/sqlite.py +29 -3
- {fleet_python-0.2.94 → fleet_python-0.2.96/fleet_python.egg-info}/PKG-INFO +1 -1
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet_python.egg-info/SOURCES.txt +2 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/pyproject.toml +1 -1
- {fleet_python-0.2.94 → fleet_python-0.2.96}/LICENSE +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/README.md +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/examples/diff_example.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/examples/dsl_example.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/examples/example.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/examples/exampleResume.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/examples/example_account.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/examples/example_action_log.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/examples/example_client.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/examples/example_mcp_anthropic.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/examples/example_mcp_openai.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/examples/example_sync.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/examples/example_task.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/examples/example_tasks.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/examples/example_verifier.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/examples/export_tasks.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/examples/fetch_tasks.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/examples/gemini_example.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/examples/import_tasks.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/examples/iterate_verifiers.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/examples/json_tasks_example.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/examples/nova_act_example.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/examples/openai_example.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/examples/openai_simple_example.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/examples/query_builder_example.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/examples/quickstart.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/examples/test_cdp_logging.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/_async/env/__init__.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/_async/env/client.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/_async/exceptions.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/_async/global_client.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/_async/instance/__init__.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/_async/instance/base.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/_async/resources/__init__.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/_async/resources/base.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/_async/resources/browser.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/_async/resources/mcp.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/_async/tasks.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/_async/verifiers/__init__.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/_async/verifiers/bundler.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/_async/verifiers/verifier.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/agent/__init__.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/agent/gemini_cua/Dockerfile +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/agent/gemini_cua/__init__.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/agent/gemini_cua/agent.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/agent/gemini_cua/mcp/main.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/agent/gemini_cua/mcp_server/__init__.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/agent/gemini_cua/mcp_server/main.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/agent/gemini_cua/mcp_server/tools.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/agent/gemini_cua/requirements.txt +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/agent/gemini_cua/start.sh +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/agent/orchestrator.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/agent/types.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/agent/utils.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/cli.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/config.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/env/__init__.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/env/client.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/eval/__init__.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/eval/uploader.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/exceptions.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/global_client.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/instance/__init__.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/instance/base.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/models.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/proxy/__init__.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/proxy/proxy.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/proxy/whitelist.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/resources/__init__.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/resources/base.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/resources/browser.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/resources/mcp.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/tasks.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/types.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/utils/__init__.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/utils/http_logging.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/utils/logging.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/utils/playwright.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/verifiers/__init__.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/verifiers/bundler.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/verifiers/code.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/verifiers/db.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/verifiers/decorator.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/verifiers/parse.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/verifiers/sql_differ.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet/verifiers/verifier.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet_python.egg-info/dependency_links.txt +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet_python.egg-info/entry_points.txt +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet_python.egg-info/requires.txt +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/fleet_python.egg-info/top_level.txt +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/scripts/fix_sync_imports.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/scripts/unasync.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/setup.cfg +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/tests/__init__.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/tests/test_app_method.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/tests/test_expect_only.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/tests/test_instance_dispatch.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/tests/test_sqlite_resource_dual_mode.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/tests/test_sqlite_shared_memory_behavior.py +0 -0
- {fleet_python-0.2.94 → fleet_python-0.2.96}/tests/test_verifier_from_string.py +0 -0
|
@@ -172,6 +172,7 @@ from .resources.base import Resource
|
|
|
172
172
|
from .resources.sqlite import AsyncSQLiteResource
|
|
173
173
|
from .resources.browser import AsyncBrowserResource
|
|
174
174
|
from .resources.mcp import AsyncMCPResource
|
|
175
|
+
from .resources.api import AsyncAPIResource
|
|
175
176
|
|
|
176
177
|
logger = logging.getLogger(__name__)
|
|
177
178
|
|
|
@@ -385,6 +386,24 @@ class AsyncEnv(EnvironmentBase):
|
|
|
385
386
|
def browser(self, name: str = "cdp") -> AsyncBrowserResource:
|
|
386
387
|
return self.instance.browser(name)
|
|
387
388
|
|
|
389
|
+
def api(self, name: str = "api") -> AsyncAPIResource:
|
|
390
|
+
"""Get an API resource for making HTTP requests to the app's API.
|
|
391
|
+
|
|
392
|
+
Args:
|
|
393
|
+
name: Name for the API resource (default: "api")
|
|
394
|
+
|
|
395
|
+
Returns:
|
|
396
|
+
AsyncAPIResource for making HTTP requests
|
|
397
|
+
"""
|
|
398
|
+
# Use urls.api if available, otherwise fall back to urls.root + "/raw"
|
|
399
|
+
if self.urls and self.urls.api:
|
|
400
|
+
base_url = self.urls.api
|
|
401
|
+
elif self.urls and self.urls.root:
|
|
402
|
+
base_url = f"{self.urls.root.rstrip('/')}/raw"
|
|
403
|
+
else:
|
|
404
|
+
raise ValueError("No API URL configured for this environment")
|
|
405
|
+
return self.instance.api(name, base_url)
|
|
406
|
+
|
|
388
407
|
@property
|
|
389
408
|
def mcp(self) -> AsyncMCPResource:
|
|
390
409
|
mcp_url = f"{self.urls.root}mcp"
|
|
@@ -9,6 +9,7 @@ from urllib.parse import urlparse
|
|
|
9
9
|
|
|
10
10
|
from ..resources.sqlite import AsyncSQLiteResource
|
|
11
11
|
from ..resources.browser import AsyncBrowserResource
|
|
12
|
+
from ..resources.api import AsyncAPIResource
|
|
12
13
|
from ..resources.base import Resource
|
|
13
14
|
|
|
14
15
|
from fleet.verifiers import DatabaseSnapshot
|
|
@@ -23,6 +24,7 @@ from ...instance.models import (
|
|
|
23
24
|
ResetResponse,
|
|
24
25
|
Resource as ResourceModel,
|
|
25
26
|
ResourceType,
|
|
27
|
+
ResourceMode,
|
|
26
28
|
HealthResponse,
|
|
27
29
|
ExecuteFunctionRequest,
|
|
28
30
|
ExecuteFunctionResponse,
|
|
@@ -35,6 +37,7 @@ logger = logging.getLogger(__name__)
|
|
|
35
37
|
RESOURCE_TYPES = {
|
|
36
38
|
ResourceType.db: AsyncSQLiteResource,
|
|
37
39
|
ResourceType.cdp: AsyncBrowserResource,
|
|
40
|
+
ResourceType.api: AsyncAPIResource,
|
|
38
41
|
}
|
|
39
42
|
|
|
40
43
|
ValidatorType = Callable[
|
|
@@ -102,6 +105,29 @@ class AsyncInstanceClient:
|
|
|
102
105
|
self._resources_state[ResourceType.cdp.value][name], self.client
|
|
103
106
|
)
|
|
104
107
|
|
|
108
|
+
def api(self, name: str, base_url: str) -> AsyncAPIResource:
|
|
109
|
+
"""
|
|
110
|
+
Returns an API resource for making HTTP requests.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
name: The name of the API resource
|
|
114
|
+
base_url: The base URL for API requests
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
An AsyncAPIResource for making HTTP requests
|
|
118
|
+
"""
|
|
119
|
+
# Create a minimal resource model for API
|
|
120
|
+
resource_model = ResourceModel(
|
|
121
|
+
name=name,
|
|
122
|
+
type=ResourceType.api,
|
|
123
|
+
mode=ResourceMode.rw,
|
|
124
|
+
)
|
|
125
|
+
return AsyncAPIResource(
|
|
126
|
+
resource_model,
|
|
127
|
+
base_url=base_url,
|
|
128
|
+
client=self.client.httpx_client if self.client else None,
|
|
129
|
+
)
|
|
130
|
+
|
|
105
131
|
async def resources(self) -> List[Resource]:
|
|
106
132
|
await self._load_resources()
|
|
107
133
|
return [
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""Async API Resource for making HTTP requests to the app's API endpoint."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Optional
|
|
4
|
+
import httpx
|
|
5
|
+
|
|
6
|
+
from .base import Resource
|
|
7
|
+
from ...instance.models import Resource as ResourceModel
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AsyncAPIResponse:
|
|
11
|
+
"""Simple wrapper around httpx.Response with a requests-like interface."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, response: httpx.Response):
|
|
14
|
+
self._response = response
|
|
15
|
+
self.status_code: int = response.status_code
|
|
16
|
+
self.headers: httpx.Headers = response.headers
|
|
17
|
+
self.text: str = response.text
|
|
18
|
+
self.content: bytes = response.content
|
|
19
|
+
self.url: str = str(response.url)
|
|
20
|
+
self.ok: bool = response.is_success
|
|
21
|
+
|
|
22
|
+
def json(self) -> Any:
|
|
23
|
+
"""Parse response body as JSON."""
|
|
24
|
+
return self._response.json()
|
|
25
|
+
|
|
26
|
+
def raise_for_status(self) -> "AsyncAPIResponse":
|
|
27
|
+
"""Raise an HTTPStatusError if the response has an error status code."""
|
|
28
|
+
self._response.raise_for_status()
|
|
29
|
+
return self
|
|
30
|
+
|
|
31
|
+
def __repr__(self) -> str:
|
|
32
|
+
return f"<AsyncAPIResponse [{self.status_code}]>"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class AsyncAPIResource(Resource):
|
|
36
|
+
"""Async HTTP client for making requests to the app's API endpoint.
|
|
37
|
+
|
|
38
|
+
Provides a requests-like interface for interacting with the app's REST API.
|
|
39
|
+
|
|
40
|
+
Example:
|
|
41
|
+
api = env.api()
|
|
42
|
+
response = await api.get("/users/1")
|
|
43
|
+
print(response.status_code) # 200
|
|
44
|
+
print(response.json()) # {"id": 1, "name": "John"}
|
|
45
|
+
|
|
46
|
+
# With headers/auth
|
|
47
|
+
response = await api.post(
|
|
48
|
+
"/users",
|
|
49
|
+
json={"name": "Jane"},
|
|
50
|
+
headers={"Authorization": "Bearer xxx"}
|
|
51
|
+
)
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
resource: ResourceModel,
|
|
57
|
+
base_url: str,
|
|
58
|
+
client: Optional[httpx.AsyncClient] = None,
|
|
59
|
+
):
|
|
60
|
+
super().__init__(resource)
|
|
61
|
+
self.base_url = base_url.rstrip("/")
|
|
62
|
+
self._client = client or httpx.AsyncClient()
|
|
63
|
+
|
|
64
|
+
def _build_url(self, path: str) -> str:
|
|
65
|
+
"""Build full URL from base_url and path."""
|
|
66
|
+
if path.startswith("/"):
|
|
67
|
+
return f"{self.base_url}{path}"
|
|
68
|
+
return f"{self.base_url}/{path}"
|
|
69
|
+
|
|
70
|
+
async def request(
|
|
71
|
+
self,
|
|
72
|
+
method: str,
|
|
73
|
+
path: str,
|
|
74
|
+
*,
|
|
75
|
+
params: Optional[Dict[str, Any]] = None,
|
|
76
|
+
json: Optional[Any] = None,
|
|
77
|
+
data: Optional[Dict[str, Any]] = None,
|
|
78
|
+
headers: Optional[Dict[str, str]] = None,
|
|
79
|
+
cookies: Optional[Dict[str, str]] = None,
|
|
80
|
+
timeout: Optional[float] = None,
|
|
81
|
+
**kwargs: Any,
|
|
82
|
+
) -> AsyncAPIResponse:
|
|
83
|
+
"""Make an HTTP request.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
method: HTTP method (GET, POST, PUT, PATCH, DELETE, etc.)
|
|
87
|
+
path: URL path (relative to base_url)
|
|
88
|
+
params: Query parameters
|
|
89
|
+
json: JSON body (will be serialized)
|
|
90
|
+
data: Form data
|
|
91
|
+
headers: Request headers
|
|
92
|
+
cookies: Request cookies
|
|
93
|
+
timeout: Request timeout in seconds
|
|
94
|
+
**kwargs: Additional arguments passed to httpx
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
AsyncAPIResponse with status_code, headers, text, content, json() method
|
|
98
|
+
"""
|
|
99
|
+
url = self._build_url(path)
|
|
100
|
+
response = await self._client.request(
|
|
101
|
+
method,
|
|
102
|
+
url,
|
|
103
|
+
params=params,
|
|
104
|
+
json=json,
|
|
105
|
+
data=data,
|
|
106
|
+
headers=headers,
|
|
107
|
+
cookies=cookies,
|
|
108
|
+
timeout=timeout,
|
|
109
|
+
**kwargs,
|
|
110
|
+
)
|
|
111
|
+
return AsyncAPIResponse(response)
|
|
112
|
+
|
|
113
|
+
async def get(
|
|
114
|
+
self,
|
|
115
|
+
path: str,
|
|
116
|
+
*,
|
|
117
|
+
params: Optional[Dict[str, Any]] = None,
|
|
118
|
+
headers: Optional[Dict[str, str]] = None,
|
|
119
|
+
**kwargs: Any,
|
|
120
|
+
) -> AsyncAPIResponse:
|
|
121
|
+
"""Make a GET request."""
|
|
122
|
+
return await self.request("GET", path, params=params, headers=headers, **kwargs)
|
|
123
|
+
|
|
124
|
+
async def post(
|
|
125
|
+
self,
|
|
126
|
+
path: str,
|
|
127
|
+
*,
|
|
128
|
+
json: Optional[Any] = None,
|
|
129
|
+
data: Optional[Dict[str, Any]] = None,
|
|
130
|
+
params: Optional[Dict[str, Any]] = None,
|
|
131
|
+
headers: Optional[Dict[str, str]] = None,
|
|
132
|
+
**kwargs: Any,
|
|
133
|
+
) -> AsyncAPIResponse:
|
|
134
|
+
"""Make a POST request."""
|
|
135
|
+
return await self.request(
|
|
136
|
+
"POST", path, json=json, data=data, params=params, headers=headers, **kwargs
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
async def put(
|
|
140
|
+
self,
|
|
141
|
+
path: str,
|
|
142
|
+
*,
|
|
143
|
+
json: Optional[Any] = None,
|
|
144
|
+
data: Optional[Dict[str, Any]] = None,
|
|
145
|
+
params: Optional[Dict[str, Any]] = None,
|
|
146
|
+
headers: Optional[Dict[str, str]] = None,
|
|
147
|
+
**kwargs: Any,
|
|
148
|
+
) -> AsyncAPIResponse:
|
|
149
|
+
"""Make a PUT request."""
|
|
150
|
+
return await self.request(
|
|
151
|
+
"PUT", path, json=json, data=data, params=params, headers=headers, **kwargs
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
async def patch(
|
|
155
|
+
self,
|
|
156
|
+
path: str,
|
|
157
|
+
*,
|
|
158
|
+
json: Optional[Any] = None,
|
|
159
|
+
data: Optional[Dict[str, Any]] = None,
|
|
160
|
+
params: Optional[Dict[str, Any]] = None,
|
|
161
|
+
headers: Optional[Dict[str, str]] = None,
|
|
162
|
+
**kwargs: Any,
|
|
163
|
+
) -> AsyncAPIResponse:
|
|
164
|
+
"""Make a PATCH request."""
|
|
165
|
+
return await self.request(
|
|
166
|
+
"PATCH", path, json=json, data=data, params=params, headers=headers, **kwargs
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
async def delete(
|
|
170
|
+
self,
|
|
171
|
+
path: str,
|
|
172
|
+
*,
|
|
173
|
+
params: Optional[Dict[str, Any]] = None,
|
|
174
|
+
headers: Optional[Dict[str, str]] = None,
|
|
175
|
+
**kwargs: Any,
|
|
176
|
+
) -> AsyncAPIResponse:
|
|
177
|
+
"""Make a DELETE request."""
|
|
178
|
+
return await self.request("DELETE", path, params=params, headers=headers, **kwargs)
|
|
179
|
+
|
|
180
|
+
async def head(
|
|
181
|
+
self,
|
|
182
|
+
path: str,
|
|
183
|
+
*,
|
|
184
|
+
params: Optional[Dict[str, Any]] = None,
|
|
185
|
+
headers: Optional[Dict[str, str]] = None,
|
|
186
|
+
**kwargs: Any,
|
|
187
|
+
) -> AsyncAPIResponse:
|
|
188
|
+
"""Make a HEAD request."""
|
|
189
|
+
return await self.request("HEAD", path, params=params, headers=headers, **kwargs)
|
|
190
|
+
|
|
191
|
+
async def options(
|
|
192
|
+
self,
|
|
193
|
+
path: str,
|
|
194
|
+
*,
|
|
195
|
+
params: Optional[Dict[str, Any]] = None,
|
|
196
|
+
headers: Optional[Dict[str, str]] = None,
|
|
197
|
+
**kwargs: Any,
|
|
198
|
+
) -> AsyncAPIResponse:
|
|
199
|
+
"""Make an OPTIONS request."""
|
|
200
|
+
return await self.request("OPTIONS", path, params=params, headers=headers, **kwargs)
|
|
@@ -1417,9 +1417,35 @@ class AsyncSnapshotDiff:
|
|
|
1417
1417
|
This version supports field-level specifications for added/removed rows,
|
|
1418
1418
|
allowing users to specify expected field values instead of just whole-row specs.
|
|
1419
1419
|
"""
|
|
1420
|
-
|
|
1421
|
-
if not
|
|
1422
|
-
|
|
1420
|
+
resource = self.after.resource
|
|
1421
|
+
if resource.client is not None and resource._mode == "http":
|
|
1422
|
+
api_diff = None
|
|
1423
|
+
try:
|
|
1424
|
+
payload = {}
|
|
1425
|
+
if self.ignore_config:
|
|
1426
|
+
payload["ignore_config"] = {
|
|
1427
|
+
"tables": list(self.ignore_config.tables),
|
|
1428
|
+
"fields": list(self.ignore_config.fields),
|
|
1429
|
+
"table_fields": {
|
|
1430
|
+
table: list(fields) for table, fields in self.ignore_config.table_fields.items()
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
response = await resource.client.request(
|
|
1434
|
+
"POST",
|
|
1435
|
+
"/diff/structured",
|
|
1436
|
+
json=payload,
|
|
1437
|
+
)
|
|
1438
|
+
result = response.json()
|
|
1439
|
+
if result.get("success") and "diff" in result:
|
|
1440
|
+
api_diff = result["diff"]
|
|
1441
|
+
except Exception as e:
|
|
1442
|
+
# Fall back to local diff if API call fails
|
|
1443
|
+
print(f"Warning: Failed to fetch structured diff from API: {e}")
|
|
1444
|
+
print("Falling back to local diff computation...")
|
|
1445
|
+
|
|
1446
|
+
# Validate outside try block so AssertionError propagates
|
|
1447
|
+
if api_diff is not None:
|
|
1448
|
+
return await self._validate_diff_against_allowed_changes(api_diff, allowed_changes)
|
|
1423
1449
|
|
|
1424
1450
|
# Fall back to full diff for v2 (no targeted optimization yet)
|
|
1425
1451
|
diff = await self._collect()
|
|
@@ -178,6 +178,7 @@ from .resources.base import Resource
|
|
|
178
178
|
from .resources.sqlite import SQLiteResource
|
|
179
179
|
from .resources.browser import BrowserResource
|
|
180
180
|
from .resources.mcp import SyncMCPResource
|
|
181
|
+
from .resources.api import APIResource
|
|
181
182
|
|
|
182
183
|
logger = logging.getLogger(__name__)
|
|
183
184
|
|
|
@@ -397,6 +398,24 @@ class SyncEnv(EnvironmentBase):
|
|
|
397
398
|
def browser(self, name: str = "cdp") -> BrowserResource:
|
|
398
399
|
return self.instance.browser(name)
|
|
399
400
|
|
|
401
|
+
def api(self, name: str = "api") -> APIResource:
|
|
402
|
+
"""Get an API resource for making HTTP requests to the app's API.
|
|
403
|
+
|
|
404
|
+
Args:
|
|
405
|
+
name: Name for the API resource (default: "api")
|
|
406
|
+
|
|
407
|
+
Returns:
|
|
408
|
+
APIResource for making HTTP requests
|
|
409
|
+
"""
|
|
410
|
+
# Use urls.api if available, otherwise fall back to urls.root + "/raw"
|
|
411
|
+
if self.urls and self.urls.api:
|
|
412
|
+
base_url = self.urls.api
|
|
413
|
+
elif self.urls and self.urls.root:
|
|
414
|
+
base_url = f"{self.urls.root.rstrip('/')}/raw"
|
|
415
|
+
else:
|
|
416
|
+
raise ValueError("No API URL configured for this environment")
|
|
417
|
+
return self.instance.api(name, base_url)
|
|
418
|
+
|
|
400
419
|
@property
|
|
401
420
|
def mcp(self) -> SyncMCPResource:
|
|
402
421
|
mcp_url = f"{self.urls.root}mcp"
|
|
@@ -9,6 +9,7 @@ from urllib.parse import urlparse
|
|
|
9
9
|
|
|
10
10
|
from ..resources.sqlite import SQLiteResource
|
|
11
11
|
from ..resources.browser import BrowserResource
|
|
12
|
+
from ..resources.api import APIResource
|
|
12
13
|
from ..resources.base import Resource
|
|
13
14
|
|
|
14
15
|
from fleet.verifiers import DatabaseSnapshot
|
|
@@ -23,6 +24,7 @@ from .models import (
|
|
|
23
24
|
ResetResponse,
|
|
24
25
|
Resource as ResourceModel,
|
|
25
26
|
ResourceType,
|
|
27
|
+
ResourceMode,
|
|
26
28
|
HealthResponse,
|
|
27
29
|
ExecuteFunctionRequest,
|
|
28
30
|
ExecuteFunctionResponse,
|
|
@@ -35,6 +37,7 @@ logger = logging.getLogger(__name__)
|
|
|
35
37
|
RESOURCE_TYPES = {
|
|
36
38
|
ResourceType.db: SQLiteResource,
|
|
37
39
|
ResourceType.cdp: BrowserResource,
|
|
40
|
+
ResourceType.api: APIResource,
|
|
38
41
|
}
|
|
39
42
|
|
|
40
43
|
ValidatorType = Callable[
|
|
@@ -100,6 +103,29 @@ class InstanceClient:
|
|
|
100
103
|
self._resources_state[ResourceType.cdp.value][name], self.client
|
|
101
104
|
)
|
|
102
105
|
|
|
106
|
+
def api(self, name: str, base_url: str) -> APIResource:
|
|
107
|
+
"""
|
|
108
|
+
Returns an API resource for making HTTP requests.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
name: The name of the API resource
|
|
112
|
+
base_url: The base URL for API requests
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
An APIResource for making HTTP requests
|
|
116
|
+
"""
|
|
117
|
+
# Create a minimal resource model for API
|
|
118
|
+
resource_model = ResourceModel(
|
|
119
|
+
name=name,
|
|
120
|
+
type=ResourceType.api,
|
|
121
|
+
mode=ResourceMode.rw,
|
|
122
|
+
)
|
|
123
|
+
return APIResource(
|
|
124
|
+
resource_model,
|
|
125
|
+
base_url=base_url,
|
|
126
|
+
client=self.client.httpx_client if self.client else None,
|
|
127
|
+
)
|
|
128
|
+
|
|
103
129
|
def resources(self) -> List[Resource]:
|
|
104
130
|
self._load_resources()
|
|
105
131
|
return [
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""API Resource for making HTTP requests to the app's API endpoint."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Optional, Union
|
|
4
|
+
import httpx
|
|
5
|
+
|
|
6
|
+
from .base import Resource
|
|
7
|
+
from ..instance.models import Resource as ResourceModel
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class APIResponse:
|
|
11
|
+
"""Simple wrapper around httpx.Response with a requests-like interface."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, response: httpx.Response):
|
|
14
|
+
self._response = response
|
|
15
|
+
self.status_code: int = response.status_code
|
|
16
|
+
self.headers: httpx.Headers = response.headers
|
|
17
|
+
self.text: str = response.text
|
|
18
|
+
self.content: bytes = response.content
|
|
19
|
+
self.url: str = str(response.url)
|
|
20
|
+
self.ok: bool = response.is_success
|
|
21
|
+
|
|
22
|
+
def json(self) -> Any:
|
|
23
|
+
"""Parse response body as JSON."""
|
|
24
|
+
return self._response.json()
|
|
25
|
+
|
|
26
|
+
def raise_for_status(self) -> "APIResponse":
|
|
27
|
+
"""Raise an HTTPStatusError if the response has an error status code."""
|
|
28
|
+
self._response.raise_for_status()
|
|
29
|
+
return self
|
|
30
|
+
|
|
31
|
+
def __repr__(self) -> str:
|
|
32
|
+
return f"<APIResponse [{self.status_code}]>"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class APIResource(Resource):
|
|
36
|
+
"""HTTP client for making requests to the app's API endpoint.
|
|
37
|
+
|
|
38
|
+
Provides a requests-like interface for interacting with the app's REST API.
|
|
39
|
+
|
|
40
|
+
Example:
|
|
41
|
+
api = env.api()
|
|
42
|
+
response = api.get("/users/1")
|
|
43
|
+
print(response.status_code) # 200
|
|
44
|
+
print(response.json()) # {"id": 1, "name": "John"}
|
|
45
|
+
|
|
46
|
+
# With headers/auth
|
|
47
|
+
response = api.post(
|
|
48
|
+
"/users",
|
|
49
|
+
json={"name": "Jane"},
|
|
50
|
+
headers={"Authorization": "Bearer xxx"}
|
|
51
|
+
)
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
resource: ResourceModel,
|
|
57
|
+
base_url: str,
|
|
58
|
+
client: Optional[httpx.Client] = None,
|
|
59
|
+
):
|
|
60
|
+
super().__init__(resource)
|
|
61
|
+
self.base_url = base_url.rstrip("/")
|
|
62
|
+
self._client = client or httpx.Client()
|
|
63
|
+
|
|
64
|
+
def _build_url(self, path: str) -> str:
|
|
65
|
+
"""Build full URL from base_url and path."""
|
|
66
|
+
if path.startswith("/"):
|
|
67
|
+
return f"{self.base_url}{path}"
|
|
68
|
+
return f"{self.base_url}/{path}"
|
|
69
|
+
|
|
70
|
+
def request(
|
|
71
|
+
self,
|
|
72
|
+
method: str,
|
|
73
|
+
path: str,
|
|
74
|
+
*,
|
|
75
|
+
params: Optional[Dict[str, Any]] = None,
|
|
76
|
+
json: Optional[Any] = None,
|
|
77
|
+
data: Optional[Dict[str, Any]] = None,
|
|
78
|
+
headers: Optional[Dict[str, str]] = None,
|
|
79
|
+
cookies: Optional[Dict[str, str]] = None,
|
|
80
|
+
timeout: Optional[float] = None,
|
|
81
|
+
**kwargs: Any,
|
|
82
|
+
) -> APIResponse:
|
|
83
|
+
"""Make an HTTP request.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
method: HTTP method (GET, POST, PUT, PATCH, DELETE, etc.)
|
|
87
|
+
path: URL path (relative to base_url)
|
|
88
|
+
params: Query parameters
|
|
89
|
+
json: JSON body (will be serialized)
|
|
90
|
+
data: Form data
|
|
91
|
+
headers: Request headers
|
|
92
|
+
cookies: Request cookies
|
|
93
|
+
timeout: Request timeout in seconds
|
|
94
|
+
**kwargs: Additional arguments passed to httpx
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
APIResponse with status_code, headers, text, content, json() method
|
|
98
|
+
"""
|
|
99
|
+
url = self._build_url(path)
|
|
100
|
+
response = self._client.request(
|
|
101
|
+
method,
|
|
102
|
+
url,
|
|
103
|
+
params=params,
|
|
104
|
+
json=json,
|
|
105
|
+
data=data,
|
|
106
|
+
headers=headers,
|
|
107
|
+
cookies=cookies,
|
|
108
|
+
timeout=timeout,
|
|
109
|
+
**kwargs,
|
|
110
|
+
)
|
|
111
|
+
return APIResponse(response)
|
|
112
|
+
|
|
113
|
+
def get(
|
|
114
|
+
self,
|
|
115
|
+
path: str,
|
|
116
|
+
*,
|
|
117
|
+
params: Optional[Dict[str, Any]] = None,
|
|
118
|
+
headers: Optional[Dict[str, str]] = None,
|
|
119
|
+
**kwargs: Any,
|
|
120
|
+
) -> APIResponse:
|
|
121
|
+
"""Make a GET request."""
|
|
122
|
+
return self.request("GET", path, params=params, headers=headers, **kwargs)
|
|
123
|
+
|
|
124
|
+
def post(
|
|
125
|
+
self,
|
|
126
|
+
path: str,
|
|
127
|
+
*,
|
|
128
|
+
json: Optional[Any] = None,
|
|
129
|
+
data: Optional[Dict[str, Any]] = None,
|
|
130
|
+
params: Optional[Dict[str, Any]] = None,
|
|
131
|
+
headers: Optional[Dict[str, str]] = None,
|
|
132
|
+
**kwargs: Any,
|
|
133
|
+
) -> APIResponse:
|
|
134
|
+
"""Make a POST request."""
|
|
135
|
+
return self.request(
|
|
136
|
+
"POST", path, json=json, data=data, params=params, headers=headers, **kwargs
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
def put(
|
|
140
|
+
self,
|
|
141
|
+
path: str,
|
|
142
|
+
*,
|
|
143
|
+
json: Optional[Any] = None,
|
|
144
|
+
data: Optional[Dict[str, Any]] = None,
|
|
145
|
+
params: Optional[Dict[str, Any]] = None,
|
|
146
|
+
headers: Optional[Dict[str, str]] = None,
|
|
147
|
+
**kwargs: Any,
|
|
148
|
+
) -> APIResponse:
|
|
149
|
+
"""Make a PUT request."""
|
|
150
|
+
return self.request(
|
|
151
|
+
"PUT", path, json=json, data=data, params=params, headers=headers, **kwargs
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
def patch(
|
|
155
|
+
self,
|
|
156
|
+
path: str,
|
|
157
|
+
*,
|
|
158
|
+
json: Optional[Any] = None,
|
|
159
|
+
data: Optional[Dict[str, Any]] = None,
|
|
160
|
+
params: Optional[Dict[str, Any]] = None,
|
|
161
|
+
headers: Optional[Dict[str, str]] = None,
|
|
162
|
+
**kwargs: Any,
|
|
163
|
+
) -> APIResponse:
|
|
164
|
+
"""Make a PATCH request."""
|
|
165
|
+
return self.request(
|
|
166
|
+
"PATCH", path, json=json, data=data, params=params, headers=headers, **kwargs
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
def delete(
|
|
170
|
+
self,
|
|
171
|
+
path: str,
|
|
172
|
+
*,
|
|
173
|
+
params: Optional[Dict[str, Any]] = None,
|
|
174
|
+
headers: Optional[Dict[str, str]] = None,
|
|
175
|
+
**kwargs: Any,
|
|
176
|
+
) -> APIResponse:
|
|
177
|
+
"""Make a DELETE request."""
|
|
178
|
+
return self.request("DELETE", path, params=params, headers=headers, **kwargs)
|
|
179
|
+
|
|
180
|
+
def head(
|
|
181
|
+
self,
|
|
182
|
+
path: str,
|
|
183
|
+
*,
|
|
184
|
+
params: Optional[Dict[str, Any]] = None,
|
|
185
|
+
headers: Optional[Dict[str, str]] = None,
|
|
186
|
+
**kwargs: Any,
|
|
187
|
+
) -> APIResponse:
|
|
188
|
+
"""Make a HEAD request."""
|
|
189
|
+
return self.request("HEAD", path, params=params, headers=headers, **kwargs)
|
|
190
|
+
|
|
191
|
+
def options(
|
|
192
|
+
self,
|
|
193
|
+
path: str,
|
|
194
|
+
*,
|
|
195
|
+
params: Optional[Dict[str, Any]] = None,
|
|
196
|
+
headers: Optional[Dict[str, str]] = None,
|
|
197
|
+
**kwargs: Any,
|
|
198
|
+
) -> APIResponse:
|
|
199
|
+
"""Make an OPTIONS request."""
|
|
200
|
+
return self.request("OPTIONS", path, params=params, headers=headers, **kwargs)
|
|
@@ -1439,9 +1439,35 @@ class SyncSnapshotDiff:
|
|
|
1439
1439
|
This version supports field-level specifications for added/removed rows,
|
|
1440
1440
|
allowing users to specify expected field values instead of just whole-row specs.
|
|
1441
1441
|
"""
|
|
1442
|
-
|
|
1443
|
-
if not
|
|
1444
|
-
|
|
1442
|
+
resource = self.after.resource
|
|
1443
|
+
if resource.client is not None and resource._mode == "http":
|
|
1444
|
+
api_diff = None
|
|
1445
|
+
try:
|
|
1446
|
+
payload = {}
|
|
1447
|
+
if self.ignore_config:
|
|
1448
|
+
payload["ignore_config"] = {
|
|
1449
|
+
"tables": list(self.ignore_config.tables),
|
|
1450
|
+
"fields": list(self.ignore_config.fields),
|
|
1451
|
+
"table_fields": {
|
|
1452
|
+
table: list(fields) for table, fields in self.ignore_config.table_fields.items()
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
response = resource.client.request(
|
|
1456
|
+
"POST",
|
|
1457
|
+
"/diff/structured",
|
|
1458
|
+
json=payload,
|
|
1459
|
+
)
|
|
1460
|
+
result = response.json()
|
|
1461
|
+
if result.get("success") and "diff" in result:
|
|
1462
|
+
api_diff = result["diff"]
|
|
1463
|
+
except Exception as e:
|
|
1464
|
+
# Fall back to local diff if API call fails
|
|
1465
|
+
print(f"Warning: Failed to fetch structured diff from API: {e}")
|
|
1466
|
+
print("Falling back to local diff computation...")
|
|
1467
|
+
|
|
1468
|
+
# Validate outside try block so AssertionError propagates
|
|
1469
|
+
if api_diff is not None:
|
|
1470
|
+
return self._validate_diff_against_allowed_changes(api_diff, allowed_changes)
|
|
1445
1471
|
|
|
1446
1472
|
# Fall back to full diff for v2 (no targeted optimization yet)
|
|
1447
1473
|
diff = self._collect()
|
|
@@ -49,6 +49,7 @@ fleet/_async/instance/__init__.py
|
|
|
49
49
|
fleet/_async/instance/base.py
|
|
50
50
|
fleet/_async/instance/client.py
|
|
51
51
|
fleet/_async/resources/__init__.py
|
|
52
|
+
fleet/_async/resources/api.py
|
|
52
53
|
fleet/_async/resources/base.py
|
|
53
54
|
fleet/_async/resources/browser.py
|
|
54
55
|
fleet/_async/resources/mcp.py
|
|
@@ -81,6 +82,7 @@ fleet/proxy/__init__.py
|
|
|
81
82
|
fleet/proxy/proxy.py
|
|
82
83
|
fleet/proxy/whitelist.py
|
|
83
84
|
fleet/resources/__init__.py
|
|
85
|
+
fleet/resources/api.py
|
|
84
86
|
fleet/resources/base.py
|
|
85
87
|
fleet/resources/browser.py
|
|
86
88
|
fleet/resources/mcp.py
|
|
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
|
|
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
|