applied-cli 0.6.2__tar.gz → 0.6.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {applied_cli-0.6.2 → applied_cli-0.6.3}/PKG-INFO +1 -1
- {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli/cli.py +21 -0
- {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli/tools.py +89 -0
- {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli/v2/domains.py +1 -0
- {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli/v2/scenarios.py +51 -0
- {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli.egg-info/PKG-INFO +1 -1
- {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli.egg-info/SOURCES.txt +1 -0
- {applied_cli-0.6.2 → applied_cli-0.6.3}/pyproject.toml +1 -1
- applied_cli-0.6.3/tests/test_scenario_bulk_cancel.py +87 -0
- {applied_cli-0.6.2 → applied_cli-0.6.3}/README.md +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli/__init__.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli/agent_scoped_flows.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli/auth.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli/client.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli/conversation_lookup.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli/conversations.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli/credentials.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli/flow_helpers.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli/formatters.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli/mcp.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli/recovery.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli/toolkit.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli/v2/__init__.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli/v2/agents.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli/v2/articles.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli/v2/catalog.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli/v2/connectors.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli/v2/content.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli/v2/conversations.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli/v2/flows.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli/v2/knowledge.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli/v2/manifest.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli/v2/products.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli/v2/taxonomy.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli/v2/tickets.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli.egg-info/dependency_links.txt +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli.egg-info/entry_points.txt +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli.egg-info/requires.txt +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli.egg-info/top_level.txt +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.3}/setup.cfg +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.3}/tests/test_agent_scoped_flows.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.3}/tests/test_audit_tools.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.3}/tests/test_auth_context.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.3}/tests/test_benchmark_clone.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.3}/tests/test_benchmark_delete_guardrail.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.3}/tests/test_benchmark_scenario_tools.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.3}/tests/test_cli.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.3}/tests/test_cli_v2.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.3}/tests/test_client.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.3}/tests/test_client_v2.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.3}/tests/test_conversation_tools.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.3}/tests/test_flow_tools.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.3}/tests/test_knowledge_content_tools.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.3}/tests/test_recovery.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.3}/tests/test_toolkit_contract.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.3}/tests/test_v2_agents.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.3}/tests/test_v2_articles.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.3}/tests/test_v2_catalog_and_mcp.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.3}/tests/test_v2_connectors.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.3}/tests/test_v2_content.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.3}/tests/test_v2_conversations.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.3}/tests/test_v2_flows.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.3}/tests/test_v2_knowledge.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.3}/tests/test_v2_products.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.3}/tests/test_v2_scenarios.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.3}/tests/test_v2_taxonomy.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.3}/tests/test_v2_tickets.py +0 -0
|
@@ -1956,6 +1956,27 @@ def scenario_bulk_status(
|
|
|
1956
1956
|
typer.echo(result)
|
|
1957
1957
|
|
|
1958
1958
|
|
|
1959
|
+
@app.command("scenario-bulk-cancel")
|
|
1960
|
+
def scenario_bulk_cancel(
|
|
1961
|
+
job_id: str = typer.Argument(..., help="Bulk run job ID"),
|
|
1962
|
+
apply: bool = typer.Option(
|
|
1963
|
+
False, "--apply", help="Cancel the pending runs (default is a dry-run plan)"
|
|
1964
|
+
),
|
|
1965
|
+
shop_id: str = typer.Option(None, "--shop-id", help="Override shop ID"),
|
|
1966
|
+
format: str = typer.Option(
|
|
1967
|
+
"text", "--format", "-f", help="Output format: text or json"
|
|
1968
|
+
),
|
|
1969
|
+
) -> None:
|
|
1970
|
+
"""Cancel a stuck bulk run by deleting its queued/running scenario runs."""
|
|
1971
|
+
client = get_client(shop_id=shop_id)
|
|
1972
|
+
result = asyncio.run(
|
|
1973
|
+
tools.scenario_bulk_cancel(
|
|
1974
|
+
client, job_id=job_id, apply=apply, output_format=format
|
|
1975
|
+
)
|
|
1976
|
+
)
|
|
1977
|
+
typer.echo(result)
|
|
1978
|
+
|
|
1979
|
+
|
|
1959
1980
|
@app.command("audit-metric-fields")
|
|
1960
1981
|
def audit_metric_fields(
|
|
1961
1982
|
is_active: bool = typer.Option(
|
|
@@ -6230,6 +6230,95 @@ async def scenario_bulk_status(
|
|
|
6230
6230
|
return output
|
|
6231
6231
|
|
|
6232
6232
|
|
|
6233
|
+
_PENDING_RUN_STATUSES = ("queued", "running")
|
|
6234
|
+
|
|
6235
|
+
|
|
6236
|
+
async def scenario_bulk_cancel(
|
|
6237
|
+
client: AppliedClient,
|
|
6238
|
+
job_id: str,
|
|
6239
|
+
*,
|
|
6240
|
+
apply: bool = False,
|
|
6241
|
+
output_format: str = "text",
|
|
6242
|
+
) -> str:
|
|
6243
|
+
"""
|
|
6244
|
+
Cancel a stuck bulk scenario run job.
|
|
6245
|
+
|
|
6246
|
+
A bulk run can get stuck with runs left in 'queued' or 'running' forever, so
|
|
6247
|
+
the job never reports complete. This cancels the job by deleting those pending
|
|
6248
|
+
runs; completed and failed runs (and their result conversations) are preserved.
|
|
6249
|
+
Once no pending runs remain, the job reports as finished.
|
|
6250
|
+
|
|
6251
|
+
This replaces the manual workaround of deleting the 'agent-test-bulk-job:'
|
|
6252
|
+
key from browser localStorage.
|
|
6253
|
+
|
|
6254
|
+
Args:
|
|
6255
|
+
client: Authenticated AppliedClient
|
|
6256
|
+
job_id: The bulk run job UUID
|
|
6257
|
+
apply: If True, delete the pending runs; otherwise just report the plan
|
|
6258
|
+
output_format: 'text' (default) or 'json'
|
|
6259
|
+
|
|
6260
|
+
Returns:
|
|
6261
|
+
Summary of pending runs cancelled (or that would be cancelled).
|
|
6262
|
+
"""
|
|
6263
|
+
try:
|
|
6264
|
+
runs = await client.list_scenario_runs(
|
|
6265
|
+
bulk_job_id=job_id, latest=False, limit=1000
|
|
6266
|
+
)
|
|
6267
|
+
except AppliedAPIError as e:
|
|
6268
|
+
return _format_error(e)
|
|
6269
|
+
|
|
6270
|
+
pending = [r for r in runs if str(r.get("status") or "") in _PENDING_RUN_STATUSES]
|
|
6271
|
+
result: dict[str, Any] = {
|
|
6272
|
+
"job_id": job_id,
|
|
6273
|
+
"apply": apply,
|
|
6274
|
+
"total_runs": len(runs),
|
|
6275
|
+
"pending_runs": len(pending),
|
|
6276
|
+
"terminal_runs": len(runs) - len(pending),
|
|
6277
|
+
"cancelled": 0,
|
|
6278
|
+
"errors": [],
|
|
6279
|
+
}
|
|
6280
|
+
|
|
6281
|
+
if not runs:
|
|
6282
|
+
result["message"] = (
|
|
6283
|
+
f"No runs found for job {job_id} — it may already be cleared or the "
|
|
6284
|
+
f"job id is invalid. Nothing to cancel."
|
|
6285
|
+
)
|
|
6286
|
+
return to_json(result) if output_format == "json" else result["message"]
|
|
6287
|
+
|
|
6288
|
+
if not pending:
|
|
6289
|
+
result["message"] = (
|
|
6290
|
+
f"Job {job_id} has no pending (queued/running) runs — nothing to "
|
|
6291
|
+
f"cancel. {result['terminal_runs']} run(s) already finished."
|
|
6292
|
+
)
|
|
6293
|
+
return to_json(result) if output_format == "json" else result["message"]
|
|
6294
|
+
|
|
6295
|
+
if not apply:
|
|
6296
|
+
result["message"] = (
|
|
6297
|
+
f"Would cancel {len(pending)} pending run(s) (queued/running) for job "
|
|
6298
|
+
f"{job_id}; {result['terminal_runs']} finished run(s) preserved. "
|
|
6299
|
+
f"Re-run with --apply to cancel."
|
|
6300
|
+
)
|
|
6301
|
+
return to_json(result) if output_format == "json" else result["message"]
|
|
6302
|
+
|
|
6303
|
+
cancelled = 0
|
|
6304
|
+
for run in pending:
|
|
6305
|
+
run_id = run.get("id")
|
|
6306
|
+
if not run_id:
|
|
6307
|
+
continue
|
|
6308
|
+
try:
|
|
6309
|
+
await client.delete_scenario_run(str(run_id))
|
|
6310
|
+
cancelled += 1
|
|
6311
|
+
except AppliedAPIError as e:
|
|
6312
|
+
result["errors"].append({"run_id": str(run_id), "error": str(e)})
|
|
6313
|
+
result["cancelled"] = cancelled
|
|
6314
|
+
result["message"] = (
|
|
6315
|
+
f"Cancelled {cancelled} pending run(s) for job {job_id}; "
|
|
6316
|
+
f"{result['terminal_runs']} finished run(s) preserved."
|
|
6317
|
+
+ (f" {len(result['errors'])} error(s)." if result["errors"] else "")
|
|
6318
|
+
)
|
|
6319
|
+
return to_json(result) if output_format == "json" else result["message"]
|
|
6320
|
+
|
|
6321
|
+
|
|
6233
6322
|
async def _load_audit_target_summaries(
|
|
6234
6323
|
client: AppliedClient,
|
|
6235
6324
|
ratings: list[dict[str, Any]],
|
|
@@ -119,6 +119,7 @@ DOMAIN_TOOL_RENAMES: dict[str, dict[str, str]] = {
|
|
|
119
119
|
"scenario_run_delete": "scenarios_runs_delete",
|
|
120
120
|
"scenario_bulk_run": "scenarios_bulk_run",
|
|
121
121
|
"scenario_bulk_status": "scenarios_bulk_status",
|
|
122
|
+
"scenario_bulk_cancel": "scenarios_bulk_cancel",
|
|
122
123
|
},
|
|
123
124
|
"taxonomy": {
|
|
124
125
|
"taxonomy_list": "taxonomy_items_list",
|
|
@@ -29,6 +29,11 @@ class ScenariosBulkRunInput(StrictInput):
|
|
|
29
29
|
contact_override: dict[str, Any] | None = None
|
|
30
30
|
|
|
31
31
|
|
|
32
|
+
class ScenariosBulkCancelInput(StrictInput):
|
|
33
|
+
job_id: str
|
|
34
|
+
apply: bool = False
|
|
35
|
+
|
|
36
|
+
|
|
32
37
|
class BenchmarksListInput(StrictInput):
|
|
33
38
|
agent_id: str | None = None
|
|
34
39
|
limit: int = 50
|
|
@@ -850,6 +855,38 @@ async def scenarios_bulk_status_handler(
|
|
|
850
855
|
)
|
|
851
856
|
|
|
852
857
|
|
|
858
|
+
async def scenarios_bulk_cancel_handler(
|
|
859
|
+
client: AppliedClient,
|
|
860
|
+
params: ScenariosBulkCancelInput,
|
|
861
|
+
) -> ToolResult[Any]:
|
|
862
|
+
from applied_cli import tools as legacy_tools
|
|
863
|
+
|
|
864
|
+
raw = await legacy_tools.scenario_bulk_cancel(
|
|
865
|
+
client,
|
|
866
|
+
job_id=params.job_id,
|
|
867
|
+
apply=params.apply,
|
|
868
|
+
output_format="json",
|
|
869
|
+
)
|
|
870
|
+
try:
|
|
871
|
+
data = json.loads(raw)
|
|
872
|
+
except (json.JSONDecodeError, TypeError):
|
|
873
|
+
return ToolResult(data={"message": raw}, summary=str(raw))
|
|
874
|
+
|
|
875
|
+
next_actions = []
|
|
876
|
+
if not params.apply and data.get("pending_runs"):
|
|
877
|
+
next_actions.append("Re-run with apply=true to cancel the pending runs.")
|
|
878
|
+
elif data.get("cancelled"):
|
|
879
|
+
next_actions.append(
|
|
880
|
+
f"Poll scenarios_bulk_status with job_id='{params.job_id}' to confirm "
|
|
881
|
+
f"the job now reports finished."
|
|
882
|
+
)
|
|
883
|
+
return ToolResult(
|
|
884
|
+
data=data,
|
|
885
|
+
summary=data.get("message", "Bulk cancel processed."),
|
|
886
|
+
next_actions=next_actions,
|
|
887
|
+
)
|
|
888
|
+
|
|
889
|
+
|
|
853
890
|
def scenario_specs() -> list[ToolSpec]:
|
|
854
891
|
return [
|
|
855
892
|
ToolSpec(
|
|
@@ -1034,4 +1071,18 @@ def scenario_specs() -> list[ToolSpec]:
|
|
|
1034
1071
|
read_write_mode="read",
|
|
1035
1072
|
tags=["scenario_bulk_status", "native"],
|
|
1036
1073
|
),
|
|
1074
|
+
ToolSpec(
|
|
1075
|
+
name="scenarios_bulk_cancel",
|
|
1076
|
+
namespace="scenarios",
|
|
1077
|
+
description=(
|
|
1078
|
+
"Cancel a stuck bulk scenario run job by deleting its queued/"
|
|
1079
|
+
"running runs (completed/failed runs are preserved). Dry-run by "
|
|
1080
|
+
"default; set apply=true to cancel."
|
|
1081
|
+
),
|
|
1082
|
+
input_model=ScenariosBulkCancelInput,
|
|
1083
|
+
output_model=None,
|
|
1084
|
+
handler=scenarios_bulk_cancel_handler,
|
|
1085
|
+
read_write_mode="write",
|
|
1086
|
+
tags=["scenario_bulk_cancel", "native"],
|
|
1087
|
+
),
|
|
1037
1088
|
]
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from applied_cli import tools
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FakeBulkClient:
|
|
9
|
+
def __init__(self, runs):
|
|
10
|
+
self._runs = runs
|
|
11
|
+
self.deleted = []
|
|
12
|
+
|
|
13
|
+
async def list_scenario_runs(
|
|
14
|
+
self, scenario_id=None, benchmark_id=None, bulk_job_id=None, latest=False,
|
|
15
|
+
limit=50,
|
|
16
|
+
):
|
|
17
|
+
return list(self._runs)
|
|
18
|
+
|
|
19
|
+
async def delete_scenario_run(self, run_id):
|
|
20
|
+
self.deleted.append(run_id)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
STUCK_RUNS = [
|
|
24
|
+
{"id": "r1", "status": "completed"},
|
|
25
|
+
{"id": "r2", "status": "queued"},
|
|
26
|
+
{"id": "r3", "status": "running"},
|
|
27
|
+
{"id": "r4", "status": "failed"},
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@pytest.mark.asyncio
|
|
32
|
+
async def test_dry_run_reports_pending_without_deleting():
|
|
33
|
+
client = FakeBulkClient(STUCK_RUNS)
|
|
34
|
+
result = await tools.scenario_bulk_cancel(client, "job-1", output_format="json")
|
|
35
|
+
data = json.loads(result)
|
|
36
|
+
assert data["pending_runs"] == 2
|
|
37
|
+
assert data["terminal_runs"] == 2
|
|
38
|
+
assert data["cancelled"] == 0
|
|
39
|
+
assert client.deleted == [] # dry run deletes nothing
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@pytest.mark.asyncio
|
|
43
|
+
async def test_apply_deletes_only_pending_runs():
|
|
44
|
+
client = FakeBulkClient(STUCK_RUNS)
|
|
45
|
+
result = await tools.scenario_bulk_cancel(
|
|
46
|
+
client, "job-1", apply=True, output_format="json"
|
|
47
|
+
)
|
|
48
|
+
data = json.loads(result)
|
|
49
|
+
assert data["cancelled"] == 2
|
|
50
|
+
# Only the queued/running runs are deleted; completed/failed preserved.
|
|
51
|
+
assert set(client.deleted) == {"r2", "r3"}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@pytest.mark.asyncio
|
|
55
|
+
async def test_no_pending_runs_is_a_noop():
|
|
56
|
+
client = FakeBulkClient(
|
|
57
|
+
[{"id": "r1", "status": "completed"}, {"id": "r2", "status": "failed"}]
|
|
58
|
+
)
|
|
59
|
+
result = await tools.scenario_bulk_cancel(
|
|
60
|
+
client, "job-1", apply=True, output_format="text"
|
|
61
|
+
)
|
|
62
|
+
assert "nothing to" in result.lower()
|
|
63
|
+
assert client.deleted == []
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@pytest.mark.asyncio
|
|
67
|
+
async def test_unknown_job_reports_no_runs():
|
|
68
|
+
client = FakeBulkClient([])
|
|
69
|
+
result = await tools.scenario_bulk_cancel(client, "missing", output_format="text")
|
|
70
|
+
assert "No runs found" in result
|
|
71
|
+
assert client.deleted == []
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@pytest.mark.asyncio
|
|
75
|
+
async def test_v2_scenarios_bulk_cancel_handler_structured():
|
|
76
|
+
from applied_cli.v2.scenarios import (
|
|
77
|
+
ScenariosBulkCancelInput,
|
|
78
|
+
scenarios_bulk_cancel_handler,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
client = FakeBulkClient(STUCK_RUNS)
|
|
82
|
+
result = await scenarios_bulk_cancel_handler(
|
|
83
|
+
client, ScenariosBulkCancelInput(job_id="job-1", apply=False)
|
|
84
|
+
)
|
|
85
|
+
assert result.data["pending_runs"] == 2
|
|
86
|
+
assert "apply=true" in " ".join(result.next_actions)
|
|
87
|
+
assert client.deleted == []
|
|
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
|