aliyun-controller 0.2.3__tar.gz → 0.2.5__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.
- {aliyun_controller-0.2.3 → aliyun_controller-0.2.5}/PKG-INFO +1 -4
- {aliyun_controller-0.2.3 → aliyun_controller-0.2.5}/README.md +0 -3
- {aliyun_controller-0.2.3 → aliyun_controller-0.2.5}/aliyun_controller/__init__.py +1 -1
- {aliyun_controller-0.2.3 → aliyun_controller-0.2.5}/aliyun_controller/dns.py +20 -0
- {aliyun_controller-0.2.3 → aliyun_controller-0.2.5}/aliyun_controller/screens/dns.py +32 -13
- {aliyun_controller-0.2.3 → aliyun_controller-0.2.5}/aliyun_controller/ui.py +14 -2
- {aliyun_controller-0.2.3 → aliyun_controller-0.2.5}/aliyun_controller/widgets.py +27 -6
- {aliyun_controller-0.2.3 → aliyun_controller-0.2.5}/aliyun_controller.egg-info/PKG-INFO +1 -4
- {aliyun_controller-0.2.3 → aliyun_controller-0.2.5}/pyproject.toml +1 -1
- {aliyun_controller-0.2.3 → aliyun_controller-0.2.5}/aliyun_controller/app.py +0 -0
- {aliyun_controller-0.2.3 → aliyun_controller-0.2.5}/aliyun_controller/app.tcss +0 -0
- {aliyun_controller-0.2.3 → aliyun_controller-0.2.5}/aliyun_controller/billing.py +0 -0
- {aliyun_controller-0.2.3 → aliyun_controller-0.2.5}/aliyun_controller/client.py +0 -0
- {aliyun_controller-0.2.3 → aliyun_controller-0.2.5}/aliyun_controller/config.py +0 -0
- {aliyun_controller-0.2.3 → aliyun_controller-0.2.5}/aliyun_controller/main.py +0 -0
- {aliyun_controller-0.2.3 → aliyun_controller-0.2.5}/aliyun_controller/screens/__init__.py +0 -0
- {aliyun_controller-0.2.3 → aliyun_controller-0.2.5}/aliyun_controller/screens/billing.py +0 -0
- {aliyun_controller-0.2.3 → aliyun_controller-0.2.5}/aliyun_controller/screens/home.py +0 -0
- {aliyun_controller-0.2.3 → aliyun_controller-0.2.5}/aliyun_controller/screens/settings.py +0 -0
- {aliyun_controller-0.2.3 → aliyun_controller-0.2.5}/aliyun_controller/screens/setup.py +0 -0
- {aliyun_controller-0.2.3 → aliyun_controller-0.2.5}/aliyun_controller.egg-info/SOURCES.txt +0 -0
- {aliyun_controller-0.2.3 → aliyun_controller-0.2.5}/aliyun_controller.egg-info/dependency_links.txt +0 -0
- {aliyun_controller-0.2.3 → aliyun_controller-0.2.5}/aliyun_controller.egg-info/entry_points.txt +0 -0
- {aliyun_controller-0.2.3 → aliyun_controller-0.2.5}/aliyun_controller.egg-info/requires.txt +0 -0
- {aliyun_controller-0.2.3 → aliyun_controller-0.2.5}/aliyun_controller.egg-info/top_level.txt +0 -0
- {aliyun_controller-0.2.3 → aliyun_controller-0.2.5}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aliyun_controller
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.5
|
|
4
4
|
Summary: A Textual TUI for managing Alibaba Cloud services including billing queries and DNS management
|
|
5
5
|
Author-email: Moha-Master <hongkongreporter@outlook.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -61,9 +61,6 @@ Requires-Dist: pytest-asyncio; extra == "dev"
|
|
|
61
61
|
| **弹窗** | `Esc` / `Ctrl+C` | 取消并关闭 |
|
|
62
62
|
| | `回车` | 提交表单或确认 |
|
|
63
63
|
|
|
64
|
-
> 鼠标全程可用:按钮 / 选项 / 列表行一律**单击即执行**(表格行单击即进入下一级或打开编辑),滚轮滚动光标所在的滚动区域。
|
|
65
|
-
> 焦点进入输入框即切换为**文本编辑模式**:使用控件原生编辑键(`Home`/`End`、`Ctrl+Shift+A` 全选、`Ctrl+X/C/V` 剪切 / 复制 / 粘贴),底部提示栏同步切换,此时 `Ctrl+R` 刷新让位给输入框;`Ctrl+C` 有选中则复制、无选中则返回 / 取消。
|
|
66
|
-
|
|
67
64
|
## 安装指南
|
|
68
65
|
|
|
69
66
|
### 通过 pip 安装(推荐)
|
|
@@ -33,9 +33,6 @@
|
|
|
33
33
|
| **弹窗** | `Esc` / `Ctrl+C` | 取消并关闭 |
|
|
34
34
|
| | `回车` | 提交表单或确认 |
|
|
35
35
|
|
|
36
|
-
> 鼠标全程可用:按钮 / 选项 / 列表行一律**单击即执行**(表格行单击即进入下一级或打开编辑),滚轮滚动光标所在的滚动区域。
|
|
37
|
-
> 焦点进入输入框即切换为**文本编辑模式**:使用控件原生编辑键(`Home`/`End`、`Ctrl+Shift+A` 全选、`Ctrl+X/C/V` 剪切 / 复制 / 粘贴),底部提示栏同步切换,此时 `Ctrl+R` 刷新让位给输入框;`Ctrl+C` 有选中则复制、无选中则返回 / 取消。
|
|
38
|
-
|
|
39
36
|
## 安装指南
|
|
40
37
|
|
|
41
38
|
### 通过 pip 安装(推荐)
|
|
@@ -128,6 +128,26 @@ def validate_dns_record(rr: str, record_type: str, value: str, ttl: int) -> str
|
|
|
128
128
|
return None
|
|
129
129
|
|
|
130
130
|
|
|
131
|
+
def record_content_changed(record: dict, rr: str, record_type: str, value: str, ttl: int) -> bool:
|
|
132
|
+
"""编辑后的内容是否与现有记录不同。
|
|
133
|
+
|
|
134
|
+
阿里云 UpdateDomainRecord 在 RR/Type/Value 与原记录完全相同时会返回
|
|
135
|
+
``DomainRecordDuplicate``(解析记录已存在),因此调用方必须先做差异判断,
|
|
136
|
+
内容未变化时跳过更新。
|
|
137
|
+
"""
|
|
138
|
+
current_ttl = record.get("TTL")
|
|
139
|
+
try:
|
|
140
|
+
same_ttl = int(current_ttl) == int(ttl)
|
|
141
|
+
except (TypeError, ValueError):
|
|
142
|
+
same_ttl = str(current_ttl) == str(ttl)
|
|
143
|
+
return not (
|
|
144
|
+
str(record.get("RR", "")).strip() == str(rr).strip()
|
|
145
|
+
and str(record.get("Type", "")).upper() == str(record_type).upper()
|
|
146
|
+
and str(record.get("Value", "")).strip() == str(value).strip()
|
|
147
|
+
and same_ttl
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
|
|
131
151
|
def sort_records(records: list, sort_type: int, sort_order: int) -> list:
|
|
132
152
|
"""返回排序后的新列表。sort_type: 0 创建时间 / 1 二级域名 / 2 首字母;sort_order: 0 逆序 / 1 正序。"""
|
|
133
153
|
records = list(records)
|
|
@@ -15,6 +15,7 @@ from ..dns import (
|
|
|
15
15
|
DNS_RECORD_TYPES,
|
|
16
16
|
SORT_ORDER_LABELS,
|
|
17
17
|
SORT_TYPE_LABELS,
|
|
18
|
+
record_content_changed,
|
|
18
19
|
sort_records,
|
|
19
20
|
validate_dns_record,
|
|
20
21
|
)
|
|
@@ -46,7 +47,7 @@ class DomainListScreen(PageScreen):
|
|
|
46
47
|
"""域名列表,回车进入该域名的解析记录。"""
|
|
47
48
|
|
|
48
49
|
TITLE = "DNS 解析管理"
|
|
49
|
-
HINT = "↑↓ 移动 ·
|
|
50
|
+
HINT = "↑↓ 移动 · 双击/回车 管理解析记录 · / 搜索 · Ctrl+R 刷新 · Esc/Ctrl+C 返回"
|
|
50
51
|
|
|
51
52
|
BINDINGS = [Binding("/", "focus_search", "搜索", show=False)]
|
|
52
53
|
|
|
@@ -203,7 +204,7 @@ class RecordListScreen(PageScreen):
|
|
|
203
204
|
"""某域名的解析记录列表,支持筛选、排序、增删改与启停。"""
|
|
204
205
|
|
|
205
206
|
TITLE = "解析记录"
|
|
206
|
-
HINT = "
|
|
207
|
+
HINT = "双击/回车 编辑 · Ctrl+N 新增 · Ctrl+E 编辑 · Ctrl+D 删除 · Ctrl+R 刷新 · Esc/Ctrl+C 返回"
|
|
207
208
|
|
|
208
209
|
BINDINGS = [
|
|
209
210
|
Binding("/", "focus_search", "搜索", show=False),
|
|
@@ -364,29 +365,47 @@ class RecordListScreen(PageScreen):
|
|
|
364
365
|
@work(exclusive=True)
|
|
365
366
|
async def _save_record(self, record: dict | None, is_new: bool, data: dict) -> None:
|
|
366
367
|
dns = self.app.get_dns()
|
|
368
|
+
record = record or {}
|
|
367
369
|
enabled = bool(data.get("status", True))
|
|
370
|
+
content_changed = False
|
|
371
|
+
status_changed = False
|
|
372
|
+
if not is_new:
|
|
373
|
+
# 内容与状态分开处理:内容未变化时不调 UpdateDomainRecord,
|
|
374
|
+
# 否则阿里云会以 DomainRecordDuplicate(解析记录已存在)拒绝请求。
|
|
375
|
+
content_changed = record_content_changed(
|
|
376
|
+
record, data["rr"], data["type"], data["value"], data["ttl"]
|
|
377
|
+
)
|
|
378
|
+
status_changed = enabled != _is_enabled(record)
|
|
379
|
+
if not content_changed and not status_changed:
|
|
380
|
+
self.app.notify_warn("未做任何修改")
|
|
381
|
+
return
|
|
368
382
|
|
|
369
|
-
def _apply() ->
|
|
383
|
+
def _apply() -> str:
|
|
370
384
|
if is_new:
|
|
371
385
|
rid = dns.add_domain_record(self.domain_name, data["rr"], data["type"], data["value"], data["ttl"])
|
|
372
386
|
if rid and not enabled:
|
|
373
387
|
dns.set_domain_record_status(rid, "Disable")
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
)
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
388
|
+
return "已新增解析记录" if enabled else "已新增解析记录(已禁用)"
|
|
389
|
+
record_id = record.get("RecordId")
|
|
390
|
+
if content_changed:
|
|
391
|
+
dns.update_domain_record(record_id, data["rr"], data["type"], data["value"], data["ttl"])
|
|
392
|
+
if status_changed:
|
|
393
|
+
dns.set_domain_record_status(record_id, "Enable" if enabled else "Disable")
|
|
394
|
+
if content_changed and status_changed:
|
|
395
|
+
return "已更新解析记录并调整启用状态"
|
|
396
|
+
if content_changed:
|
|
397
|
+
return "已更新解析记录"
|
|
398
|
+
return "已启用解析记录" if enabled else "已禁用解析记录"
|
|
382
399
|
|
|
383
400
|
try:
|
|
384
401
|
async with busy(self.app, "正在保存解析记录…", mode="bar"):
|
|
385
|
-
await asyncio.to_thread(_apply)
|
|
402
|
+
message = await asyncio.to_thread(_apply)
|
|
386
403
|
except AliyunApiError as e:
|
|
387
404
|
self.app.notify_err(str(e))
|
|
405
|
+
# 内容与状态是两次独立调用,失败时可能已部分生效;重载列表以反映实际状态
|
|
406
|
+
self._load()
|
|
388
407
|
return
|
|
389
|
-
self.app.notify_ok(
|
|
408
|
+
self.app.notify_ok(message)
|
|
390
409
|
self._load()
|
|
391
410
|
|
|
392
411
|
def _delete_record(self, record: dict) -> None:
|
|
@@ -13,6 +13,7 @@ from rich.text import Text
|
|
|
13
13
|
from textual import on
|
|
14
14
|
from textual.binding import Binding
|
|
15
15
|
from textual.containers import Horizontal, Vertical, VerticalScroll
|
|
16
|
+
from textual.css.query import NoMatches
|
|
16
17
|
from textual.message import Message
|
|
17
18
|
from textual.reactive import reactive
|
|
18
19
|
from textual.screen import Screen
|
|
@@ -448,14 +449,25 @@ class PageScreen(Screen):
|
|
|
448
449
|
# ------------------------------------------------------------ 标题 / 副标题
|
|
449
450
|
|
|
450
451
|
def set_subtitle(self, text: str) -> None:
|
|
452
|
+
"""更新顶栏副标题。
|
|
453
|
+
|
|
454
|
+
判定不能用 ``is_mounted``:`on_mount` 里它仍是 `False`(Textual 派发完
|
|
455
|
+
Mount 事件之后才置位),首屏副标题会永远停在空串。改为按顶栏控件是否
|
|
456
|
+
已组合判断:未组合时仅保存文本,交由 compose 使用。
|
|
457
|
+
"""
|
|
451
458
|
self.subtitle = text
|
|
452
|
-
|
|
459
|
+
try:
|
|
453
460
|
self.query_one("#tb-sub", Static).update(text)
|
|
461
|
+
except NoMatches:
|
|
462
|
+
pass
|
|
454
463
|
|
|
455
464
|
def set_title(self, text: str) -> None:
|
|
465
|
+
"""更新顶栏标题;判定口径同 set_subtitle。"""
|
|
456
466
|
self.TITLE = text
|
|
457
|
-
|
|
467
|
+
try:
|
|
458
468
|
self.query_one(".tb-title", Static).update(Text(text, style="bold"))
|
|
469
|
+
except NoMatches:
|
|
470
|
+
pass
|
|
459
471
|
|
|
460
472
|
# ------------------------------------------------------------ 折叠菜单
|
|
461
473
|
|
|
@@ -613,32 +613,53 @@ class MenuMarquee:
|
|
|
613
613
|
|
|
614
614
|
|
|
615
615
|
class ClickTable(DataTable):
|
|
616
|
-
"""
|
|
616
|
+
"""可操作表格:单击只选中,双击(或 Enter)才执行。
|
|
617
|
+
|
|
618
|
+
``activate="double"``(默认):单击数据格只移动光标(发 ``RowHighlighted``,
|
|
619
|
+
供状态栏 / 控制按钮刷新),``event.chain >= 2``(同格 0.5s 内第二次点击)
|
|
620
|
+
才发 ``RowSelected``。避免误点一下就把详情 / 表单顶到脸上。
|
|
621
|
+
|
|
622
|
+
``activate="click"``:单击即发 ``RowSelected``,用于「选中即生效」的选择器
|
|
623
|
+
(如格式列表、纯选择用途的主从列表)。
|
|
617
624
|
|
|
618
625
|
注意:Textual 沿 MRO 逐类派发事件,重写 `_on_click` 后父类实现仍会被调用一次,
|
|
619
626
|
因此接管分支与回落分支都必须 prevent_default(),防止 RowSelected 双发。
|
|
620
627
|
"""
|
|
621
628
|
|
|
629
|
+
def __init__(self, *, activate: str = "double", **kwargs) -> None:
|
|
630
|
+
super().__init__(**kwargs)
|
|
631
|
+
self.activate_mode = "click" if activate == "click" else "double"
|
|
632
|
+
|
|
622
633
|
async def _on_click(self, event: events.Click) -> None:
|
|
623
634
|
meta = event.style.meta if event.style is not None else {}
|
|
624
635
|
row = meta.get("row", -1)
|
|
625
636
|
col = meta.get("column", -1)
|
|
626
637
|
is_data_cell = (
|
|
627
638
|
self.cursor_type == "row"
|
|
628
|
-
and isinstance(row, int)
|
|
629
|
-
and
|
|
639
|
+
and isinstance(row, int)
|
|
640
|
+
and isinstance(col, int)
|
|
641
|
+
and 0 <= row < self.row_count
|
|
642
|
+
and col >= 0
|
|
630
643
|
and not meta.get("out_of_bounds", False)
|
|
631
644
|
)
|
|
632
645
|
if is_data_cell:
|
|
633
646
|
self.cursor_coordinate = Coordinate(row, col)
|
|
634
|
-
self.
|
|
647
|
+
if self.activate_mode == "click" or event.chain >= 2:
|
|
648
|
+
self._post_selected_message()
|
|
635
649
|
else:
|
|
636
650
|
await super()._on_click(event)
|
|
637
651
|
event.prevent_default()
|
|
638
652
|
|
|
639
653
|
|
|
640
|
-
def make_table(
|
|
641
|
-
|
|
654
|
+
def make_table(
|
|
655
|
+
*headers,
|
|
656
|
+
cursor: str = "row",
|
|
657
|
+
zebra: bool = True,
|
|
658
|
+
classes: str = "tbl",
|
|
659
|
+
activate: str = "double",
|
|
660
|
+
**kwargs,
|
|
661
|
+
) -> ClickTable:
|
|
662
|
+
dt = ClickTable(cursor_type=cursor, zebra_stripes=zebra, classes=classes, activate=activate, **kwargs)
|
|
642
663
|
dt.add_columns(*headers)
|
|
643
664
|
return dt
|
|
644
665
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aliyun_controller
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.5
|
|
4
4
|
Summary: A Textual TUI for managing Alibaba Cloud services including billing queries and DNS management
|
|
5
5
|
Author-email: Moha-Master <hongkongreporter@outlook.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -61,9 +61,6 @@ Requires-Dist: pytest-asyncio; extra == "dev"
|
|
|
61
61
|
| **弹窗** | `Esc` / `Ctrl+C` | 取消并关闭 |
|
|
62
62
|
| | `回车` | 提交表单或确认 |
|
|
63
63
|
|
|
64
|
-
> 鼠标全程可用:按钮 / 选项 / 列表行一律**单击即执行**(表格行单击即进入下一级或打开编辑),滚轮滚动光标所在的滚动区域。
|
|
65
|
-
> 焦点进入输入框即切换为**文本编辑模式**:使用控件原生编辑键(`Home`/`End`、`Ctrl+Shift+A` 全选、`Ctrl+X/C/V` 剪切 / 复制 / 粘贴),底部提示栏同步切换,此时 `Ctrl+R` 刷新让位给输入框;`Ctrl+C` 有选中则复制、无选中则返回 / 取消。
|
|
66
|
-
|
|
67
64
|
## 安装指南
|
|
68
65
|
|
|
69
66
|
### 通过 pip 安装(推荐)
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "aliyun_controller"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.5"
|
|
8
8
|
description = "A Textual TUI for managing Alibaba Cloud services including billing queries and DNS management"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [
|
|
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
|
{aliyun_controller-0.2.3 → aliyun_controller-0.2.5}/aliyun_controller.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{aliyun_controller-0.2.3 → aliyun_controller-0.2.5}/aliyun_controller.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
{aliyun_controller-0.2.3 → aliyun_controller-0.2.5}/aliyun_controller.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|