sing-box-cli 0.0.22__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.
Files changed (29) hide show
  1. {sing_box_cli-0.0.22 → sing_box_cli-0.0.23}/CHANGELOG.md +10 -0
  2. {sing_box_cli-0.0.22 → sing_box_cli-0.0.23}/PKG-INFO +4 -1
  3. sing_box_cli-0.0.23/assets/image.png +0 -0
  4. {sing_box_cli-0.0.22 → sing_box_cli-0.0.23}/pyproject.toml +5 -1
  5. {sing_box_cli-0.0.22 → sing_box_cli-0.0.23}/src/sing_box_service/__init__.py +1 -1
  6. {sing_box_cli-0.0.22 → sing_box_cli-0.0.23}/src/sing_box_service/monitor.py +204 -109
  7. sing_box_cli-0.0.23/src/sing_box_service/widget.py +64 -0
  8. {sing_box_cli-0.0.22 → sing_box_cli-0.0.23}/uv.lock +117 -0
  9. sing_box_cli-0.0.22/assets/image.png +0 -0
  10. sing_box_cli-0.0.22/src/sing_box_service/widget.py +0 -28
  11. {sing_box_cli-0.0.22 → sing_box_cli-0.0.23}/.github/dependabot.yml +0 -0
  12. {sing_box_cli-0.0.22 → sing_box_cli-0.0.23}/.github/workflows/main.yml +0 -0
  13. {sing_box_cli-0.0.22 → sing_box_cli-0.0.23}/.gitignore +0 -0
  14. {sing_box_cli-0.0.22 → sing_box_cli-0.0.23}/.pre-commit-config.yaml +0 -0
  15. {sing_box_cli-0.0.22 → sing_box_cli-0.0.23}/LICENSE +0 -0
  16. {sing_box_cli-0.0.22 → sing_box_cli-0.0.23}/README.md +0 -0
  17. {sing_box_cli-0.0.22 → sing_box_cli-0.0.23}/scripts/bump.sh +0 -0
  18. {sing_box_cli-0.0.22 → sing_box_cli-0.0.23}/scripts/format.sh +0 -0
  19. {sing_box_cli-0.0.22 → sing_box_cli-0.0.23}/scripts/lint.sh +0 -0
  20. {sing_box_cli-0.0.22 → sing_box_cli-0.0.23}/scripts/pre-commit.sh +0 -0
  21. {sing_box_cli-0.0.22 → sing_box_cli-0.0.23}/src/sing_box_service/client.py +0 -0
  22. {sing_box_cli-0.0.22 → sing_box_cli-0.0.23}/src/sing_box_service/config.py +0 -0
  23. {sing_box_cli-0.0.22 → sing_box_cli-0.0.23}/src/sing_box_service/connections.py +0 -0
  24. {sing_box_cli-0.0.22 → sing_box_cli-0.0.23}/src/sing_box_service/main.py +0 -0
  25. {sing_box_cli-0.0.22 → sing_box_cli-0.0.23}/src/sing_box_service/policy.py +0 -0
  26. {sing_box_cli-0.0.22 → sing_box_cli-0.0.23}/src/sing_box_service/py.typed +0 -0
  27. {sing_box_cli-0.0.22 → sing_box_cli-0.0.23}/src/sing_box_service/run.py +0 -0
  28. {sing_box_cli-0.0.22 → sing_box_cli-0.0.23}/src/sing_box_service/service.py +0 -0
  29. {sing_box_cli-0.0.22 → sing_box_cli-0.0.23}/tests/test_config.py +0 -0
@@ -2,6 +2,16 @@
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
+
5
15
  ## 0.0.22 - 2025-03-18
6
16
 
7
17
  ### 🐛 Bug Fixes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sing-box-cli
3
- Version: 0.0.22
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.22"
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}"
@@ -2,4 +2,4 @@ from .main import main
2
2
 
3
3
  __all__ = ["main"]
4
4
 
5
- __version__ = "0.0.22"
5
+ __version__ = "0.0.23"
@@ -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
- from rich import box
9
- from rich.console import Console, Group
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 sing_box_service.client import SingBoxAPIClient
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 into upper and connections
489
+ # Split main section for tables and graph
400
490
  layout["main"].split(
401
- Layout(name="upper", ratio=1), Layout(name="connections", ratio=3)
491
+ Layout(name="stats", ratio=1), Layout(name="graph", ratio=3)
402
492
  )
403
493
 
404
- # Split upper section into left and right
405
- layout["upper"].split_row(Layout(name="left"), Layout(name="right"))
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
- Text("Sing-Box Resource Monitor", justify="center"),
411
- style="bold white on bright_blue",
412
- box=box.SIMPLE,
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, connections_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