pipecatcloud 0.1.6__tar.gz → 0.1.7__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.
- {pipecatcloud-0.1.6 → pipecatcloud-0.1.7}/CHANGELOG.md +10 -0
- {pipecatcloud-0.1.6/src/pipecatcloud.egg-info → pipecatcloud-0.1.7}/PKG-INFO +2 -2
- {pipecatcloud-0.1.6 → pipecatcloud-0.1.7}/pyproject.toml +2 -2
- {pipecatcloud-0.1.6 → pipecatcloud-0.1.7}/src/pipecatcloud/_utils/console_utils.py +63 -0
- {pipecatcloud-0.1.6 → pipecatcloud-0.1.7}/src/pipecatcloud/api.py +8 -0
- {pipecatcloud-0.1.6 → pipecatcloud-0.1.7}/src/pipecatcloud/cli/commands/agent.py +112 -18
- {pipecatcloud-0.1.6 → pipecatcloud-0.1.7}/src/pipecatcloud/config.py +1 -0
- {pipecatcloud-0.1.6 → pipecatcloud-0.1.7/src/pipecatcloud.egg-info}/PKG-INFO +2 -2
- {pipecatcloud-0.1.6 → pipecatcloud-0.1.7}/src/pipecatcloud.egg-info/requires.txt +1 -1
- {pipecatcloud-0.1.6 → pipecatcloud-0.1.7}/.github/workflows/publish-pypi.yml +0 -0
- {pipecatcloud-0.1.6 → pipecatcloud-0.1.7}/.github/workflows/publish-test.yml +0 -0
- {pipecatcloud-0.1.6 → pipecatcloud-0.1.7}/.github/workflows/python-publish.yml +0 -0
- {pipecatcloud-0.1.6 → pipecatcloud-0.1.7}/.gitignore +0 -0
- {pipecatcloud-0.1.6 → pipecatcloud-0.1.7}/LICENSE +0 -0
- {pipecatcloud-0.1.6 → pipecatcloud-0.1.7}/README.md +0 -0
- {pipecatcloud-0.1.6 → pipecatcloud-0.1.7}/dev-requirements.txt +0 -0
- {pipecatcloud-0.1.6 → pipecatcloud-0.1.7}/pipecat-cloud.png +0 -0
- {pipecatcloud-0.1.6 → pipecatcloud-0.1.7}/setup.cfg +0 -0
- {pipecatcloud-0.1.6 → pipecatcloud-0.1.7}/src/pipecatcloud/__init__.py +0 -0
- {pipecatcloud-0.1.6 → pipecatcloud-0.1.7}/src/pipecatcloud/__main__.py +0 -0
- {pipecatcloud-0.1.6 → pipecatcloud-0.1.7}/src/pipecatcloud/__version__.py +0 -0
- {pipecatcloud-0.1.6 → pipecatcloud-0.1.7}/src/pipecatcloud/_utils/__init__.py +0 -0
- {pipecatcloud-0.1.6 → pipecatcloud-0.1.7}/src/pipecatcloud/_utils/async_utils.py +0 -0
- {pipecatcloud-0.1.6 → pipecatcloud-0.1.7}/src/pipecatcloud/_utils/auth_utils.py +0 -0
- {pipecatcloud-0.1.6 → pipecatcloud-0.1.7}/src/pipecatcloud/_utils/deploy_utils.py +0 -0
- {pipecatcloud-0.1.6 → pipecatcloud-0.1.7}/src/pipecatcloud/_utils/local_runner.py +0 -0
- {pipecatcloud-0.1.6 → pipecatcloud-0.1.7}/src/pipecatcloud/agent.py +0 -0
- {pipecatcloud-0.1.6 → pipecatcloud-0.1.7}/src/pipecatcloud/cli/__init__.py +0 -0
- {pipecatcloud-0.1.6 → pipecatcloud-0.1.7}/src/pipecatcloud/cli/api.py +0 -0
- {pipecatcloud-0.1.6 → pipecatcloud-0.1.7}/src/pipecatcloud/cli/commands/__init__.py +0 -0
- {pipecatcloud-0.1.6 → pipecatcloud-0.1.7}/src/pipecatcloud/cli/commands/auth.py +0 -0
- {pipecatcloud-0.1.6 → pipecatcloud-0.1.7}/src/pipecatcloud/cli/commands/deploy.py +0 -0
- {pipecatcloud-0.1.6 → pipecatcloud-0.1.7}/src/pipecatcloud/cli/commands/init.py +0 -0
- {pipecatcloud-0.1.6 → pipecatcloud-0.1.7}/src/pipecatcloud/cli/commands/organizations.py +0 -0
- {pipecatcloud-0.1.6 → pipecatcloud-0.1.7}/src/pipecatcloud/cli/commands/run.py +0 -0
- {pipecatcloud-0.1.6 → pipecatcloud-0.1.7}/src/pipecatcloud/cli/commands/secrets.py +0 -0
- {pipecatcloud-0.1.6 → pipecatcloud-0.1.7}/src/pipecatcloud/cli/config.py +0 -0
- {pipecatcloud-0.1.6 → pipecatcloud-0.1.7}/src/pipecatcloud/cli/entry_point.py +0 -0
- {pipecatcloud-0.1.6 → pipecatcloud-0.1.7}/src/pipecatcloud/exception.py +0 -0
- {pipecatcloud-0.1.6 → pipecatcloud-0.1.7}/src/pipecatcloud/session.py +0 -0
- {pipecatcloud-0.1.6 → pipecatcloud-0.1.7}/src/pipecatcloud.egg-info/SOURCES.txt +0 -0
- {pipecatcloud-0.1.6 → pipecatcloud-0.1.7}/src/pipecatcloud.egg-info/dependency_links.txt +0 -0
- {pipecatcloud-0.1.6 → pipecatcloud-0.1.7}/src/pipecatcloud.egg-info/entry_points.txt +0 -0
- {pipecatcloud-0.1.6 → pipecatcloud-0.1.7}/src/pipecatcloud.egg-info/top_level.txt +0 -0
|
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.1.7] - 2025-06-12
|
|
11
|
+
|
|
12
|
+
# Added
|
|
13
|
+
|
|
14
|
+
- `agent sessions` command lists session history and various statistics (avg. start times, cold starts etc.)
|
|
15
|
+
|
|
16
|
+
# Fixed
|
|
17
|
+
|
|
18
|
+
- `typer` bumped to `0.15` which was causing errors on some setups
|
|
19
|
+
|
|
10
20
|
## [0.1.6] - 2025-04-25
|
|
11
21
|
|
|
12
22
|
# Changed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pipecatcloud
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.7
|
|
4
4
|
Summary: Cloud hosting for Pipecat AI applications
|
|
5
5
|
License: BSD 2-Clause License
|
|
6
6
|
Project-URL: Website, https://docs.pipecat.daily.co/
|
|
@@ -13,7 +13,7 @@ Requires-Dist: fastapi~=0.115.6
|
|
|
13
13
|
Requires-Dist: loguru~=0.7.3
|
|
14
14
|
Requires-Dist: python-dotenv~=1.0.1
|
|
15
15
|
Requires-Dist: uvicorn~=0.32.0
|
|
16
|
-
Requires-Dist: typer~=0.
|
|
16
|
+
Requires-Dist: typer~=0.15.3
|
|
17
17
|
Requires-Dist: synchronicity~=0.7.6
|
|
18
18
|
Requires-Dist: toml~=0.10.2
|
|
19
19
|
Requires-Dist: questionary~=2.1.0
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pipecatcloud"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.7"
|
|
8
8
|
requires-python = ">=3.10"
|
|
9
9
|
description = "Cloud hosting for Pipecat AI applications"
|
|
10
10
|
license = { text = "BSD 2-Clause License" }
|
|
@@ -16,7 +16,7 @@ dependencies = [
|
|
|
16
16
|
"loguru~=0.7.3",
|
|
17
17
|
"python-dotenv~=1.0.1",
|
|
18
18
|
"uvicorn~=0.32.0",
|
|
19
|
-
"typer~=0.
|
|
19
|
+
"typer~=0.15.3",
|
|
20
20
|
"synchronicity~=0.7.6",
|
|
21
21
|
"toml~=0.10.2",
|
|
22
22
|
"questionary~=2.1.0"
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
#
|
|
6
6
|
|
|
7
7
|
from typing import Optional, Union
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
import statistics
|
|
8
10
|
|
|
9
11
|
from rich.console import Console
|
|
10
12
|
from rich.panel import Panel
|
|
@@ -140,3 +142,64 @@ def format_timestamp(timestamp: str) -> str:
|
|
|
140
142
|
|
|
141
143
|
# Return original if parsing fails
|
|
142
144
|
return timestamp
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def format_duration(created_at_str: str, ended_at_str: str) -> str | None:
|
|
148
|
+
"""Calculate and format session duration as HH:MM:SS"""
|
|
149
|
+
if not ended_at_str:
|
|
150
|
+
return None
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
created_at = datetime.fromisoformat(created_at_str.replace('Z', '+00:00'))
|
|
154
|
+
ended_at = datetime.fromisoformat(ended_at_str.replace('Z', '+00:00'))
|
|
155
|
+
duration = ended_at - created_at
|
|
156
|
+
|
|
157
|
+
# Convert to total seconds
|
|
158
|
+
total_seconds = int(duration.total_seconds())
|
|
159
|
+
|
|
160
|
+
# Calculate hours, minutes, seconds
|
|
161
|
+
hours = total_seconds // 3600
|
|
162
|
+
minutes = (total_seconds % 3600) // 60
|
|
163
|
+
seconds = total_seconds % 60
|
|
164
|
+
|
|
165
|
+
return f"{hours:02d}:{minutes:02d}:{seconds:02d}"
|
|
166
|
+
except Exception:
|
|
167
|
+
return None
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def calculate_percentiles(data: list[float]) -> tuple[float, float, float] | None:
|
|
171
|
+
"""
|
|
172
|
+
Calculate average, 5th percentile, and 95th percentile for a list of numeric data.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
data: List of float values to analyze
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
Tuple of (average, p5, p95) if data is provided, None if empty list.
|
|
179
|
+
For single values, p5 and p95 will equal the single value.
|
|
180
|
+
"""
|
|
181
|
+
if not data:
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
avg = statistics.mean(data)
|
|
185
|
+
|
|
186
|
+
if len(data) == 1:
|
|
187
|
+
return avg, data[0], data[0]
|
|
188
|
+
|
|
189
|
+
sorted_data = sorted(data)
|
|
190
|
+
|
|
191
|
+
def percentile(data_list, p):
|
|
192
|
+
if not data_list:
|
|
193
|
+
return 0
|
|
194
|
+
k = (len(data_list) - 1) * p / 100
|
|
195
|
+
f = int(k)
|
|
196
|
+
c = k - f
|
|
197
|
+
if f + 1 < len(data_list):
|
|
198
|
+
return data_list[f] * (1 - c) + data_list[f + 1] * c
|
|
199
|
+
else:
|
|
200
|
+
return data_list[f]
|
|
201
|
+
|
|
202
|
+
p5 = percentile(sorted_data, 5)
|
|
203
|
+
p95 = percentile(sorted_data, 95)
|
|
204
|
+
|
|
205
|
+
return avg, p5, p95
|
|
@@ -428,3 +428,11 @@ class _API:
|
|
|
428
428
|
@property
|
|
429
429
|
def agent_logs(self):
|
|
430
430
|
return self.create_api_method(self._agent_logs)
|
|
431
|
+
|
|
432
|
+
async def _agent_sessions(self, agent_name: str, org: str) -> dict | None:
|
|
433
|
+
url = f"{self.construct_api_url('services_sessions_path').format(org=org, service=agent_name)}"
|
|
434
|
+
return await self._base_request("GET", url) or {}
|
|
435
|
+
|
|
436
|
+
@property
|
|
437
|
+
def agent_sessions(self):
|
|
438
|
+
return self.create_api_method(self._agent_sessions)
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
#
|
|
6
6
|
|
|
7
7
|
import json
|
|
8
|
+
import statistics
|
|
8
9
|
from enum import Enum
|
|
9
10
|
|
|
10
11
|
import aiohttp
|
|
@@ -21,7 +22,7 @@ from rich.text import Text
|
|
|
21
22
|
|
|
22
23
|
from pipecatcloud._utils.async_utils import synchronizer
|
|
23
24
|
from pipecatcloud._utils.auth_utils import requires_login
|
|
24
|
-
from pipecatcloud._utils.console_utils import console, format_timestamp
|
|
25
|
+
from pipecatcloud._utils.console_utils import console, format_duration, format_timestamp, calculate_percentiles
|
|
25
26
|
from pipecatcloud.cli import PIPECAT_CLI_NAME
|
|
26
27
|
from pipecatcloud.cli.api import API
|
|
27
28
|
from pipecatcloud.cli.config import config
|
|
@@ -200,18 +201,31 @@ async def status(
|
|
|
200
201
|
@agent_cli.command(name="sessions", help="List active sessions for an agent")
|
|
201
202
|
@synchronizer.create_blocking
|
|
202
203
|
@requires_login
|
|
204
|
+
@with_deploy_config
|
|
203
205
|
async def sessions(
|
|
204
|
-
|
|
206
|
+
deploy_config=typer.Option(None, hidden=True),
|
|
207
|
+
agent_name: str = typer.Argument(
|
|
208
|
+
None,
|
|
209
|
+
help="Name of the agent to list sessions for e.g. 'my-agent'",
|
|
210
|
+
show_default=False
|
|
211
|
+
),
|
|
212
|
+
session_id: str = typer.Option(
|
|
213
|
+
None,
|
|
214
|
+
"--id",
|
|
215
|
+
"-i",
|
|
216
|
+
help="Session ID to filter by",
|
|
217
|
+
),
|
|
205
218
|
organization: str = typer.Option(
|
|
206
219
|
None, "--organization", "-o", help="Organization to list sessions for"
|
|
207
220
|
),
|
|
208
221
|
):
|
|
209
222
|
org = organization or config.get("org")
|
|
223
|
+
agent_name = agent_name or deploy_config.agent_name
|
|
210
224
|
|
|
211
225
|
with Live(
|
|
212
226
|
console.status(f"[dim]Looking up agent with name '{agent_name}'[/dim]", spinner="dots")
|
|
213
227
|
) as live:
|
|
214
|
-
data, error = await API.
|
|
228
|
+
data, error = await API.agent_sessions(agent_name=agent_name, org=org, live=live)
|
|
215
229
|
|
|
216
230
|
live.stop()
|
|
217
231
|
|
|
@@ -219,24 +233,104 @@ async def sessions(
|
|
|
219
233
|
return typer.Exit()
|
|
220
234
|
|
|
221
235
|
if not data:
|
|
222
|
-
console.error(f"No
|
|
236
|
+
console.error(f"No session data found for agent with name '{agent_name}'")
|
|
223
237
|
return typer.Exit()
|
|
224
238
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
)
|
|
239
|
+
sessions_list = data.get("sessions", [])
|
|
240
|
+
total_sessions = len(sessions_list)
|
|
228
241
|
|
|
229
|
-
if
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
242
|
+
completed_sessions = [s for s in sessions_list if s.get("endedAt")]
|
|
243
|
+
|
|
244
|
+
durations = []
|
|
245
|
+
for session in completed_sessions:
|
|
246
|
+
try:
|
|
247
|
+
from datetime import datetime
|
|
248
|
+
created_at = datetime.fromisoformat(session["createdAt"].replace('Z', '+00:00'))
|
|
249
|
+
ended_at = datetime.fromisoformat(session["endedAt"].replace('Z', '+00:00'))
|
|
250
|
+
duration_seconds = (ended_at - created_at).total_seconds()
|
|
251
|
+
durations.append(duration_seconds)
|
|
252
|
+
except BaseException:
|
|
253
|
+
continue
|
|
254
|
+
|
|
255
|
+
bot_start_times = [s["botStartSeconds"]
|
|
256
|
+
for s in sessions_list if s.get("botStartSeconds") is not None]
|
|
257
|
+
bot_start_metrics = calculate_percentiles(bot_start_times)
|
|
258
|
+
duration_metrics = calculate_percentiles(durations)
|
|
259
|
+
cold_starts_count = sum(1 for s in sessions_list if s.get("coldStart") is True)
|
|
260
|
+
cold_start_percent = cold_starts_count / total_sessions * 100
|
|
261
|
+
|
|
262
|
+
if duration_metrics and bot_start_metrics and total_sessions > 0:
|
|
263
|
+
metric_renderables = [
|
|
264
|
+
Panel(
|
|
265
|
+
f"[bold]Total Sessions:[/bold]\n{total_sessions}\n ",
|
|
266
|
+
expand=True,
|
|
267
|
+
),
|
|
268
|
+
Panel(
|
|
269
|
+
f"[bold]Average Duration:[/bold]\n{duration_metrics[0]:.1f}s\n[dim](p5: {duration_metrics[1]:.1f}s, p95: {duration_metrics[2]:.1f}s)[/dim]",
|
|
270
|
+
expand=True,
|
|
271
|
+
),
|
|
272
|
+
Panel(
|
|
273
|
+
f"[bold]Bot Start Time:[/bold]\n{bot_start_metrics[0]:.1f}s\n[dim](p5: {bot_start_metrics[1]:.1f}s, p95: {bot_start_metrics[2]:.1f}s)[/dim]",
|
|
274
|
+
expand=True,
|
|
275
|
+
),
|
|
276
|
+
Panel(
|
|
277
|
+
f"[bold]Cold Starts:[/bold]\n{cold_starts_count}/{total_sessions}\n[dim]({cold_start_percent:.1f}%)[/dim]",
|
|
278
|
+
expand=True,
|
|
279
|
+
),
|
|
280
|
+
]
|
|
281
|
+
|
|
282
|
+
table = Table(show_header=True, show_lines=True, border_style="dim", box=box.SIMPLE)
|
|
283
|
+
table.add_column("Session ID")
|
|
284
|
+
table.add_column("Created At")
|
|
285
|
+
table.add_column("Ended At")
|
|
286
|
+
table.add_column("Duration")
|
|
287
|
+
table.add_column("Status")
|
|
288
|
+
table.add_column("Bot Start Time")
|
|
289
|
+
table.add_column("Cold Start")
|
|
290
|
+
|
|
291
|
+
for session in data.get("sessions", []):
|
|
292
|
+
if session_id and session["sessionId"] != session_id:
|
|
293
|
+
continue
|
|
294
|
+
|
|
295
|
+
session_duration = format_duration(
|
|
296
|
+
session["createdAt"], session["endedAt"]) or "[dim]N/A[/dim]"
|
|
297
|
+
status = session.get("completionStatus", "")
|
|
298
|
+
if session["endedAt"]:
|
|
299
|
+
if status == "500":
|
|
300
|
+
status_display = "[red]Error (500)[/red]"
|
|
301
|
+
else:
|
|
302
|
+
status_display = "Complete"
|
|
303
|
+
else:
|
|
304
|
+
status_display = "[yellow]Active[/yellow]"
|
|
305
|
+
|
|
306
|
+
is_cold_start = session["coldStart"] is True
|
|
307
|
+
row_style = "on red" if is_cold_start else ""
|
|
308
|
+
|
|
309
|
+
row_data = [
|
|
310
|
+
session["sessionId"],
|
|
311
|
+
format_timestamp(
|
|
312
|
+
session["createdAt"]),
|
|
313
|
+
format_timestamp(
|
|
314
|
+
session["endedAt"]) if session["endedAt"] else "[dim]N/A[/dim]",
|
|
315
|
+
session_duration,
|
|
316
|
+
status_display,
|
|
317
|
+
f"{session['botStartSeconds']}s" if session["botStartSeconds"] is not None else "[dim]N/A[/dim]",
|
|
318
|
+
"[red]Yes[/red]" if session["coldStart"] is True else "No" if session["coldStart"] is False else "[dim]N/A[/dim]",
|
|
319
|
+
]
|
|
320
|
+
|
|
321
|
+
if is_cold_start:
|
|
322
|
+
row_data = [f"[{row_style}]{cell}[/]" for cell in row_data]
|
|
323
|
+
|
|
324
|
+
table.add_row(*row_data)
|
|
325
|
+
|
|
326
|
+
console.success(
|
|
327
|
+
Group(
|
|
328
|
+
Columns(
|
|
329
|
+
metric_renderables,
|
|
330
|
+
equal=True) if metric_renderables and not session_id else "",
|
|
331
|
+
table),
|
|
332
|
+
title=f"Session data for agent {agent_name} [dim]({org})[/dim]",
|
|
333
|
+
)
|
|
240
334
|
|
|
241
335
|
|
|
242
336
|
@agent_cli.command(name="scale", help="Modify agent runtime configuration")
|
|
@@ -30,6 +30,7 @@ _SETTINGS = {
|
|
|
30
30
|
"services_path": _Setting("/v1/organizations/{org}/services"),
|
|
31
31
|
"services_logs_path": _Setting("/v1/organizations/{org}/services/{service}/logs"),
|
|
32
32
|
"services_deployments_path": _Setting("/v1/organizations/{org}/services/{service}/deployments"),
|
|
33
|
+
"services_sessions_path": _Setting("/v1/organizations/{org}/services/{service}/sessions"),
|
|
33
34
|
"start_path": _Setting("/v1/public/{service}/start"),
|
|
34
35
|
"api_keys_path": _Setting("/v1/organizations/{org}/apiKeys"),
|
|
35
36
|
"secrets_path": _Setting("/v1/organizations/{org}/secrets"),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pipecatcloud
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.7
|
|
4
4
|
Summary: Cloud hosting for Pipecat AI applications
|
|
5
5
|
License: BSD 2-Clause License
|
|
6
6
|
Project-URL: Website, https://docs.pipecat.daily.co/
|
|
@@ -13,7 +13,7 @@ Requires-Dist: fastapi~=0.115.6
|
|
|
13
13
|
Requires-Dist: loguru~=0.7.3
|
|
14
14
|
Requires-Dist: python-dotenv~=1.0.1
|
|
15
15
|
Requires-Dist: uvicorn~=0.32.0
|
|
16
|
-
Requires-Dist: typer~=0.
|
|
16
|
+
Requires-Dist: typer~=0.15.3
|
|
17
17
|
Requires-Dist: synchronicity~=0.7.6
|
|
18
18
|
Requires-Dist: toml~=0.10.2
|
|
19
19
|
Requires-Dist: questionary~=2.1.0
|
|
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
|