sing-box-cli 0.0.25__tar.gz → 0.0.27__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.25 → sing_box_cli-0.0.27}/CHANGELOG.md +25 -0
- {sing_box_cli-0.0.25 → sing_box_cli-0.0.27}/PKG-INFO +2 -1
- {sing_box_cli-0.0.25 → sing_box_cli-0.0.27}/pyproject.toml +2 -1
- {sing_box_cli-0.0.25 → sing_box_cli-0.0.27}/src/sing_box_service/__init__.py +1 -1
- sing_box_cli-0.0.27/src/sing_box_service/api/__init__.py +99 -0
- sing_box_cli-0.0.27/src/sing_box_service/api/client.py +367 -0
- {sing_box_cli-0.0.25/src/sing_box_service → sing_box_cli-0.0.27/src/sing_box_service/api}/connections.py +31 -32
- sing_box_cli-0.0.27/src/sing_box_service/api/logs.py +93 -0
- {sing_box_cli-0.0.25/src/sing_box_service → sing_box_cli-0.0.27/src/sing_box_service/api}/monitor.py +23 -26
- {sing_box_cli-0.0.25/src/sing_box_service → sing_box_cli-0.0.27/src/sing_box_service/api}/policy.py +39 -82
- sing_box_cli-0.0.27/src/sing_box_service/common.py +26 -0
- sing_box_cli-0.0.27/src/sing_box_service/config/__init__.py +46 -0
- {sing_box_cli-0.0.25/src/sing_box_service → sing_box_cli-0.0.27/src/sing_box_service/config}/config.py +24 -57
- sing_box_cli-0.0.27/src/sing_box_service/config/utils.py +51 -0
- sing_box_cli-0.0.27/src/sing_box_service/main.py +74 -0
- sing_box_cli-0.0.27/src/sing_box_service/service/__init__.py +71 -0
- sing_box_cli-0.0.25/src/sing_box_service/service.py → sing_box_cli-0.0.27/src/sing_box_service/service/manager.py +21 -29
- {sing_box_cli-0.0.25 → sing_box_cli-0.0.27}/tests/test_config.py +1 -1
- {sing_box_cli-0.0.25 → sing_box_cli-0.0.27}/uv.lock +2 -0
- sing_box_cli-0.0.25/src/sing_box_service/client.py +0 -245
- sing_box_cli-0.0.25/src/sing_box_service/main.py +0 -262
- sing_box_cli-0.0.25/src/sing_box_service/run.py +0 -24
- {sing_box_cli-0.0.25 → sing_box_cli-0.0.27}/.github/dependabot.yml +0 -0
- {sing_box_cli-0.0.25 → sing_box_cli-0.0.27}/.github/workflows/main.yml +0 -0
- {sing_box_cli-0.0.25 → sing_box_cli-0.0.27}/.gitignore +0 -0
- {sing_box_cli-0.0.25 → sing_box_cli-0.0.27}/.pre-commit-config.yaml +0 -0
- {sing_box_cli-0.0.25 → sing_box_cli-0.0.27}/LICENSE +0 -0
- {sing_box_cli-0.0.25 → sing_box_cli-0.0.27}/README.md +0 -0
- {sing_box_cli-0.0.25 → sing_box_cli-0.0.27}/assets/image.png +0 -0
- {sing_box_cli-0.0.25 → sing_box_cli-0.0.27}/scripts/bump.sh +0 -0
- {sing_box_cli-0.0.25 → sing_box_cli-0.0.27}/scripts/format.sh +0 -0
- {sing_box_cli-0.0.25 → sing_box_cli-0.0.27}/scripts/lint.sh +0 -0
- {sing_box_cli-0.0.25 → sing_box_cli-0.0.27}/scripts/pre-commit.sh +0 -0
- {sing_box_cli-0.0.25/src/sing_box_service → sing_box_cli-0.0.27/src/sing_box_service/api}/widget.py +0 -0
- {sing_box_cli-0.0.25 → sing_box_cli-0.0.27}/src/sing_box_service/py.typed +0 -0
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## 0.0.27 - 2025-03-21
|
|
6
|
+
|
|
7
|
+
### 🐛 Bug Fixes
|
|
8
|
+
|
|
9
|
+
- Catch `asyncio.exceptions.CancelledError` while canceling stats display
|
|
10
|
+
- Logic of load groups, when to update delay and load delay history
|
|
11
|
+
|
|
12
|
+
### 🚜 Refactor
|
|
13
|
+
|
|
14
|
+
- Replace SingBoxCli class with typer context and modularize commands files
|
|
15
|
+
- Make_stream_request
|
|
16
|
+
- Display logs from api_client
|
|
17
|
+
- Simplify config_dir
|
|
18
|
+
|
|
19
|
+
### ⚙️ Miscellaneous Tasks
|
|
20
|
+
|
|
21
|
+
- Remove EmptyResponse
|
|
22
|
+
- Simplify api commands in top level
|
|
23
|
+
|
|
24
|
+
## 0.0.26 - 2025-03-20
|
|
25
|
+
|
|
26
|
+
### 🚜 Refactor
|
|
27
|
+
|
|
28
|
+
- Validate responses using pydantic
|
|
29
|
+
|
|
5
30
|
## 0.0.25 - 2025-03-20
|
|
6
31
|
|
|
7
32
|
### 🐛 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.27
|
|
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
|
|
@@ -42,6 +42,7 @@ Requires-Dist: httpx>=0.28.1
|
|
|
42
42
|
Requires-Dist: plotext>=5.3.2
|
|
43
43
|
Requires-Dist: prompt-toolkit>=3.0.50
|
|
44
44
|
Requires-Dist: psutil>=6.1.1
|
|
45
|
+
Requires-Dist: pydantic>=2.10.6
|
|
45
46
|
Requires-Dist: typer>=0.15.1
|
|
46
47
|
Description-Content-Type: text/markdown
|
|
47
48
|
|
|
@@ -28,6 +28,7 @@ dependencies = [
|
|
|
28
28
|
"plotext>=5.3.2",
|
|
29
29
|
"prompt-toolkit>=3.0.50",
|
|
30
30
|
"psutil>=6.1.1",
|
|
31
|
+
"pydantic>=2.10.6",
|
|
31
32
|
"typer>=0.15.1",
|
|
32
33
|
]
|
|
33
34
|
|
|
@@ -212,7 +213,7 @@ sort_commits = "oldest"
|
|
|
212
213
|
|
|
213
214
|
|
|
214
215
|
[tool.bumpversion]
|
|
215
|
-
current_version = "0.0.
|
|
216
|
+
current_version = "0.0.27"
|
|
216
217
|
parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
|
|
217
218
|
serialize = ["{major}.{minor}.{patch}"]
|
|
218
219
|
search = "{current_version}"
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from functools import lru_cache
|
|
3
|
+
from typing import Annotated
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from ..common import LogLevel, StrOrNone
|
|
8
|
+
from ..config import SingBoxConfig
|
|
9
|
+
from .client import SingBoxAPIClient
|
|
10
|
+
from .connections import ConnectionsManager
|
|
11
|
+
from .logs import get_logs
|
|
12
|
+
from .monitor import ResourceMonitor, ResourceVisualizer
|
|
13
|
+
from .policy import PolicyGroupManager
|
|
14
|
+
|
|
15
|
+
ApiUrlOption = Annotated[
|
|
16
|
+
StrOrNone,
|
|
17
|
+
typer.Option(
|
|
18
|
+
"--base-url",
|
|
19
|
+
"-u",
|
|
20
|
+
help="Base URL of the sing-box API, read from configuration file if not provided",
|
|
21
|
+
),
|
|
22
|
+
]
|
|
23
|
+
ApiTokenOption = Annotated[
|
|
24
|
+
StrOrNone,
|
|
25
|
+
typer.Option(
|
|
26
|
+
"--token",
|
|
27
|
+
"-t",
|
|
28
|
+
help="Authentication token for the sing-box API, read from configuration file if not provided",
|
|
29
|
+
),
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
LogLevelOption = Annotated[
|
|
33
|
+
LogLevel,
|
|
34
|
+
typer.Option(
|
|
35
|
+
"--log-level",
|
|
36
|
+
"-l",
|
|
37
|
+
help="Log level of trace, debug, info, warning, error, fatal, panic",
|
|
38
|
+
case_sensitive=False,
|
|
39
|
+
),
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
api = typer.Typer(help="sing-box manager.")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@lru_cache
|
|
46
|
+
def create_client(
|
|
47
|
+
config: SingBoxConfig, base_url: StrOrNone = None, token: StrOrNone = None
|
|
48
|
+
) -> SingBoxAPIClient:
|
|
49
|
+
# read from config if not provided
|
|
50
|
+
if base_url is None:
|
|
51
|
+
base_url = config.api_base_url
|
|
52
|
+
if token is None:
|
|
53
|
+
token = config.api_secret
|
|
54
|
+
return SingBoxAPIClient(base_url, token)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@api.command()
|
|
58
|
+
def stats(
|
|
59
|
+
ctx: typer.Context, base_url: ApiUrlOption = None, token: ApiTokenOption = None
|
|
60
|
+
) -> None:
|
|
61
|
+
"""Show sing-box traffic, memory statistics and connections, requires API token(Optional)"""
|
|
62
|
+
|
|
63
|
+
api_client = create_client(ctx.obj.config, base_url, token)
|
|
64
|
+
visualizer = ResourceVisualizer()
|
|
65
|
+
monitor = ResourceMonitor(api_client, visualizer)
|
|
66
|
+
asyncio.run(monitor.start())
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@api.command()
|
|
70
|
+
def conns(
|
|
71
|
+
ctx: typer.Context, base_url: ApiUrlOption = None, token: ApiTokenOption = None
|
|
72
|
+
) -> None:
|
|
73
|
+
"""Manage sing-box connections, requires API token(Optional)"""
|
|
74
|
+
api_client = create_client(ctx.obj.config, base_url, token)
|
|
75
|
+
manager = ConnectionsManager(api_client)
|
|
76
|
+
asyncio.run(manager.run())
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@api.command()
|
|
80
|
+
def proxy(
|
|
81
|
+
ctx: typer.Context, base_url: ApiUrlOption = None, token: ApiTokenOption = None
|
|
82
|
+
) -> None:
|
|
83
|
+
"""Manage sing-box policy groups, requires API token(Optional)"""
|
|
84
|
+
api_client = create_client(ctx.obj.config, base_url, token)
|
|
85
|
+
manager = PolicyGroupManager(api_client)
|
|
86
|
+
asyncio.run(manager.run())
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@api.command()
|
|
90
|
+
def logs(
|
|
91
|
+
ctx: typer.Context,
|
|
92
|
+
log_level: LogLevelOption = LogLevel.info,
|
|
93
|
+
base_url: ApiUrlOption = None,
|
|
94
|
+
token: ApiTokenOption = None,
|
|
95
|
+
) -> None:
|
|
96
|
+
"""Show sing-box logs, requires API token(Optional)"""
|
|
97
|
+
api_client = create_client(ctx.obj.config, base_url, token)
|
|
98
|
+
print("⌛ Showing real-time logs (Press Ctrl+C to exit)")
|
|
99
|
+
asyncio.run(get_logs(api_client, log_level))
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sing-Box API Client module.
|
|
3
|
+
Provides an async client for interacting with Sing-Box HTTP API.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
from collections.abc import AsyncGenerator
|
|
8
|
+
from typing import Any, TypeVar
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class FrozenBaseModel(BaseModel):
|
|
15
|
+
"""Base model with frozen configuration."""
|
|
16
|
+
|
|
17
|
+
model_config = ConfigDict(frozen=True)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# Define Pydantic models for API responses
|
|
21
|
+
class TrafficData(FrozenBaseModel):
|
|
22
|
+
"""http://base_url/traffic/"""
|
|
23
|
+
|
|
24
|
+
up: int = Field(default=0, description="Upload speed in B/s")
|
|
25
|
+
down: int = Field(default=0, description="Download speed in B/s")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class MemoryData(FrozenBaseModel):
|
|
29
|
+
"""http://base_url/memory/"""
|
|
30
|
+
|
|
31
|
+
inuse: int = Field(default=0, description="Memory in use in bytes")
|
|
32
|
+
total: int = Field(default=0, description="Total memory in bytes")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ConnectionMetadata(FrozenBaseModel):
|
|
36
|
+
destinationIP: str = ""
|
|
37
|
+
destinationPort: str
|
|
38
|
+
dnsMode: str
|
|
39
|
+
host: str = ""
|
|
40
|
+
network: str = Field(description="Network type")
|
|
41
|
+
processPath: str = ""
|
|
42
|
+
sourceIP: str
|
|
43
|
+
sourcePort: str
|
|
44
|
+
type: str = Field(description="Inbound type")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class ConnectionInfo(FrozenBaseModel):
|
|
48
|
+
"""https://base_url/connections/"""
|
|
49
|
+
|
|
50
|
+
id: str
|
|
51
|
+
download: int = Field(default=0, description="Download in bytes")
|
|
52
|
+
upload: int = Field(default=0, description="Upload in bytes")
|
|
53
|
+
rule: str = Field(description="Rule name => outbound")
|
|
54
|
+
start: str = Field(description="Start time")
|
|
55
|
+
chains: list[str] = Field(description="Proxy chains list")
|
|
56
|
+
metadata: ConnectionMetadata
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ConnectionData(FrozenBaseModel):
|
|
60
|
+
uploadTotal: int = Field(default=0, description="Total upload in bytes")
|
|
61
|
+
downloadTotal: int = Field(default=0, description="Total download in bytes")
|
|
62
|
+
memory: int = Field(default=0, description="Memory usage in bytes")
|
|
63
|
+
connections: list[ConnectionInfo] = Field(description="List of active connections")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class DelayHistory(FrozenBaseModel):
|
|
67
|
+
time: str = ""
|
|
68
|
+
delay: int = Field(default=0, description="Outbound delay in milliseconds")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class GroupInfo(FrozenBaseModel):
|
|
72
|
+
"""http://base_url/group/{group_name}/"""
|
|
73
|
+
|
|
74
|
+
type: str = Field(description="Proxy Group type")
|
|
75
|
+
name: str
|
|
76
|
+
udp: bool = Field(description="UDP support")
|
|
77
|
+
history: list[DelayHistory] = Field(
|
|
78
|
+
description="History delay of selected proxies", default=[]
|
|
79
|
+
)
|
|
80
|
+
now: str = Field(description="Currently selected proxy")
|
|
81
|
+
all: list[str] = Field(description="List of all outbound proxies")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class GroupsData(FrozenBaseModel):
|
|
85
|
+
"""http://base_url/group/"""
|
|
86
|
+
|
|
87
|
+
proxies: list[GroupInfo] = Field(description="List of proxies in the group")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class DelayTestResult(FrozenBaseModel):
|
|
91
|
+
"""Delay test result model."""
|
|
92
|
+
|
|
93
|
+
outbound: str = Field(default="", description="Outbound proxy name")
|
|
94
|
+
delay: int = Field(default=0, description="Outbound delay in milliseconds")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class LogEntry(FrozenBaseModel):
|
|
98
|
+
type: str = Field(description="Logs level")
|
|
99
|
+
payload: str = Field(description="Log message")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# Generic response type for API methods
|
|
103
|
+
T = TypeVar("T", bound=FrozenBaseModel)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class SingBoxAPIClient:
|
|
107
|
+
"""Async client for interacting with Sing-Box API.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
base_url: The base URL of the Sing-Box API
|
|
111
|
+
token: The API authentication token
|
|
112
|
+
timeout: Request timeout in seconds
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
def __init__(
|
|
116
|
+
self,
|
|
117
|
+
base_url: str = "http://127.0.0.1:9090",
|
|
118
|
+
token: str | None = None,
|
|
119
|
+
timeout: float = 10.0,
|
|
120
|
+
) -> None:
|
|
121
|
+
self.base_url = base_url
|
|
122
|
+
self.headers = {"Authorization": f"Bearer {token}"} if token else {}
|
|
123
|
+
self.timeout = timeout
|
|
124
|
+
if not self._health_check():
|
|
125
|
+
raise ValueError(f"Invalid to initialize client: {self.base_url}")
|
|
126
|
+
|
|
127
|
+
def _health_check(self) -> bool:
|
|
128
|
+
"""Check if the API is reachable.
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
True if the API is reachable, False otherwise
|
|
132
|
+
"""
|
|
133
|
+
try:
|
|
134
|
+
response = httpx.get(
|
|
135
|
+
f"{self.base_url}", timeout=self.timeout, headers=self.headers
|
|
136
|
+
)
|
|
137
|
+
return response.status_code == 200
|
|
138
|
+
except httpx.HTTPError:
|
|
139
|
+
return False
|
|
140
|
+
|
|
141
|
+
async def _make_request_raw(
|
|
142
|
+
self,
|
|
143
|
+
method: str,
|
|
144
|
+
endpoint: str,
|
|
145
|
+
params: dict[str, Any] | None = None,
|
|
146
|
+
data: dict[str, Any] | None = None,
|
|
147
|
+
) -> dict[str, Any]:
|
|
148
|
+
"""
|
|
149
|
+
Make a request to the API.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
method: HTTP method (GET, POST, etc.)
|
|
153
|
+
endpoint: API endpoint
|
|
154
|
+
params: URL parameters
|
|
155
|
+
data: Request body data
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
JSON response as dictionary
|
|
159
|
+
|
|
160
|
+
Raises:
|
|
161
|
+
httpx.HTTPError: If the request fails
|
|
162
|
+
"""
|
|
163
|
+
url = f"{self.base_url}{endpoint}"
|
|
164
|
+
async with httpx.AsyncClient() as client:
|
|
165
|
+
response = await client.request(
|
|
166
|
+
method=method,
|
|
167
|
+
url=url,
|
|
168
|
+
params=params,
|
|
169
|
+
json=data,
|
|
170
|
+
headers=self.headers,
|
|
171
|
+
timeout=self.timeout,
|
|
172
|
+
)
|
|
173
|
+
response.raise_for_status()
|
|
174
|
+
if response.content:
|
|
175
|
+
return dict(response.json())
|
|
176
|
+
return {}
|
|
177
|
+
|
|
178
|
+
async def _make_request(
|
|
179
|
+
self,
|
|
180
|
+
method: str,
|
|
181
|
+
endpoint: str,
|
|
182
|
+
model_class: type[T],
|
|
183
|
+
params: dict[str, Any] | None = None,
|
|
184
|
+
data: dict[str, Any] | None = None,
|
|
185
|
+
) -> T:
|
|
186
|
+
"""Make a request and return data as a specific model."""
|
|
187
|
+
raw = await self._make_request_raw(method, endpoint, params, data)
|
|
188
|
+
return model_class.model_validate(raw)
|
|
189
|
+
|
|
190
|
+
async def _make_stream_request(
|
|
191
|
+
self, endpoint: str, model_class: type[T], params: dict[str, str] | None = None
|
|
192
|
+
) -> AsyncGenerator[T, None]:
|
|
193
|
+
"""
|
|
194
|
+
Make a streaming request to the API.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
endpoint: API endpoint
|
|
198
|
+
model_class: Pydantic model class to validate response data
|
|
199
|
+
params: Optional query parameters for the request
|
|
200
|
+
|
|
201
|
+
Yields:
|
|
202
|
+
Validated model instances from the stream
|
|
203
|
+
"""
|
|
204
|
+
url = f"{self.base_url}{endpoint}"
|
|
205
|
+
|
|
206
|
+
async with httpx.AsyncClient() as client:
|
|
207
|
+
async with client.stream(
|
|
208
|
+
"GET", url, headers=self.headers, params=params, timeout=None
|
|
209
|
+
) as response:
|
|
210
|
+
response.raise_for_status()
|
|
211
|
+
async for line in response.aiter_lines():
|
|
212
|
+
if line.strip(): # Skip empty lines
|
|
213
|
+
try:
|
|
214
|
+
data = json.loads(line)
|
|
215
|
+
yield model_class.model_validate(data)
|
|
216
|
+
except json.JSONDecodeError:
|
|
217
|
+
# Skip invalid JSON lines
|
|
218
|
+
continue
|
|
219
|
+
|
|
220
|
+
async def traffic_stream(self) -> AsyncGenerator[TrafficData, None]:
|
|
221
|
+
"""
|
|
222
|
+
Get traffic statistics as a stream of updates.
|
|
223
|
+
|
|
224
|
+
Yields:
|
|
225
|
+
TrafficData object containing traffic data (up/down in B/s)
|
|
226
|
+
"""
|
|
227
|
+
async for data in self._make_stream_request("/traffic", TrafficData):
|
|
228
|
+
yield data
|
|
229
|
+
|
|
230
|
+
async def memory_stream(self) -> AsyncGenerator[MemoryData, None]:
|
|
231
|
+
"""
|
|
232
|
+
Get memory statistics as a stream of updates.
|
|
233
|
+
|
|
234
|
+
Yields:
|
|
235
|
+
MemoryData object containing memory data (inuse/total in bytes)
|
|
236
|
+
"""
|
|
237
|
+
async for data in self._make_stream_request("/memory", MemoryData):
|
|
238
|
+
yield data
|
|
239
|
+
|
|
240
|
+
async def log_stream(
|
|
241
|
+
self, level: str | None = None
|
|
242
|
+
) -> AsyncGenerator[LogEntry, None]:
|
|
243
|
+
"""
|
|
244
|
+
Get log entries as a stream of updates.
|
|
245
|
+
|
|
246
|
+
Yields:
|
|
247
|
+
LogEntry object containing log data (type/payload)
|
|
248
|
+
"""
|
|
249
|
+
params = {"level": level} if level else None
|
|
250
|
+
async for data in self._make_stream_request("/logs", LogEntry, params=params):
|
|
251
|
+
yield data
|
|
252
|
+
|
|
253
|
+
async def get_connections(self) -> ConnectionData:
|
|
254
|
+
"""
|
|
255
|
+
Get current connections.
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
ConnectionData object containing active connections
|
|
259
|
+
"""
|
|
260
|
+
return await self._make_request(
|
|
261
|
+
"GET", "/connections", model_class=ConnectionData
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
async def close_connection(self, connection_id: str) -> dict[str, Any]:
|
|
265
|
+
"""
|
|
266
|
+
Close a specific connection.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
connection_id: ID of the connection to close
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
Response from the API
|
|
273
|
+
"""
|
|
274
|
+
return await self._make_request_raw("DELETE", f"/connections/{connection_id}")
|
|
275
|
+
|
|
276
|
+
async def close_all_connections(self) -> dict[str, Any]:
|
|
277
|
+
"""
|
|
278
|
+
Close all connections.
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
Response from the API
|
|
282
|
+
"""
|
|
283
|
+
return await self._make_request_raw("DELETE", "/connections")
|
|
284
|
+
|
|
285
|
+
async def get_groups(self) -> GroupsData:
|
|
286
|
+
"""
|
|
287
|
+
Get all policy groups.
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
GroupData object containing policy groups information
|
|
291
|
+
"""
|
|
292
|
+
return await self._make_request("GET", "/group", model_class=GroupsData)
|
|
293
|
+
|
|
294
|
+
async def get_group(self, group_name: str) -> GroupInfo:
|
|
295
|
+
"""
|
|
296
|
+
Get information about a specific policy group.
|
|
297
|
+
|
|
298
|
+
Args:
|
|
299
|
+
group_name: Name of the policy group
|
|
300
|
+
|
|
301
|
+
Returns:
|
|
302
|
+
GroupData object containing the policy group information
|
|
303
|
+
"""
|
|
304
|
+
return await self._make_request(
|
|
305
|
+
"GET", f"/group/{group_name}", model_class=GroupInfo
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
async def test_group_delay(
|
|
309
|
+
self,
|
|
310
|
+
group_name: str,
|
|
311
|
+
url: str = "https://cp.cloudflare.com/generate_204",
|
|
312
|
+
timeout: int = 5000,
|
|
313
|
+
) -> list[DelayTestResult]:
|
|
314
|
+
"""
|
|
315
|
+
Test delay for all proxies in a policy group.
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
group_name: Name of the policy group
|
|
319
|
+
timeout: Timeout in milliseconds
|
|
320
|
+
"""
|
|
321
|
+
params = {"url": url, "timeout": timeout}
|
|
322
|
+
reps = await self._make_request_raw(
|
|
323
|
+
"GET", f"/group/{group_name}/delay", params=params
|
|
324
|
+
)
|
|
325
|
+
return [
|
|
326
|
+
DelayTestResult(outbound=key, delay=value) for key, value in reps.items()
|
|
327
|
+
]
|
|
328
|
+
|
|
329
|
+
async def test_proxy_delay(
|
|
330
|
+
self,
|
|
331
|
+
proxy_name: str,
|
|
332
|
+
url: str = "https://cp.cloudflare.com/generate_204",
|
|
333
|
+
timeout: int = 5000,
|
|
334
|
+
) -> DelayTestResult:
|
|
335
|
+
"""
|
|
336
|
+
Test delay for a specific proxy.
|
|
337
|
+
|
|
338
|
+
Args:
|
|
339
|
+
timeout: Timeout in milliseconds
|
|
340
|
+
"""
|
|
341
|
+
params = {"url": url, "timeout": timeout}
|
|
342
|
+
return await self._make_request(
|
|
343
|
+
"GET",
|
|
344
|
+
f"/proxies/{proxy_name}/delay",
|
|
345
|
+
params=params,
|
|
346
|
+
model_class=DelayTestResult,
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
async def select_proxy(self, proxy_name: str, selected: str) -> dict[str, Any]:
|
|
350
|
+
"""
|
|
351
|
+
Select a proxy for a selector proxy group.
|
|
352
|
+
|
|
353
|
+
Args:
|
|
354
|
+
proxy_name: Name of the proxy selector
|
|
355
|
+
selected: Name of the proxy to select
|
|
356
|
+
"""
|
|
357
|
+
data = {"name": selected}
|
|
358
|
+
return await self._make_request_raw("PUT", f"/proxies/{proxy_name}", data=data)
|
|
359
|
+
|
|
360
|
+
async def get_version(self) -> dict[str, Any]:
|
|
361
|
+
"""
|
|
362
|
+
Get Sing-Box version.
|
|
363
|
+
|
|
364
|
+
Returns:
|
|
365
|
+
Dictionary containing version information
|
|
366
|
+
"""
|
|
367
|
+
return await self._make_request_raw("GET", "/version")
|