dnspod-tool 0.1.0__py3-none-any.whl
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.
- dnspod_tool/__init__.py +3 -0
- dnspod_tool/__main__.py +5 -0
- dnspod_tool/api.py +206 -0
- dnspod_tool/cli.py +794 -0
- dnspod_tool/config.py +372 -0
- dnspod_tool/errors.py +14 -0
- dnspod_tool/main.py +3 -0
- dnspod_tool/models.py +73 -0
- dnspod_tool/tui.py +288 -0
- dnspod_tool-0.1.0.dist-info/METADATA +197 -0
- dnspod_tool-0.1.0.dist-info/RECORD +15 -0
- dnspod_tool-0.1.0.dist-info/WHEEL +5 -0
- dnspod_tool-0.1.0.dist-info/entry_points.txt +2 -0
- dnspod_tool-0.1.0.dist-info/licenses/LICENSE +674 -0
- dnspod_tool-0.1.0.dist-info/top_level.txt +1 -0
dnspod_tool/__init__.py
ADDED
dnspod_tool/__main__.py
ADDED
dnspod_tool/api.py
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from .config import Credential
|
|
7
|
+
from .errors import ApiError, ConfigError
|
|
8
|
+
from .models import Domain, Record
|
|
9
|
+
|
|
10
|
+
DEFAULT_RECORD_LINE = "\u9ed8\u8ba4"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DnspodClient:
|
|
14
|
+
def __init__(self, credential: Credential, timeout: int = 10):
|
|
15
|
+
self.credential = credential
|
|
16
|
+
self.timeout = timeout
|
|
17
|
+
self._sdk_client: Any | None = None
|
|
18
|
+
|
|
19
|
+
def list_domains(self) -> list[Domain]:
|
|
20
|
+
items = self._paged("DescribeDomainList", ["domains", "DomainList"])
|
|
21
|
+
return [Domain.from_api(item) for item in items]
|
|
22
|
+
|
|
23
|
+
def list_records(self, domain: str) -> list[Record]:
|
|
24
|
+
items = self._paged("DescribeRecordList", ["records", "RecordList"], {"Domain": domain})
|
|
25
|
+
return [Record.from_api(item) for item in items]
|
|
26
|
+
|
|
27
|
+
def create_record(
|
|
28
|
+
self,
|
|
29
|
+
domain: str,
|
|
30
|
+
name: str,
|
|
31
|
+
record_type: str,
|
|
32
|
+
value: str,
|
|
33
|
+
line: str = DEFAULT_RECORD_LINE,
|
|
34
|
+
mx: int | None = None,
|
|
35
|
+
ttl: int | None = None,
|
|
36
|
+
) -> dict[str, Any]:
|
|
37
|
+
params: dict[str, Any] = {
|
|
38
|
+
"Domain": domain,
|
|
39
|
+
"SubDomain": name,
|
|
40
|
+
"RecordType": record_type,
|
|
41
|
+
"RecordLine": line,
|
|
42
|
+
"Value": value,
|
|
43
|
+
}
|
|
44
|
+
if mx is not None:
|
|
45
|
+
params["MX"] = mx
|
|
46
|
+
if ttl is not None:
|
|
47
|
+
params["TTL"] = ttl
|
|
48
|
+
return self._call("CreateRecord", params)
|
|
49
|
+
|
|
50
|
+
def modify_record(
|
|
51
|
+
self,
|
|
52
|
+
domain: str,
|
|
53
|
+
record_id: str,
|
|
54
|
+
name: str,
|
|
55
|
+
record_type: str,
|
|
56
|
+
value: str,
|
|
57
|
+
line: str = DEFAULT_RECORD_LINE,
|
|
58
|
+
mx: int | None = None,
|
|
59
|
+
ttl: int | None = None,
|
|
60
|
+
) -> dict[str, Any]:
|
|
61
|
+
params: dict[str, Any] = {
|
|
62
|
+
"Domain": domain,
|
|
63
|
+
"RecordId": record_id,
|
|
64
|
+
"SubDomain": name,
|
|
65
|
+
"RecordType": record_type,
|
|
66
|
+
"RecordLine": line,
|
|
67
|
+
"Value": value,
|
|
68
|
+
}
|
|
69
|
+
if mx is not None:
|
|
70
|
+
params["MX"] = mx
|
|
71
|
+
if ttl is not None:
|
|
72
|
+
params["TTL"] = ttl
|
|
73
|
+
return self._call("ModifyRecord", params)
|
|
74
|
+
|
|
75
|
+
def delete_record(self, domain: str, record_id: str) -> dict[str, Any]:
|
|
76
|
+
return self._call("DeleteRecord", {"Domain": domain, "RecordId": record_id})
|
|
77
|
+
|
|
78
|
+
def _paged(
|
|
79
|
+
self,
|
|
80
|
+
action: str,
|
|
81
|
+
item_keys: list[str],
|
|
82
|
+
params: dict[str, Any] | None = None,
|
|
83
|
+
limit: int = 100,
|
|
84
|
+
) -> list[dict[str, Any]]:
|
|
85
|
+
items: list[dict[str, Any]] = []
|
|
86
|
+
offset = 0
|
|
87
|
+
base_params = params or {}
|
|
88
|
+
while True:
|
|
89
|
+
response = self._call(action, {**base_params, "Offset": offset, "Limit": limit})
|
|
90
|
+
batch = _first_list(response, item_keys)
|
|
91
|
+
items.extend(batch)
|
|
92
|
+
if len(batch) < limit:
|
|
93
|
+
break
|
|
94
|
+
offset += limit
|
|
95
|
+
return items
|
|
96
|
+
|
|
97
|
+
def _call(self, action: str, params: dict[str, Any]) -> dict[str, Any]:
|
|
98
|
+
clean_params = {key: value for key, value in params.items() if value is not None}
|
|
99
|
+
if "RecordId" in clean_params:
|
|
100
|
+
clean_params["RecordId"] = self._normalize_record_id(clean_params["RecordId"])
|
|
101
|
+
|
|
102
|
+
if self.credential.mode == "token":
|
|
103
|
+
return self._token_call(action, clean_params)
|
|
104
|
+
if self.credential.mode == "key":
|
|
105
|
+
return self._sdk_call(action, clean_params)
|
|
106
|
+
raise ConfigError(f"Unsupported credential mode: {self.credential.mode}")
|
|
107
|
+
|
|
108
|
+
def _token_call(self, action: str, params: dict[str, Any]) -> dict[str, Any]:
|
|
109
|
+
try:
|
|
110
|
+
import requests
|
|
111
|
+
except Exception as exc:
|
|
112
|
+
raise ConfigError("The requests package is required for token API mode.") from exc
|
|
113
|
+
|
|
114
|
+
api_map = {
|
|
115
|
+
"DescribeDomainList": "Domain.List",
|
|
116
|
+
"DescribeRecordList": "Record.List",
|
|
117
|
+
"CreateRecord": "Record.Create",
|
|
118
|
+
"DeleteRecord": "Record.Remove",
|
|
119
|
+
"ModifyRecord": "Record.Modify",
|
|
120
|
+
}
|
|
121
|
+
url = f"https://dnsapi.cn/{api_map.get(action, action)}"
|
|
122
|
+
payload = {"login_token": self.credential.login_token, "format": "json"}
|
|
123
|
+
payload.update(_to_token_params(params))
|
|
124
|
+
try:
|
|
125
|
+
response = requests.post(url, data=payload, timeout=self.timeout)
|
|
126
|
+
response.raise_for_status()
|
|
127
|
+
data = response.json()
|
|
128
|
+
except Exception as exc:
|
|
129
|
+
raise ApiError(f"DNSPod token API request failed: {exc}") from exc
|
|
130
|
+
|
|
131
|
+
status = data.get("status", {}) if isinstance(data, dict) else {}
|
|
132
|
+
if str(status.get("code")) != "1":
|
|
133
|
+
message = status.get("message") or data
|
|
134
|
+
raise ApiError(f"DNSPod API error: {message}")
|
|
135
|
+
return data
|
|
136
|
+
|
|
137
|
+
def _sdk_call(self, action: str, params: dict[str, Any]) -> dict[str, Any]:
|
|
138
|
+
try:
|
|
139
|
+
from tencentcloud.dnspod.v20210323 import models
|
|
140
|
+
except Exception as exc:
|
|
141
|
+
raise ConfigError("The Tencent Cloud SDK package is required for key mode.") from exc
|
|
142
|
+
|
|
143
|
+
method = getattr(self.sdk_client, action, None)
|
|
144
|
+
request_class = getattr(models, action + "Request", None)
|
|
145
|
+
if method is None or request_class is None:
|
|
146
|
+
raise ApiError(f"Unsupported SDK action: {action}")
|
|
147
|
+
|
|
148
|
+
request = request_class()
|
|
149
|
+
request.from_json_string(json.dumps(params))
|
|
150
|
+
try:
|
|
151
|
+
response = method(request)
|
|
152
|
+
return json.loads(response.to_json_string())
|
|
153
|
+
except Exception as exc:
|
|
154
|
+
raise ApiError(f"Tencent Cloud SDK request failed: {exc}") from exc
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def sdk_client(self) -> Any:
|
|
158
|
+
if self._sdk_client is not None:
|
|
159
|
+
return self._sdk_client
|
|
160
|
+
if not self.credential.secret_id or not self.credential.secret_key:
|
|
161
|
+
raise ConfigError("Tencent Cloud key credentials are incomplete.")
|
|
162
|
+
try:
|
|
163
|
+
from tencentcloud.common import credential
|
|
164
|
+
from tencentcloud.dnspod.v20210323 import dnspod_client
|
|
165
|
+
except Exception as exc:
|
|
166
|
+
raise ConfigError("The Tencent Cloud SDK package is required for key mode.") from exc
|
|
167
|
+
cred = credential.Credential(self.credential.secret_id, self.credential.secret_key)
|
|
168
|
+
self._sdk_client = dnspod_client.DnspodClient(cred, "")
|
|
169
|
+
return self._sdk_client
|
|
170
|
+
|
|
171
|
+
def _normalize_record_id(self, record_id: Any) -> Any:
|
|
172
|
+
if self.credential.mode == "key":
|
|
173
|
+
try:
|
|
174
|
+
return int(record_id)
|
|
175
|
+
except (TypeError, ValueError):
|
|
176
|
+
return record_id
|
|
177
|
+
return str(record_id)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _to_token_params(params: dict[str, Any]) -> dict[str, Any]:
|
|
181
|
+
key_map = {
|
|
182
|
+
"Domain": "domain",
|
|
183
|
+
"SubDomain": "sub_domain",
|
|
184
|
+
"RecordType": "record_type",
|
|
185
|
+
"RecordLine": "record_line",
|
|
186
|
+
"Value": "value",
|
|
187
|
+
"RecordId": "record_id",
|
|
188
|
+
"MX": "mx",
|
|
189
|
+
"TTL": "ttl",
|
|
190
|
+
"Status": "status",
|
|
191
|
+
"Offset": "offset",
|
|
192
|
+
"Limit": "length",
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
token_key: value
|
|
196
|
+
for source_key, token_key in key_map.items()
|
|
197
|
+
if (value := params.get(source_key)) is not None
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _first_list(data: dict[str, Any], keys: list[str]) -> list[dict[str, Any]]:
|
|
202
|
+
for key in keys:
|
|
203
|
+
value = data.get(key)
|
|
204
|
+
if isinstance(value, list):
|
|
205
|
+
return [item for item in value if isinstance(item, dict)]
|
|
206
|
+
return []
|