sing-box-cli 0.0.21__tar.gz → 0.0.23__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.
- {sing_box_cli-0.0.21 → sing_box_cli-0.0.23}/CHANGELOG.md +16 -0
- {sing_box_cli-0.0.21 → sing_box_cli-0.0.23}/PKG-INFO +4 -1
- sing_box_cli-0.0.23/assets/image.png +0 -0
- {sing_box_cli-0.0.21 → sing_box_cli-0.0.23}/pyproject.toml +5 -1
- {sing_box_cli-0.0.21 → sing_box_cli-0.0.23}/src/sing_box_service/__init__.py +1 -1
- {sing_box_cli-0.0.21 → sing_box_cli-0.0.23}/src/sing_box_service/main.py +3 -3
- {sing_box_cli-0.0.21 → sing_box_cli-0.0.23}/src/sing_box_service/monitor.py +204 -109
- sing_box_cli-0.0.23/src/sing_box_service/widget.py +64 -0
- {sing_box_cli-0.0.21 → sing_box_cli-0.0.23}/uv.lock +117 -0
- sing_box_cli-0.0.21/assets/image.png +0 -0
- sing_box_cli-0.0.21/src/sing_box_service/widget.py +0 -28
- {sing_box_cli-0.0.21 → sing_box_cli-0.0.23}/.github/dependabot.yml +0 -0
- {sing_box_cli-0.0.21 → sing_box_cli-0.0.23}/.github/workflows/main.yml +0 -0
- {sing_box_cli-0.0.21 → sing_box_cli-0.0.23}/.gitignore +0 -0
- {sing_box_cli-0.0.21 → sing_box_cli-0.0.23}/.pre-commit-config.yaml +0 -0
- {sing_box_cli-0.0.21 → sing_box_cli-0.0.23}/LICENSE +0 -0
- {sing_box_cli-0.0.21 → sing_box_cli-0.0.23}/README.md +0 -0
- {sing_box_cli-0.0.21 → sing_box_cli-0.0.23}/scripts/bump.sh +0 -0
- {sing_box_cli-0.0.21 → sing_box_cli-0.0.23}/scripts/format.sh +0 -0
- {sing_box_cli-0.0.21 → sing_box_cli-0.0.23}/scripts/lint.sh +0 -0
- {sing_box_cli-0.0.21 → sing_box_cli-0.0.23}/scripts/pre-commit.sh +0 -0
- {sing_box_cli-0.0.21 → sing_box_cli-0.0.23}/src/sing_box_service/client.py +0 -0
- {sing_box_cli-0.0.21 → sing_box_cli-0.0.23}/src/sing_box_service/config.py +0 -0
- {sing_box_cli-0.0.21 → sing_box_cli-0.0.23}/src/sing_box_service/connections.py +0 -0
- {sing_box_cli-0.0.21 → sing_box_cli-0.0.23}/src/sing_box_service/policy.py +0 -0
- {sing_box_cli-0.0.21 → sing_box_cli-0.0.23}/src/sing_box_service/py.typed +0 -0
- {sing_box_cli-0.0.21 → sing_box_cli-0.0.23}/src/sing_box_service/run.py +0 -0
- {sing_box_cli-0.0.21 → sing_box_cli-0.0.23}/src/sing_box_service/service.py +0 -0
- {sing_box_cli-0.0.21 → sing_box_cli-0.0.23}/tests/test_config.py +0 -0
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## 0.0.23 - 2025-03-19
|
|
6
|
+
|
|
7
|
+
### 🚀 Features
|
|
8
|
+
|
|
9
|
+
- Replace connection table with traffic graph in stats command
|
|
10
|
+
|
|
11
|
+
### 📚 Documentation
|
|
12
|
+
|
|
13
|
+
- Update README.md command --help
|
|
14
|
+
|
|
15
|
+
## 0.0.22 - 2025-03-18
|
|
16
|
+
|
|
17
|
+
### 🐛 Bug Fixes
|
|
18
|
+
|
|
19
|
+
- Commands annotation
|
|
20
|
+
|
|
5
21
|
## 0.0.21 - 2025-03-18
|
|
6
22
|
|
|
7
23
|
### 🐛 Bug Fixes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sing-box-cli
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.23
|
|
4
4
|
Summary: cross-platform sing-box-service manager.
|
|
5
5
|
Project-URL: Homepage, https://github.com/aittcuszz/sing-box-service
|
|
6
6
|
Project-URL: Issues, https://github.com/atticuszz/sing-box-service/issues
|
|
@@ -39,8 +39,11 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
39
39
|
Classifier: Programming Language :: Python :: 3.13
|
|
40
40
|
Requires-Python: >=3.10
|
|
41
41
|
Requires-Dist: httpx>=0.28.1
|
|
42
|
+
Requires-Dist: numpy>=2.2.4
|
|
43
|
+
Requires-Dist: plotext>=5.3.2
|
|
42
44
|
Requires-Dist: prompt-toolkit>=3.0.50
|
|
43
45
|
Requires-Dist: psutil>=6.1.1
|
|
46
|
+
Requires-Dist: scipy>=1.15.2
|
|
44
47
|
Requires-Dist: typer>=0.15.1
|
|
45
48
|
Description-Content-Type: text/markdown
|
|
46
49
|
|
|
Binary file
|
|
@@ -25,8 +25,11 @@ maintainers = [{ name = "A.J.Zeller", email = "zhouge1831@gmail.com" }]
|
|
|
25
25
|
dynamic = ["version"] # https://hatch.pypa.io/latest/config/metadata/#version
|
|
26
26
|
dependencies = [
|
|
27
27
|
"httpx>=0.28.1",
|
|
28
|
+
"numpy>=2.2.4",
|
|
29
|
+
"plotext>=5.3.2",
|
|
28
30
|
"prompt-toolkit>=3.0.50",
|
|
29
31
|
"psutil>=6.1.1",
|
|
32
|
+
"scipy>=1.15.2",
|
|
30
33
|
"typer>=0.15.1",
|
|
31
34
|
]
|
|
32
35
|
|
|
@@ -59,6 +62,7 @@ dev = [
|
|
|
59
62
|
"typos>=1.26.8",
|
|
60
63
|
"types-psutil>=6.1.0.20241221",
|
|
61
64
|
"pytest>=8.3.5",
|
|
65
|
+
"scipy-stubs>=1.15.2.1",
|
|
62
66
|
]
|
|
63
67
|
|
|
64
68
|
[tool.hatch.version]
|
|
@@ -210,7 +214,7 @@ sort_commits = "oldest"
|
|
|
210
214
|
|
|
211
215
|
|
|
212
216
|
[tool.bumpversion]
|
|
213
|
-
current_version = "0.0.
|
|
217
|
+
current_version = "0.0.23"
|
|
214
218
|
parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
|
|
215
219
|
serialize = ["{major}.{minor}.{patch}"]
|
|
216
220
|
search = "{current_version}"
|
|
@@ -88,7 +88,7 @@ def stats(
|
|
|
88
88
|
help="Authentication token for the sing-box API, read from configuration file if not provided",
|
|
89
89
|
),
|
|
90
90
|
) -> None:
|
|
91
|
-
"""Show sing-box traffic, memory statistics and connections, requires API token"""
|
|
91
|
+
"""Show sing-box traffic, memory statistics and connections, requires API token(Optional)"""
|
|
92
92
|
cli = SingBoxCLI()
|
|
93
93
|
api_client = cli.create_client(base_url, token)
|
|
94
94
|
visualizer = ResourceVisualizer()
|
|
@@ -111,7 +111,7 @@ def conns(
|
|
|
111
111
|
help="Authentication token for the sing-box API, read from configuration file if not provided",
|
|
112
112
|
),
|
|
113
113
|
) -> None:
|
|
114
|
-
"""
|
|
114
|
+
"""Manage sing-box connections, requires API token(Optional)"""
|
|
115
115
|
cli = SingBoxCLI()
|
|
116
116
|
api_client = cli.create_client(base_url, token)
|
|
117
117
|
manager = ConnectionsManager(api_client)
|
|
@@ -133,7 +133,7 @@ def proxy(
|
|
|
133
133
|
help="Authentication token for the sing-box API, read from configuration file if not provided",
|
|
134
134
|
),
|
|
135
135
|
) -> None:
|
|
136
|
-
"""
|
|
136
|
+
"""Manage sing-box policy groups, requires API token(Optional)"""
|
|
137
137
|
cli = SingBoxCLI()
|
|
138
138
|
api_client = cli.create_client(base_url, token)
|
|
139
139
|
manager = PolicyGroupManager(api_client)
|
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import math
|
|
2
3
|
import statistics
|
|
3
4
|
from collections import deque
|
|
4
5
|
from datetime import datetime
|
|
5
6
|
from enum import Enum
|
|
6
7
|
from typing import Any
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
import numpy as np
|
|
10
|
+
import plotext as plt # type: ignore
|
|
11
|
+
from rich.console import Console
|
|
10
12
|
from rich.layout import Layout
|
|
11
13
|
from rich.live import Live
|
|
12
14
|
from rich.panel import Panel
|
|
13
15
|
from rich.table import Table
|
|
14
16
|
from rich.text import Text
|
|
17
|
+
from scipy.interpolate import PchipInterpolator, make_interp_spline
|
|
15
18
|
|
|
16
|
-
from
|
|
19
|
+
from .client import SingBoxAPIClient
|
|
20
|
+
from .widget import RichPlotMixin, create_header_panel
|
|
17
21
|
|
|
18
22
|
|
|
19
23
|
class RefreshRate(Enum):
|
|
@@ -193,6 +197,175 @@ def format_chain(chains: list[str]) -> str:
|
|
|
193
197
|
raise ValueError(f"Invalid chains data: {chains}")
|
|
194
198
|
|
|
195
199
|
|
|
200
|
+
def interpolate_data(
|
|
201
|
+
data: list[float], method: str = "linear", factor: int = 5
|
|
202
|
+
) -> tuple[list[float], list[float]]:
|
|
203
|
+
"""
|
|
204
|
+
Interpolate data points to create a smoother curve.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
data: List of data points to interpolate
|
|
208
|
+
method: Interpolation method ('spline', 'pchip', or 'linear')
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
Tuple of (x_new, y_new) containing interpolated data points
|
|
212
|
+
"""
|
|
213
|
+
if len(data) < 3:
|
|
214
|
+
# Not enough points for interpolation, return original data
|
|
215
|
+
return list(range(len(data))), data
|
|
216
|
+
|
|
217
|
+
x = np.array(range(len(data)))
|
|
218
|
+
y = np.array(data)
|
|
219
|
+
|
|
220
|
+
# Create new x values for interpolated points
|
|
221
|
+
points = len(data) * factor
|
|
222
|
+
x_new = np.linspace(x.min(), x.max(), points)
|
|
223
|
+
|
|
224
|
+
if method == "spline":
|
|
225
|
+
# Cubic spline interpolation
|
|
226
|
+
spl = make_interp_spline(x, y, k=3) # k=3 for cubic spline
|
|
227
|
+
y_new = spl(x_new)
|
|
228
|
+
elif method == "pchip":
|
|
229
|
+
# PCHIP interpolation (Piecewise Cubic Hermite Interpolating Polynomial)
|
|
230
|
+
# This preserves monotonicity and doesn't overshoot
|
|
231
|
+
pchip = PchipInterpolator(x, y)
|
|
232
|
+
y_new = pchip(x_new)
|
|
233
|
+
else: # 'linear' or any other value
|
|
234
|
+
# Simple linear interpolation
|
|
235
|
+
y_new = np.interp(x_new, x, y)
|
|
236
|
+
|
|
237
|
+
return list(x_new), list(y_new)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class TrafficGraph(RichPlotMixin):
|
|
241
|
+
"""A Rich-compatible network traffic graph using plotext."""
|
|
242
|
+
|
|
243
|
+
def __init__(self, max_points: int = 60) -> None:
|
|
244
|
+
"""
|
|
245
|
+
Initialize the traffic graph.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
max_points: Maximum number of data points to keep
|
|
249
|
+
"""
|
|
250
|
+
super().__init__()
|
|
251
|
+
|
|
252
|
+
self.max_points = max_points
|
|
253
|
+
# Initialize data structures for storing history
|
|
254
|
+
# At least 2 points needed for a line o avoid rendering issues
|
|
255
|
+
self.upload_speeds: deque[float] = deque([0.0, 0.0], maxlen=max_points)
|
|
256
|
+
self.download_speeds: deque[float] = deque([0.0, 0.0], maxlen=max_points)
|
|
257
|
+
|
|
258
|
+
# Number of points to generate during inte= 5
|
|
259
|
+
self.interp_factor = 5
|
|
260
|
+
|
|
261
|
+
def update_from_traffic_data(self, traffic_data: dict[str, Any]) -> None:
|
|
262
|
+
"""
|
|
263
|
+
Update the graph with new traffic data.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
traffic_data: Dictionary with 'up' and 'down' traffic totals
|
|
267
|
+
"""
|
|
268
|
+
|
|
269
|
+
# Update the graph with new data
|
|
270
|
+
self.upload_speeds.append(traffic_data.get("up", 0))
|
|
271
|
+
self.download_speeds.append(traffic_data.get("down", 0))
|
|
272
|
+
|
|
273
|
+
def make_plot(self, width: int, height: int, interp_method: str = "pchip") -> str:
|
|
274
|
+
"""
|
|
275
|
+
Create the plot with interpolated data.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
width: Width of the plot
|
|
279
|
+
height: Height of the plot
|
|
280
|
+
interp_method: Interpolation method ('spline', 'pchip', or 'linear')
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
String representation of the plot
|
|
284
|
+
"""
|
|
285
|
+
# Clear the previous plot
|
|
286
|
+
plt.clf()
|
|
287
|
+
|
|
288
|
+
# Set up the plot styling
|
|
289
|
+
plt.theme("dark")
|
|
290
|
+
plt.axes_color((10, 14, 27))
|
|
291
|
+
plt.ticks_color((133, 159, 213))
|
|
292
|
+
plt.plotsize(width, height)
|
|
293
|
+
|
|
294
|
+
# Set titles and legend
|
|
295
|
+
plt.xlabel("Time")
|
|
296
|
+
plt.ylabel("Speed")
|
|
297
|
+
|
|
298
|
+
# Get the data for plotting
|
|
299
|
+
y_upload = list(self.upload_speeds)
|
|
300
|
+
y_download = list(self.download_speeds)
|
|
301
|
+
|
|
302
|
+
if len(y_upload) >= 2 and len(y_download) >= 2:
|
|
303
|
+
# Interpolate data
|
|
304
|
+
x_upload, y_upload_smooth = interpolate_data(
|
|
305
|
+
y_upload, interp_method, self.interp_factor
|
|
306
|
+
)
|
|
307
|
+
x_download, y_download_smooth = interpolate_data(
|
|
308
|
+
y_download, interp_method, self.interp_factor
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
# Plot upload and download data with interpolated values
|
|
312
|
+
plt.plot(
|
|
313
|
+
x_upload,
|
|
314
|
+
y_upload_smooth,
|
|
315
|
+
marker=".",
|
|
316
|
+
label="Upload",
|
|
317
|
+
color=(255, 73, 112), # Red
|
|
318
|
+
)
|
|
319
|
+
plt.plot(
|
|
320
|
+
x_download,
|
|
321
|
+
y_download_smooth,
|
|
322
|
+
marker=".",
|
|
323
|
+
label="Download",
|
|
324
|
+
color=(68, 180, 255), # Blue
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
# Calculate and set y-axis ticks with a better algorithm
|
|
328
|
+
all_y_values = y_upload_smooth + y_download_smooth
|
|
329
|
+
max_y_value = max(all_y_values) if all_y_values else 1.0
|
|
330
|
+
|
|
331
|
+
# Ensure we have a reasonable minimum value
|
|
332
|
+
max_y_value = max(max_y_value, 1.0)
|
|
333
|
+
|
|
334
|
+
# Round up to a nice number for the maximum tick
|
|
335
|
+
# Find the magnitude of the value (1, 10, 100, 1000, etc.)
|
|
336
|
+
magnitude = 10 ** math.floor(math.log10(max_y_value))
|
|
337
|
+
|
|
338
|
+
# Scale to a number between 1-10
|
|
339
|
+
scaled = max_y_value / magnitude
|
|
340
|
+
|
|
341
|
+
# Round up to a nice number (1, 2, 5, 10)
|
|
342
|
+
if scaled <= 1:
|
|
343
|
+
nice_max = 1
|
|
344
|
+
elif scaled <= 2:
|
|
345
|
+
nice_max = 2
|
|
346
|
+
elif scaled <= 5:
|
|
347
|
+
nice_max = 5
|
|
348
|
+
else:
|
|
349
|
+
nice_max = 10
|
|
350
|
+
|
|
351
|
+
# Final nice maximum value
|
|
352
|
+
nice_max_value = nice_max * magnitude
|
|
353
|
+
|
|
354
|
+
# Create ticks with appropriate intervals
|
|
355
|
+
num_ticks = 8 # Will give 7 intervals
|
|
356
|
+
y_ticks = [i * (nice_max_value / (num_ticks - 1)) for i in range(num_ticks)]
|
|
357
|
+
|
|
358
|
+
# Create labels
|
|
359
|
+
y_labels = [format_speed(val) for val in y_ticks]
|
|
360
|
+
|
|
361
|
+
# Apply ticks to the plot
|
|
362
|
+
plt.yticks(y_ticks, y_labels)
|
|
363
|
+
|
|
364
|
+
return str(plt.build())
|
|
365
|
+
else:
|
|
366
|
+
return "Collecting data..."
|
|
367
|
+
|
|
368
|
+
|
|
196
369
|
class ResourceVisualizer:
|
|
197
370
|
"""Class for visualizing Sing-Box resource statistics."""
|
|
198
371
|
|
|
@@ -205,6 +378,11 @@ class ResourceVisualizer:
|
|
|
205
378
|
self.traffic_data_history: deque[dict[str, int]] = deque(maxlen=10)
|
|
206
379
|
self.memory_data_history: deque[dict[str, int]] = deque(maxlen=10)
|
|
207
380
|
|
|
381
|
+
# For traffic graph
|
|
382
|
+
self.traffic_graph = TrafficGraph(
|
|
383
|
+
max_points=120 # Store 2 minutes of data
|
|
384
|
+
)
|
|
385
|
+
|
|
208
386
|
def create_traffic_table(self, traffic_data: dict[str, Any]) -> Table:
|
|
209
387
|
"""Create a table displaying traffic statistics with averages."""
|
|
210
388
|
# Add current data to history
|
|
@@ -295,98 +473,10 @@ class ResourceVisualizer:
|
|
|
295
473
|
|
|
296
474
|
return table
|
|
297
475
|
|
|
298
|
-
@staticmethod
|
|
299
|
-
def create_connections_table(connections_data: dict[str, Any]) -> Panel:
|
|
300
|
-
"""
|
|
301
|
-
Create a panel displaying active connections.
|
|
302
|
-
|
|
303
|
-
Args:
|
|
304
|
-
connections_data: Connections data from the API
|
|
305
|
-
|
|
306
|
-
Returns:
|
|
307
|
-
Rich Panel containing the connections information
|
|
308
|
-
"""
|
|
309
|
-
if not connections_data or "connections" not in connections_data:
|
|
310
|
-
return Panel("No connections data available", title="Active Connections")
|
|
311
|
-
|
|
312
|
-
connections = connections_data.get("connections", [])
|
|
313
|
-
|
|
314
|
-
if not connections:
|
|
315
|
-
return Panel("No active connections", title="Active Connections")
|
|
316
|
-
|
|
317
|
-
# Create table to display connections
|
|
318
|
-
table = Table(
|
|
319
|
-
title=f"Active Connections ({len(connections)})",
|
|
320
|
-
expand=True,
|
|
321
|
-
pad_edge=False, # Reduce padding
|
|
322
|
-
)
|
|
323
|
-
table.add_column("Host", style="cyan", no_wrap=True, max_width=20)
|
|
324
|
-
table.add_column("Rule", style="bright_green", no_wrap=True, max_width=15)
|
|
325
|
-
table.add_column("Chain", style="yellow", no_wrap=True, max_width=20)
|
|
326
|
-
table.add_column("Network", justify="center", style="green")
|
|
327
|
-
table.add_column("↑", justify="right", style="bright_blue")
|
|
328
|
-
table.add_column("↓", justify="right", style="magenta")
|
|
329
|
-
table.add_column("Duration", justify="right", style="green")
|
|
330
|
-
|
|
331
|
-
# Sort connections by time
|
|
332
|
-
sorted_connections = sort_connections(connections)
|
|
333
|
-
|
|
334
|
-
# Show top connections (limit to avoid overwhelming the display)
|
|
335
|
-
max_display = 15
|
|
336
|
-
for conn in sorted_connections[:max_display]:
|
|
337
|
-
# Extract data
|
|
338
|
-
metadata = conn.get("metadata", {})
|
|
339
|
-
host = metadata.get("host", "") or metadata.get("destinationIP", "?")
|
|
340
|
-
|
|
341
|
-
network = metadata.get("network", "?").upper()
|
|
342
|
-
upload = format_bytes(conn.get("upload", 0))
|
|
343
|
-
download = format_bytes(conn.get("download", 0))
|
|
344
|
-
|
|
345
|
-
# Extract rule information
|
|
346
|
-
# Format rule display - extract useful parts
|
|
347
|
-
rule_display = format_rule(conn.get("rule", ""))
|
|
348
|
-
|
|
349
|
-
# Calculate duration
|
|
350
|
-
duration_str = format_duration(conn.get("start", ""))
|
|
351
|
-
|
|
352
|
-
# Format chains (usually shows proxy names)
|
|
353
|
-
chain_str = format_chain(conn.get("chains", []))
|
|
354
|
-
|
|
355
|
-
table.add_row(
|
|
356
|
-
host, rule_display, chain_str, network, upload, download, duration_str
|
|
357
|
-
)
|
|
358
|
-
|
|
359
|
-
# Add summary information
|
|
360
|
-
total_upload = format_bytes(connections_data.get("uploadTotal", 0))
|
|
361
|
-
total_download = format_bytes(connections_data.get("downloadTotal", 0))
|
|
362
|
-
memory_usage = format_bytes(connections_data.get("memory", 0))
|
|
363
|
-
|
|
364
|
-
summary = f"Total Upload: {total_upload} | Total Download: {total_download} | Memory: {memory_usage}"
|
|
365
|
-
if len(connections) > max_display:
|
|
366
|
-
summary += f" | Showing {max_display} of {len(connections)} connections"
|
|
367
|
-
|
|
368
|
-
# Return as a panel
|
|
369
|
-
return Panel(
|
|
370
|
-
Group(table, Text(summary, justify="center")), title="Connection Details"
|
|
371
|
-
)
|
|
372
|
-
|
|
373
476
|
def create_resources_layout(
|
|
374
|
-
self,
|
|
375
|
-
traffic_data: dict[str, Any],
|
|
376
|
-
memory_data: dict[str, Any],
|
|
377
|
-
connections_data: dict[str, Any],
|
|
477
|
+
self, traffic_data: dict[str, Any], memory_data: dict[str, Any]
|
|
378
478
|
) -> Layout:
|
|
379
|
-
"""
|
|
380
|
-
Create the main layout with all resource components.
|
|
381
|
-
|
|
382
|
-
Args:
|
|
383
|
-
traffic_data: Traffic statistics data
|
|
384
|
-
memory_data: Memory usage data
|
|
385
|
-
connections_data: Connections data
|
|
386
|
-
|
|
387
|
-
Returns:
|
|
388
|
-
Rich Layout object
|
|
389
|
-
"""
|
|
479
|
+
"""Create the main layout with all resource components."""
|
|
390
480
|
layout = Layout()
|
|
391
481
|
|
|
392
482
|
# Create main sections
|
|
@@ -396,29 +486,35 @@ class ResourceVisualizer:
|
|
|
396
486
|
Layout(name="footer", size=1),
|
|
397
487
|
)
|
|
398
488
|
|
|
399
|
-
# Split main section
|
|
489
|
+
# Split main section for tables and graph
|
|
400
490
|
layout["main"].split(
|
|
401
|
-
Layout(name="
|
|
491
|
+
Layout(name="stats", ratio=1), Layout(name="graph", ratio=3)
|
|
402
492
|
)
|
|
403
493
|
|
|
404
|
-
# Split
|
|
405
|
-
layout["
|
|
494
|
+
# Split stats section into left and right
|
|
495
|
+
layout["stats"].split_row(
|
|
496
|
+
Layout(name="traffic_stats"), Layout(name="memory_stats")
|
|
497
|
+
)
|
|
406
498
|
|
|
407
499
|
# Add components
|
|
408
|
-
layout["header"].update(
|
|
500
|
+
layout["header"].update(create_header_panel("Sing-Box Resource Monitor"))
|
|
501
|
+
|
|
502
|
+
# Add tables
|
|
503
|
+
layout["stats"]["traffic_stats"].update(self.create_traffic_table(traffic_data))
|
|
504
|
+
layout["stats"]["memory_stats"].update(self.create_memory_table(memory_data))
|
|
505
|
+
|
|
506
|
+
# Update the traffic graph with new data
|
|
507
|
+
self.traffic_graph.update_from_traffic_data(traffic_data)
|
|
508
|
+
|
|
509
|
+
# Add the traffic graph to the layout
|
|
510
|
+
layout["graph"].update(
|
|
409
511
|
Panel(
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
512
|
+
self.traffic_graph,
|
|
513
|
+
title="Network Traffic Over Time",
|
|
514
|
+
border_style="cyan",
|
|
413
515
|
)
|
|
414
516
|
)
|
|
415
517
|
|
|
416
|
-
layout["upper"]["left"].update(self.create_traffic_table(traffic_data))
|
|
417
|
-
layout["upper"]["right"].update(self.create_memory_table(memory_data))
|
|
418
|
-
|
|
419
|
-
# Add connections panel
|
|
420
|
-
layout["connections"].update(self.create_connections_table(connections_data))
|
|
421
|
-
|
|
422
518
|
# Add footer
|
|
423
519
|
layout["footer"].update(Text("Press Ctrl+C to exit", justify="center"))
|
|
424
520
|
|
|
@@ -482,11 +578,10 @@ class ResourceMonitor:
|
|
|
482
578
|
# Use the current data from the streams
|
|
483
579
|
traffic_data = self.current_traffic
|
|
484
580
|
memory_data = self.current_memory
|
|
485
|
-
connections_data = await self.api_client.get_connections()
|
|
486
581
|
|
|
487
582
|
# Update display
|
|
488
583
|
layout = self.visualizer.create_resources_layout(
|
|
489
|
-
traffic_data, memory_data
|
|
584
|
+
traffic_data, memory_data
|
|
490
585
|
)
|
|
491
586
|
live.update(layout)
|
|
492
587
|
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from collections.abc import Generator
|
|
2
|
+
from io import StringIO
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from prompt_toolkit import ANSI
|
|
6
|
+
from prompt_toolkit.formatted_text import FormattedText, to_formatted_text
|
|
7
|
+
from rich import box
|
|
8
|
+
from rich.ansi import AnsiDecoder
|
|
9
|
+
from rich.console import Console, Group
|
|
10
|
+
from rich.jupyter import JupyterMixin
|
|
11
|
+
from rich.panel import Panel
|
|
12
|
+
from rich.text import Text
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def create_header_panel(title: str) -> Panel:
|
|
16
|
+
return Panel(
|
|
17
|
+
Text(title, justify="center"), style="bold white on bright_blue", box=box.SIMPLE
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def render_rich_to_prompt_toolkit(rich_obj: Any) -> FormattedText:
|
|
22
|
+
"""Convert a Rich object to prompt_toolkit formatted text."""
|
|
23
|
+
# Capture rich output as string
|
|
24
|
+
console = Console(
|
|
25
|
+
file=StringIO(), color_system="truecolor", highlight=True, force_terminal=True
|
|
26
|
+
)
|
|
27
|
+
console.print(rich_obj)
|
|
28
|
+
output = console.file.getvalue() # type: ignore
|
|
29
|
+
|
|
30
|
+
# Convert to prompt_toolkit formatted text
|
|
31
|
+
return to_formatted_text(ANSI(output))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class RichPlotMixin(JupyterMixin):
|
|
35
|
+
"""A Rich-compatible plot using plotext."""
|
|
36
|
+
|
|
37
|
+
def __init__(self) -> None:
|
|
38
|
+
super().__init__()
|
|
39
|
+
# For Rich rendering
|
|
40
|
+
self.decoder = AnsiDecoder()
|
|
41
|
+
|
|
42
|
+
def make_plot(self, width: int, height: int) -> str:
|
|
43
|
+
raise NotImplementedError
|
|
44
|
+
|
|
45
|
+
def __rich_console__(self, console, options) -> Generator[Group | str, Any, None]: # type: ignore[no-untyped-def]
|
|
46
|
+
"""
|
|
47
|
+
Render the graph as a Rich console renderable.
|
|
48
|
+
Ref: https://github.com/piccolomo/plotext/blob/master/readme/environments.md#rich
|
|
49
|
+
Args:
|
|
50
|
+
console: The Rich console
|
|
51
|
+
options: Console options including width and height
|
|
52
|
+
|
|
53
|
+
Yields:
|
|
54
|
+
Rich-compatible renderables
|
|
55
|
+
"""
|
|
56
|
+
# Adapt to the available width and height
|
|
57
|
+
width = options.max_width or console.width
|
|
58
|
+
height = options.height or console.height
|
|
59
|
+
# Build the plot and convert to Rich format
|
|
60
|
+
try:
|
|
61
|
+
canvas = self.make_plot(width, height)
|
|
62
|
+
yield Group(*self.decoder.decode(canvas))
|
|
63
|
+
except Exception as e:
|
|
64
|
+
yield f"Error rendering graph: {str(e)}"
|
|
@@ -277,6 +277,58 @@ wheels = [
|
|
|
277
277
|
{ url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 },
|
|
278
278
|
]
|
|
279
279
|
|
|
280
|
+
[[package]]
|
|
281
|
+
name = "numpy"
|
|
282
|
+
version = "2.2.4"
|
|
283
|
+
source = { registry = "https://pypi.org/simple" }
|
|
284
|
+
sdist = { url = "https://files.pythonhosted.org/packages/e1/78/31103410a57bc2c2b93a3597340a8119588571f6a4539067546cb9a0bfac/numpy-2.2.4.tar.gz", hash = "sha256:9ba03692a45d3eef66559efe1d1096c4b9b75c0986b5dff5530c378fb8331d4f", size = 20270701 }
|
|
285
|
+
wheels = [
|
|
286
|
+
{ url = "https://files.pythonhosted.org/packages/4c/22/fb1be710a14434c09080dd4a0acc08939f612ec02efcb04b9e210474782d/numpy-2.2.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7716e4a9b7af82c06a2543c53ca476fa0b57e4d760481273e09da04b74ee6ee2", size = 14368066 },
|
|
287
|
+
{ url = "https://files.pythonhosted.org/packages/c2/07/2e5cc71193e3ef3a219ffcf6ca4858e46ea2be09c026ddd480d596b32867/numpy-2.2.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:adf8c1d66f432ce577d0197dceaac2ac00c0759f573f28516246351c58a85020", size = 16417040 },
|
|
288
|
+
{ url = "https://files.pythonhosted.org/packages/1a/97/3b1537776ad9a6d1a41813818343745e8dd928a2916d4c9edcd9a8af1dac/numpy-2.2.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:218f061d2faa73621fa23d6359442b0fc658d5b9a70801373625d958259eaca3", size = 15879862 },
|
|
289
|
+
{ url = "https://files.pythonhosted.org/packages/b0/b7/4472f603dd45ef36ff3d8e84e84fe02d9467c78f92cc121633dce6da307b/numpy-2.2.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:df2f57871a96bbc1b69733cd4c51dc33bea66146b8c63cacbfed73eec0883017", size = 18206032 },
|
|
290
|
+
{ url = "https://files.pythonhosted.org/packages/0d/bd/6a092963fb82e6c5aa0d0440635827bbb2910da229545473bbb58c537ed3/numpy-2.2.4-cp310-cp310-win32.whl", hash = "sha256:a0258ad1f44f138b791327961caedffbf9612bfa504ab9597157806faa95194a", size = 6608517 },
|
|
291
|
+
{ url = "https://files.pythonhosted.org/packages/01/e3/cb04627bc2a1638948bc13e818df26495aa18e20d5be1ed95ab2b10b6847/numpy-2.2.4-cp310-cp310-win_amd64.whl", hash = "sha256:0d54974f9cf14acf49c60f0f7f4084b6579d24d439453d5fc5805d46a165b542", size = 12943498 },
|
|
292
|
+
{ url = "https://files.pythonhosted.org/packages/d5/ee/96457c943265de9fadeb3d2ffdbab003f7fba13d971084a9876affcda095/numpy-2.2.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2fa8fa7697ad1646b5c93de1719965844e004fcad23c91228aca1cf0800044a1", size = 14373079 },
|
|
293
|
+
{ url = "https://files.pythonhosted.org/packages/c5/5c/ceefca458559f0ccc7a982319f37ed07b0d7b526964ae6cc61f8ad1b6119/numpy-2.2.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4162988a360a29af158aeb4a2f4f09ffed6a969c9776f8f3bdee9b06a8ab7e5", size = 16428819 },
|
|
294
|
+
{ url = "https://files.pythonhosted.org/packages/22/31/9b2ac8eee99e001eb6add9fa27514ef5e9faf176169057a12860af52704c/numpy-2.2.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:892c10d6a73e0f14935c31229e03325a7b3093fafd6ce0af704be7f894d95687", size = 15881470 },
|
|
295
|
+
{ url = "https://files.pythonhosted.org/packages/f0/dc/8569b5f25ff30484b555ad8a3f537e0225d091abec386c9420cf5f7a2976/numpy-2.2.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db1f1c22173ac1c58db249ae48aa7ead29f534b9a948bc56828337aa84a32ed6", size = 18218144 },
|
|
296
|
+
{ url = "https://files.pythonhosted.org/packages/5e/05/463c023a39bdeb9bb43a99e7dee2c664cb68d5bb87d14f92482b9f6011cc/numpy-2.2.4-cp311-cp311-win32.whl", hash = "sha256:ea2bb7e2ae9e37d96835b3576a4fa4b3a97592fbea8ef7c3587078b0068b8f09", size = 6606368 },
|
|
297
|
+
{ url = "https://files.pythonhosted.org/packages/8b/72/10c1d2d82101c468a28adc35de6c77b308f288cfd0b88e1070f15b98e00c/numpy-2.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:f7de08cbe5551911886d1ab60de58448c6df0f67d9feb7d1fb21e9875ef95e91", size = 12947526 },
|
|
298
|
+
{ url = "https://files.pythonhosted.org/packages/0e/b2/54122b3c6df5df3e87582b2e9430f1bdb63af4023c739ba300164c9ae503/numpy-2.2.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3f7ac96b16955634e223b579a3e5798df59007ca43e8d451a0e6a50f6bfdfba", size = 14084742 },
|
|
299
|
+
{ url = "https://files.pythonhosted.org/packages/02/e2/e2cbb8d634151aab9528ef7b8bab52ee4ab10e076509285602c2a3a686e0/numpy-2.2.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f92084defa704deadd4e0a5ab1dc52d8ac9e8a8ef617f3fbb853e79b0ea3592", size = 16134051 },
|
|
300
|
+
{ url = "https://files.pythonhosted.org/packages/8e/21/efd47800e4affc993e8be50c1b768de038363dd88865920439ef7b422c60/numpy-2.2.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7a4e84a6283b36632e2a5b56e121961f6542ab886bc9e12f8f9818b3c266bfbb", size = 15578972 },
|
|
301
|
+
{ url = "https://files.pythonhosted.org/packages/04/1e/f8bb88f6157045dd5d9b27ccf433d016981032690969aa5c19e332b138c0/numpy-2.2.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:11c43995255eb4127115956495f43e9343736edb7fcdb0d973defd9de14cd84f", size = 17898106 },
|
|
302
|
+
{ url = "https://files.pythonhosted.org/packages/2b/93/df59a5a3897c1f036ae8ff845e45f4081bb06943039ae28a3c1c7c780f22/numpy-2.2.4-cp312-cp312-win32.whl", hash = "sha256:65ef3468b53269eb5fdb3a5c09508c032b793da03251d5f8722b1194f1790c00", size = 6311190 },
|
|
303
|
+
{ url = "https://files.pythonhosted.org/packages/46/69/8c4f928741c2a8efa255fdc7e9097527c6dc4e4df147e3cadc5d9357ce85/numpy-2.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:2aad3c17ed2ff455b8eaafe06bcdae0062a1db77cb99f4b9cbb5f4ecb13c5146", size = 12644305 },
|
|
304
|
+
{ url = "https://files.pythonhosted.org/packages/13/73/41b7b27f169ecf368b52533edb72e56a133f9e86256e809e169362553b49/numpy-2.2.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f527d8fdb0286fd2fd97a2a96c6be17ba4232da346931d967a0630050dfd298", size = 14081422 },
|
|
305
|
+
{ url = "https://files.pythonhosted.org/packages/4b/04/e208ff3ae3ddfbafc05910f89546382f15a3f10186b1f56bd99f159689c2/numpy-2.2.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bce43e386c16898b91e162e5baaad90c4b06f9dcbe36282490032cec98dc8ae7", size = 16132202 },
|
|
306
|
+
{ url = "https://files.pythonhosted.org/packages/fe/bc/2218160574d862d5e55f803d88ddcad88beff94791f9c5f86d67bd8fbf1c/numpy-2.2.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:31504f970f563d99f71a3512d0c01a645b692b12a63630d6aafa0939e52361e6", size = 15573131 },
|
|
307
|
+
{ url = "https://files.pythonhosted.org/packages/a5/78/97c775bc4f05abc8a8426436b7cb1be806a02a2994b195945600855e3a25/numpy-2.2.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:81413336ef121a6ba746892fad881a83351ee3e1e4011f52e97fba79233611fd", size = 17894270 },
|
|
308
|
+
{ url = "https://files.pythonhosted.org/packages/b9/eb/38c06217a5f6de27dcb41524ca95a44e395e6a1decdc0c99fec0832ce6ae/numpy-2.2.4-cp313-cp313-win32.whl", hash = "sha256:f486038e44caa08dbd97275a9a35a283a8f1d2f0ee60ac260a1790e76660833c", size = 6308141 },
|
|
309
|
+
{ url = "https://files.pythonhosted.org/packages/52/17/d0dd10ab6d125c6d11ffb6dfa3423c3571befab8358d4f85cd4471964fcd/numpy-2.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:207a2b8441cc8b6a2a78c9ddc64d00d20c303d79fba08c577752f080c4007ee3", size = 12636885 },
|
|
310
|
+
{ url = "https://files.pythonhosted.org/packages/41/78/96dddb75bb9be730b87c72f30ffdd62611aba234e4e460576a068c98eff6/numpy-2.2.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f05d4198c1bacc9124018109c5fba2f3201dbe7ab6e92ff100494f236209c960", size = 14051725 },
|
|
311
|
+
{ url = "https://files.pythonhosted.org/packages/00/06/5306b8199bffac2a29d9119c11f457f6c7d41115a335b78d3f86fad4dbe8/numpy-2.2.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2f085ce2e813a50dfd0e01fbfc0c12bbe5d2063d99f8b29da30e544fb6483b8", size = 16101638 },
|
|
312
|
+
{ url = "https://files.pythonhosted.org/packages/fa/03/74c5b631ee1ded596945c12027649e6344614144369fd3ec1aaced782882/numpy-2.2.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:92bda934a791c01d6d9d8e038363c50918ef7c40601552a58ac84c9613a665bc", size = 15571717 },
|
|
313
|
+
{ url = "https://files.pythonhosted.org/packages/cb/dc/4fc7c0283abe0981e3b89f9b332a134e237dd476b0c018e1e21083310c31/numpy-2.2.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ee4d528022f4c5ff67332469e10efe06a267e32f4067dc76bb7e2cddf3cd25ff", size = 17879998 },
|
|
314
|
+
{ url = "https://files.pythonhosted.org/packages/e5/2b/878576190c5cfa29ed896b518cc516aecc7c98a919e20706c12480465f43/numpy-2.2.4-cp313-cp313t-win32.whl", hash = "sha256:05c076d531e9998e7e694c36e8b349969c56eadd2cdcd07242958489d79a7286", size = 6366896 },
|
|
315
|
+
{ url = "https://files.pythonhosted.org/packages/3e/05/eb7eec66b95cf697f08c754ef26c3549d03ebd682819f794cb039574a0a6/numpy-2.2.4-cp313-cp313t-win_amd64.whl", hash = "sha256:188dcbca89834cc2e14eb2f106c96d6d46f200fe0200310fc29089657379c58d", size = 12739119 },
|
|
316
|
+
{ url = "https://files.pythonhosted.org/packages/54/f5/ab0d2f48b490535c7a80e05da4a98902b632369efc04f0e47bb31ca97d8f/numpy-2.2.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0f35b19894a9e08639fd60a1ec1978cb7f5f7f1eace62f38dd36be8aecdef4d", size = 16233350 },
|
|
317
|
+
{ url = "https://files.pythonhosted.org/packages/3b/3a/2f6d8c1f8e45d496bca6baaec93208035faeb40d5735c25afac092ec9a12/numpy-2.2.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b4adfbbc64014976d2f91084915ca4e626fbf2057fb81af209c1a6d776d23e3d", size = 12857565 },
|
|
318
|
+
]
|
|
319
|
+
|
|
320
|
+
[[package]]
|
|
321
|
+
name = "optype"
|
|
322
|
+
version = "0.9.2"
|
|
323
|
+
source = { registry = "https://pypi.org/simple" }
|
|
324
|
+
dependencies = [
|
|
325
|
+
{ name = "typing-extensions", marker = "(python_full_version < '3.13' and sys_platform == 'linux') or (python_full_version < '3.13' and sys_platform == 'win32')" },
|
|
326
|
+
]
|
|
327
|
+
sdist = { url = "https://files.pythonhosted.org/packages/32/c2/76a520809f1a5dd970eab9cc7da3aaa0e16e3fb8a0252fecec1a7a23ad73/optype-0.9.2.tar.gz", hash = "sha256:d7487a5b7fe96bacfc5dc8fab6b90c14f1e65e7ba6e7b22a8bef48d29690edac", size = 96126 }
|
|
328
|
+
wheels = [
|
|
329
|
+
{ url = "https://files.pythonhosted.org/packages/ed/1a/40593f6fb9432fea750c9833e0cff0cd71452b9711b9df83faa251b0fe4e/optype-0.9.2-py3-none-any.whl", hash = "sha256:f9aee29e24794a7af637af80347b9fefbb110dffe012c133079615e551e52ef9", size = 84314 },
|
|
330
|
+
]
|
|
331
|
+
|
|
280
332
|
[[package]]
|
|
281
333
|
name = "packaging"
|
|
282
334
|
version = "24.2"
|
|
@@ -295,6 +347,15 @@ wheels = [
|
|
|
295
347
|
{ url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 },
|
|
296
348
|
]
|
|
297
349
|
|
|
350
|
+
[[package]]
|
|
351
|
+
name = "plotext"
|
|
352
|
+
version = "5.3.2"
|
|
353
|
+
source = { registry = "https://pypi.org/simple" }
|
|
354
|
+
sdist = { url = "https://files.pythonhosted.org/packages/c9/d7/f75f397af966fe252d0d34ffd3cae765317fce2134f925f95e7d6725d1ce/plotext-5.3.2.tar.gz", hash = "sha256:52d1e932e67c177bf357a3f0fe6ce14d1a96f7f7d5679d7b455b929df517068e", size = 61967 }
|
|
355
|
+
wheels = [
|
|
356
|
+
{ url = "https://files.pythonhosted.org/packages/f6/1e/12fe7c40cd2099a1f454518754ed229b01beaf3bbb343127f0cc13ce6c22/plotext-5.3.2-py3-none-any.whl", hash = "sha256:394362349c1ddbf319548cfac17ca65e6d5dfc03200c40dfdc0503b3e95a2283", size = 64047 },
|
|
357
|
+
]
|
|
358
|
+
|
|
298
359
|
[[package]]
|
|
299
360
|
name = "pluggy"
|
|
300
361
|
version = "1.5.0"
|
|
@@ -571,6 +632,54 @@ wheels = [
|
|
|
571
632
|
{ url = "https://files.pythonhosted.org/packages/4e/f7/096f6efabe69b49d7ca61052fc70289c05d8d35735c137ef5ba5ef423662/ruff-0.11.0-py3-none-win_arm64.whl", hash = "sha256:868364fc23f5aa122b00c6f794211e85f7e78f5dffdf7c590ab90b8c4e69b657", size = 10538956 },
|
|
572
633
|
]
|
|
573
634
|
|
|
635
|
+
[[package]]
|
|
636
|
+
name = "scipy"
|
|
637
|
+
version = "1.15.2"
|
|
638
|
+
source = { registry = "https://pypi.org/simple" }
|
|
639
|
+
dependencies = [
|
|
640
|
+
{ name = "numpy", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
|
|
641
|
+
]
|
|
642
|
+
sdist = { url = "https://files.pythonhosted.org/packages/b7/b9/31ba9cd990e626574baf93fbc1ac61cf9ed54faafd04c479117517661637/scipy-1.15.2.tar.gz", hash = "sha256:cd58a314d92838f7e6f755c8a2167ead4f27e1fd5c1251fd54289569ef3495ec", size = 59417316 }
|
|
643
|
+
wheels = [
|
|
644
|
+
{ url = "https://files.pythonhosted.org/packages/dd/65/f9c5755b995ad892020381b8ae11f16d18616208e388621dfacc11df6de6/scipy-1.15.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87994da02e73549dfecaed9e09a4f9d58a045a053865679aeb8d6d43747d4df3", size = 35536214 },
|
|
645
|
+
{ url = "https://files.pythonhosted.org/packages/de/3c/c96d904b9892beec978562f64d8cc43f9cca0842e65bd3cd1b7f7389b0ba/scipy-1.15.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69ea6e56d00977f355c0f84eba69877b6df084516c602d93a33812aa04d90a3d", size = 37646981 },
|
|
646
|
+
{ url = "https://files.pythonhosted.org/packages/3d/74/c2d8a24d18acdeae69ed02e132b9bc1bb67b7bee90feee1afe05a68f9d67/scipy-1.15.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:888307125ea0c4466287191e5606a2c910963405ce9671448ff9c81c53f85f58", size = 37230048 },
|
|
647
|
+
{ url = "https://files.pythonhosted.org/packages/42/19/0aa4ce80eca82d487987eff0bc754f014dec10d20de2f66754fa4ea70204/scipy-1.15.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9412f5e408b397ff5641080ed1e798623dbe1ec0d78e72c9eca8992976fa65aa", size = 40010322 },
|
|
648
|
+
{ url = "https://files.pythonhosted.org/packages/d0/d2/f0683b7e992be44d1475cc144d1f1eeae63c73a14f862974b4db64af635e/scipy-1.15.2-cp310-cp310-win_amd64.whl", hash = "sha256:b5e025e903b4f166ea03b109bb241355b9c42c279ea694d8864d033727205e65", size = 41233385 },
|
|
649
|
+
{ url = "https://files.pythonhosted.org/packages/e0/22/4b4a26fe1cd9ed0bc2b2cb87b17d57e32ab72c346949eaf9288001f8aa8e/scipy-1.15.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a8e34cf4c188b6dd004654f88586d78f95639e48a25dfae9c5e34a6dc34547e", size = 35491547 },
|
|
650
|
+
{ url = "https://files.pythonhosted.org/packages/32/ea/564bacc26b676c06a00266a3f25fdfe91a9d9a2532ccea7ce6dd394541bc/scipy-1.15.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28a0d2c2075946346e4408b211240764759e0fabaeb08d871639b5f3b1aca8a0", size = 37634077 },
|
|
651
|
+
{ url = "https://files.pythonhosted.org/packages/43/c2/bfd4e60668897a303b0ffb7191e965a5da4056f0d98acfb6ba529678f0fb/scipy-1.15.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:42dabaaa798e987c425ed76062794e93a243be8f0f20fff6e7a89f4d61cb3d40", size = 37231657 },
|
|
652
|
+
{ url = "https://files.pythonhosted.org/packages/4a/75/5f13050bf4f84c931bcab4f4e83c212a36876c3c2244475db34e4b5fe1a6/scipy-1.15.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6f5e296ec63c5da6ba6fa0343ea73fd51b8b3e1a300b0a8cae3ed4b1122c7462", size = 40035857 },
|
|
653
|
+
{ url = "https://files.pythonhosted.org/packages/b9/8b/7ec1832b09dbc88f3db411f8cdd47db04505c4b72c99b11c920a8f0479c3/scipy-1.15.2-cp311-cp311-win_amd64.whl", hash = "sha256:597a0c7008b21c035831c39927406c6181bcf8f60a73f36219b69d010aa04737", size = 41217654 },
|
|
654
|
+
{ url = "https://files.pythonhosted.org/packages/f0/24/1a181a9e5050090e0b5138c5f496fee33293c342b788d02586bc410c6477/scipy-1.15.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a6fd6eac1ce74a9f77a7fc724080d507c5812d61e72bd5e4c489b042455865e", size = 35198095 },
|
|
655
|
+
{ url = "https://files.pythonhosted.org/packages/c0/53/eaada1a414c026673eb983f8b4a55fe5eb172725d33d62c1b21f63ff6ca4/scipy-1.15.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b871df1fe1a3ba85d90e22742b93584f8d2b8e6124f8372ab15c71b73e428b8", size = 37297371 },
|
|
656
|
+
{ url = "https://files.pythonhosted.org/packages/e9/06/0449b744892ed22b7e7b9a1994a866e64895363572677a316a9042af1fe5/scipy-1.15.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:03205d57a28e18dfd39f0377d5002725bf1f19a46f444108c29bdb246b6c8a11", size = 36872390 },
|
|
657
|
+
{ url = "https://files.pythonhosted.org/packages/6a/6f/a8ac3cfd9505ec695c1bc35edc034d13afbd2fc1882a7c6b473e280397bb/scipy-1.15.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:601881dfb761311045b03114c5fe718a12634e5608c3b403737ae463c9885d53", size = 39700276 },
|
|
658
|
+
{ url = "https://files.pythonhosted.org/packages/f5/6f/e6e5aff77ea2a48dd96808bb51d7450875af154ee7cbe72188afb0b37929/scipy-1.15.2-cp312-cp312-win_amd64.whl", hash = "sha256:e7c68b6a43259ba0aab737237876e5c2c549a031ddb7abc28c7b47f22e202ded", size = 40942317 },
|
|
659
|
+
{ url = "https://files.pythonhosted.org/packages/37/3b/9bda92a85cd93f19f9ed90ade84aa1e51657e29988317fabdd44544f1dd4/scipy-1.15.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9de9d1416b3d9e7df9923ab23cd2fe714244af10b763975bea9e4f2e81cebd27", size = 35163195 },
|
|
660
|
+
{ url = "https://files.pythonhosted.org/packages/03/5a/fc34bf1aa14dc7c0e701691fa8685f3faec80e57d816615e3625f28feb43/scipy-1.15.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb530e4794fc8ea76a4a21ccb67dea33e5e0e60f07fc38a49e821e1eae3b71a0", size = 37255404 },
|
|
661
|
+
{ url = "https://files.pythonhosted.org/packages/4a/71/472eac45440cee134c8a180dbe4c01b3ec247e0338b7c759e6cd71f199a7/scipy-1.15.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5ea7ed46d437fc52350b028b1d44e002646e28f3e8ddc714011aaf87330f2f32", size = 36860011 },
|
|
662
|
+
{ url = "https://files.pythonhosted.org/packages/01/b3/21f890f4f42daf20e4d3aaa18182dddb9192771cd47445aaae2e318f6738/scipy-1.15.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:11e7ad32cf184b74380f43d3c0a706f49358b904fa7d5345f16ddf993609184d", size = 39657406 },
|
|
663
|
+
{ url = "https://files.pythonhosted.org/packages/0d/76/77cf2ac1f2a9cc00c073d49e1e16244e389dd88e2490c91d84e1e3e4d126/scipy-1.15.2-cp313-cp313-win_amd64.whl", hash = "sha256:a5080a79dfb9b78b768cebf3c9dcbc7b665c5875793569f48bf0e2b1d7f68f6f", size = 40961243 },
|
|
664
|
+
{ url = "https://files.pythonhosted.org/packages/b1/53/1cbb148e6e8f1660aacd9f0a9dfa2b05e9ff1cb54b4386fe868477972ac2/scipy-1.15.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7cd5b77413e1855351cdde594eca99c1f4a588c2d63711388b6a1f1c01f62274", size = 34952867 },
|
|
665
|
+
{ url = "https://files.pythonhosted.org/packages/2c/23/e0eb7f31a9c13cf2dca083828b97992dd22f8184c6ce4fec5deec0c81fcf/scipy-1.15.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d0194c37037707b2afa7a2f2a924cf7bac3dc292d51b6a925e5fcb89bc5c776", size = 36890009 },
|
|
666
|
+
{ url = "https://files.pythonhosted.org/packages/03/f3/e699e19cabe96bbac5189c04aaa970718f0105cff03d458dc5e2b6bd1e8c/scipy-1.15.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:bae43364d600fdc3ac327db99659dcb79e6e7ecd279a75fe1266669d9a652828", size = 36545159 },
|
|
667
|
+
{ url = "https://files.pythonhosted.org/packages/af/f5/ab3838e56fe5cc22383d6fcf2336e48c8fe33e944b9037fbf6cbdf5a11f8/scipy-1.15.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f031846580d9acccd0044efd1a90e6f4df3a6e12b4b6bd694a7bc03a89892b28", size = 39136566 },
|
|
668
|
+
{ url = "https://files.pythonhosted.org/packages/0a/c8/b3f566db71461cabd4b2d5b39bcc24a7e1c119535c8361f81426be39bb47/scipy-1.15.2-cp313-cp313t-win_amd64.whl", hash = "sha256:fe8a9eb875d430d81755472c5ba75e84acc980e4a8f6204d402849234d3017db", size = 40477705 },
|
|
669
|
+
]
|
|
670
|
+
|
|
671
|
+
[[package]]
|
|
672
|
+
name = "scipy-stubs"
|
|
673
|
+
version = "1.15.2.1"
|
|
674
|
+
source = { registry = "https://pypi.org/simple" }
|
|
675
|
+
dependencies = [
|
|
676
|
+
{ name = "optype", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
|
|
677
|
+
]
|
|
678
|
+
sdist = { url = "https://files.pythonhosted.org/packages/55/51/3d6dd7233d8fe19063cf0f4280fabbf5a0b0eaf270438808ca96b57eb118/scipy_stubs-1.15.2.1.tar.gz", hash = "sha256:ba9590ef3cd24511dced121ac0a9c8d9d0681ca5ac091128f0f7ba5a936a8693", size = 274081 }
|
|
679
|
+
wheels = [
|
|
680
|
+
{ url = "https://files.pythonhosted.org/packages/17/9a/1019c6177105d8a26f9b29ad17b280c4d9495eedcd78b819ef8cee6a7c28/scipy_stubs-1.15.2.1-py3-none-any.whl", hash = "sha256:10977d731ec9b3fbff95dd5868903c9cd1caf8621f4ff8b3b45ef30148337bde", size = 458329 },
|
|
681
|
+
]
|
|
682
|
+
|
|
574
683
|
[[package]]
|
|
575
684
|
name = "shellingham"
|
|
576
685
|
version = "1.5.4"
|
|
@@ -585,8 +694,11 @@ name = "sing-box-cli"
|
|
|
585
694
|
source = { editable = "." }
|
|
586
695
|
dependencies = [
|
|
587
696
|
{ name = "httpx", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
|
|
697
|
+
{ name = "numpy", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
|
|
698
|
+
{ name = "plotext", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
|
|
588
699
|
{ name = "prompt-toolkit", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
|
|
589
700
|
{ name = "psutil", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
|
|
701
|
+
{ name = "scipy", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
|
|
590
702
|
{ name = "typer", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
|
|
591
703
|
]
|
|
592
704
|
|
|
@@ -598,6 +710,7 @@ dev = [
|
|
|
598
710
|
{ name = "pre-commit", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
|
|
599
711
|
{ name = "pytest", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
|
|
600
712
|
{ name = "ruff", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
|
|
713
|
+
{ name = "scipy-stubs", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
|
|
601
714
|
{ name = "types-psutil", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
|
|
602
715
|
{ name = "typos", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
|
|
603
716
|
]
|
|
@@ -605,8 +718,11 @@ dev = [
|
|
|
605
718
|
[package.metadata]
|
|
606
719
|
requires-dist = [
|
|
607
720
|
{ name = "httpx", specifier = ">=0.28.1" },
|
|
721
|
+
{ name = "numpy", specifier = ">=2.2.4" },
|
|
722
|
+
{ name = "plotext", specifier = ">=5.3.2" },
|
|
608
723
|
{ name = "prompt-toolkit", specifier = ">=3.0.50" },
|
|
609
724
|
{ name = "psutil", specifier = ">=6.1.1" },
|
|
725
|
+
{ name = "scipy", specifier = ">=1.15.2" },
|
|
610
726
|
{ name = "typer", specifier = ">=0.15.1" },
|
|
611
727
|
]
|
|
612
728
|
|
|
@@ -618,6 +734,7 @@ dev = [
|
|
|
618
734
|
{ name = "pre-commit", specifier = ">=3.8.0" },
|
|
619
735
|
{ name = "pytest", specifier = ">=8.3.5" },
|
|
620
736
|
{ name = "ruff", specifier = ">=0.6.3" },
|
|
737
|
+
{ name = "scipy-stubs", specifier = ">=1.15.2.1" },
|
|
621
738
|
{ name = "types-psutil", specifier = ">=6.1.0.20241221" },
|
|
622
739
|
{ name = "typos", specifier = ">=1.26.8" },
|
|
623
740
|
]
|
|
Binary file
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
from io import StringIO
|
|
2
|
-
from typing import Any
|
|
3
|
-
|
|
4
|
-
from prompt_toolkit import ANSI
|
|
5
|
-
from prompt_toolkit.formatted_text import FormattedText, to_formatted_text
|
|
6
|
-
from rich import box
|
|
7
|
-
from rich.console import Console
|
|
8
|
-
from rich.panel import Panel
|
|
9
|
-
from rich.text import Text
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def create_header_panel(title: str) -> Panel:
|
|
13
|
-
return Panel(
|
|
14
|
-
Text(title, justify="center"), style="bold white on bright_blue", box=box.SIMPLE
|
|
15
|
-
)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def render_rich_to_prompt_toolkit(rich_obj: Any) -> FormattedText:
|
|
19
|
-
"""Convert a Rich object to prompt_toolkit formatted text."""
|
|
20
|
-
# Capture rich output as string
|
|
21
|
-
console = Console(
|
|
22
|
-
file=StringIO(), color_system="truecolor", highlight=True, force_terminal=True
|
|
23
|
-
)
|
|
24
|
-
console.print(rich_obj)
|
|
25
|
-
output = console.file.getvalue() # type: ignore
|
|
26
|
-
|
|
27
|
-
# Convert to prompt_toolkit formatted text
|
|
28
|
-
return to_formatted_text(ANSI(output))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|