PyPtt 2.3.17__tar.gz → 2.3.18__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.
- {pyptt-2.3.17 → pyptt-2.3.18}/PKG-INFO +1 -1
- {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/__init__.py +1 -1
- {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/_api_get_post_list.py +27 -6
- {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/screens.py +7 -1
- {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt.egg-info/PKG-INFO +1 -1
- {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_get_post_list_parser.py +10 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_incremental_parity.py +2 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_vt100_parser.py +23 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/LICENSE +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/PTT.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/_api_bucket.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/_api_call_status.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/_api_change_pw.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/_api_comment.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/_api_del_post.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/_api_get_board_info.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/_api_get_board_list.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/_api_get_bottom_post_list.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/_api_get_favourite_board.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/_api_get_newest_index.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/_api_get_post.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/_api_get_post_index.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/_api_get_time.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/_api_get_user.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/_api_get_waterball.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/_api_give_money.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/_api_has_new_mail.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/_api_loginout.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/_api_mail.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/_api_mark_post.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/_api_post.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/_api_reply_post.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/_api_search_user.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/_api_set_board_title.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/_api_set_signature_file.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/_api_util.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/_uao/LICENSE +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/_uao/__init__.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/_uao/b2u.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/_uao/u2b.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/api_server.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/check_value.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/command.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/config.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/connect_core.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/data_type.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/exceptions.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/i18n.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/lang_en_US.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/lang_zh_TW.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/lib_util.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/log.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/service.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/ssl_config.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt.egg-info/SOURCES.txt +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt.egg-info/dependency_links.txt +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt.egg-info/requires.txt +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt.egg-info/top_level.txt +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/README.md +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/pyproject.toml +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/setup.cfg +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/setup.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_backup_effective.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_backup_param.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_board_setting_flow.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_bucket.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_change_pw.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_comment.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_connect_core_timeout.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_del_post.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_del_post_unit.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_exceptions.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_fast_post.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_get_board_info.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_get_board_info_parser.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_get_board_list.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_get_bottom_post_list.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_get_content_formula.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_get_favourite_boards.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_get_mail.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_get_newest_index.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_get_post.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_get_post_list.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_get_post_parser.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_get_time.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_get_user.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_get_waterball.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_give_money.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_goto_board_same_board.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_half_dead_connection.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_i18n.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_init.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_logger.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_login_logout.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_mail_send_and_del.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_main_menu_status_bar.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_mark_post.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_new_screen_fixes.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_parse_query_post.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_post.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_post_anonymous.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_pyte_parity.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_reply_post.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_search_user.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_service.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_set_board_title.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_set_signature_file.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_status_line.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_uao_codec.py +0 -0
- {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_websocket_endpoint.py +0 -0
|
@@ -11,6 +11,24 @@ from . import log
|
|
|
11
11
|
from . import screens
|
|
12
12
|
|
|
13
13
|
|
|
14
|
+
def _cell_width(char: str) -> int:
|
|
15
|
+
# 終端機上全形字佔 2 格 (例如推文數的「爆」),用 big5uao 的位元組數當作顯示寬度
|
|
16
|
+
return len(char.encode('big5uao', 'replace'))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _split_columns(line: str, bounds: List[int]) -> List[str]:
|
|
20
|
+
"""依顯示欄位 (不是字元位置) 切開列表的一行。bounds 是各欄的起始欄位。"""
|
|
21
|
+
fields = [''] * len(bounds)
|
|
22
|
+
column = 0
|
|
23
|
+
for char in line:
|
|
24
|
+
for i in range(len(bounds) - 1, -1, -1):
|
|
25
|
+
if column >= bounds[i]:
|
|
26
|
+
fields[i] += char
|
|
27
|
+
break
|
|
28
|
+
column += _cell_width(char)
|
|
29
|
+
return fields
|
|
30
|
+
|
|
31
|
+
|
|
14
32
|
def get_post_list(api, board: str, limit: int = 20, offset: int = 0) -> List[Dict]:
|
|
15
33
|
_api_util.one_thread(api)
|
|
16
34
|
|
|
@@ -84,18 +102,21 @@ def get_post_list(api, board: str, limit: int = 20, offset: int = 0) -> List[Dic
|
|
|
84
102
|
# offsets here were tuned to that padded layout.
|
|
85
103
|
line = f' {line}'
|
|
86
104
|
|
|
105
|
+
# 欄位是固定的顯示寬度;推文數「爆」是全形字,用字元位置切會讓後面每一欄都偏一格
|
|
106
|
+
index_field, status, comment, list_date, _, author, title = _split_columns(
|
|
107
|
+
line, [0, 8, 10, 12, 17, 18, 31])
|
|
108
|
+
|
|
87
109
|
try:
|
|
88
|
-
cur_index = int(
|
|
110
|
+
cur_index = int(index_field.strip())
|
|
89
111
|
except ValueError:
|
|
90
112
|
# Pinned/announcement posts use '★' instead of a numeric
|
|
91
113
|
# index — they have no sequential index and cannot be
|
|
92
114
|
# fetched by index, so skip them.
|
|
93
115
|
continue
|
|
94
|
-
status =
|
|
95
|
-
comment =
|
|
96
|
-
list_date =
|
|
97
|
-
author =
|
|
98
|
-
title = line[31:]
|
|
116
|
+
status = status.strip()
|
|
117
|
+
comment = comment.strip()
|
|
118
|
+
list_date = list_date.strip()
|
|
119
|
+
author = author.strip()
|
|
99
120
|
|
|
100
121
|
cur_post = {
|
|
101
122
|
data_type.PostField.board: board,
|
|
@@ -356,7 +356,8 @@ xy_pattern_h = re.compile(r'^=ESC=\[[\d]+;[\d]+H')
|
|
|
356
356
|
xy_pattern_s = re.compile(r'^=ESC=\[[\d]+;[\d]+s')
|
|
357
357
|
# 依 ECMA-48 剝掉引擎不處理的 CSI (顏色、ESC[?2026h、ESC[6n…, PttCurrent M.1789835163.A.633),
|
|
358
358
|
# 以及被下一個 ESC 打斷的孤立 ESC (主選單動畫尾端的 '\x1b\x1b[?2026h'); 留著會讓引擎凍住畫面
|
|
359
|
-
|
|
359
|
+
# OSC (ESC ] ... BEL 或 ESC \\) 也一併剝掉, 例如內文網址的 OSC 8 超連結 '\x1b]8;;URL\x1b\\'
|
|
360
|
+
_color_sgr = re.compile(r'\x1B\][^\x07\x1B]*(?:\x07|\x1B\\)|\x1B(?=\x1B)|\x1B\[(?!(?:\d+;\d+)?[Hs]|K|2J)[0-?]*[ -/]*[@-~]')
|
|
360
361
|
|
|
361
362
|
|
|
362
363
|
def _preprocess(data: str) -> str:
|
|
@@ -541,6 +542,8 @@ def _escape_complete(seq: str) -> bool:
|
|
|
541
542
|
engine will bail on it, same as the batch parser)."""
|
|
542
543
|
if len(seq) < 2:
|
|
543
544
|
return False # lone ESC — wait for more
|
|
545
|
+
if seq[1] == ']':
|
|
546
|
+
return '\x07' in seq or '\x1b\\' in seq # OSC 要等到 BEL 或 ESC \\ 結尾
|
|
544
547
|
if seq[1] != '[':
|
|
545
548
|
return True # ESC + non-'[': engine bails on it
|
|
546
549
|
for ch in seq[2:]:
|
|
@@ -581,6 +584,9 @@ class IncrementalScreen(_VT100Engine):
|
|
|
581
584
|
while cut > 0 and s[cut - 1] in ' \x08':
|
|
582
585
|
cut -= 1
|
|
583
586
|
esc = s.rfind('\x1b')
|
|
587
|
+
osc = s.rfind('\x1b]')
|
|
588
|
+
if osc != -1 and not _escape_complete(s[osc:]):
|
|
589
|
+
esc = osc # OSC 的 ESC \\ 結尾可能還沒到, 從 OSC 開頭整段留著
|
|
584
590
|
if esc != -1 and not _escape_complete(s[esc:]):
|
|
585
591
|
while esc > 0 and s[esc - 1] == '\x1b':
|
|
586
592
|
esc -= 1 # 孤立 ESC 要跟後面的 ESC 一起送, _preprocess 才認得
|
|
@@ -118,6 +118,16 @@ class TestLineLayouts:
|
|
|
118
118
|
assert posts[0][data_type.PostField.push_number] == ''
|
|
119
119
|
assert posts[0][data_type.PostField.list_date] == '4/17'
|
|
120
120
|
|
|
121
|
+
def test_full_width_push_count(self):
|
|
122
|
+
# 「爆」是全形字,終端機上佔 2 格;用字元位置切的話後面每一欄都會左移一格
|
|
123
|
+
cursor = '> 9446 +爆10/15 someone □ [問安] 1015'
|
|
124
|
+
api = _build_api(_build_screen(cursor, []), newest=9446)
|
|
125
|
+
posts = _api_get_post_list.get_post_list(api, 'Python', limit=1)
|
|
126
|
+
assert posts[0][data_type.PostField.push_number] == '爆'
|
|
127
|
+
assert posts[0][data_type.PostField.list_date] == '10/15'
|
|
128
|
+
assert posts[0][data_type.PostField.author] == "someone"
|
|
129
|
+
assert posts[0][data_type.PostField.title].strip() == '□ [問安] 1015'
|
|
130
|
+
|
|
121
131
|
def test_reply_marker_R_in_title(self):
|
|
122
132
|
# Title section includes the kind marker ('R:', '□', '轉', '鎖').
|
|
123
133
|
cursor = '> 9440 4 1/15 ddavid R: [問題] @property 真正的運用是啥'
|
|
@@ -54,6 +54,8 @@ CRAFTED = [
|
|
|
54
54
|
b' \x08\x08\xe2\x97\x8e tail', # ◎ via reservation pair
|
|
55
55
|
b'\x1b[10;1H \x08\x08\xe2\x80\xbb\x1b[10;3H \xe7\x99\xbc\xe4\xbf\xa1\xe7\xab\x99: x',
|
|
56
56
|
b'\x1b[6nHELLO', # unknown escape -> bail
|
|
57
|
+
b'a\x1b]8;;https://x.y/z\x1b\\link\x1b]8;;\x1b\\b', # OSC 8 hyperlink (ST)
|
|
58
|
+
b'a\x1b]8;;https://x.y/z\x07link\x1b]8;;\x07b', # OSC 8 hyperlink (BEL)
|
|
57
59
|
b'line1\r\nline2', # CRLF
|
|
58
60
|
b'helloworld\x1b[1;2HXY', # replace mode
|
|
59
61
|
b'\x1b[100;1HX', # row clamp
|
|
@@ -481,3 +481,26 @@ def test_stray_esc_split_across_chunks():
|
|
|
481
481
|
s.feed(data[:cut])
|
|
482
482
|
s.feed(data[cut:])
|
|
483
483
|
assert s.screen == VT100Parser(data, 'utf-8').screen, cut
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
# 內文網址被包成 OSC 8 超連結 (ESC ] 8 ; ; URL ST 文字 ESC ] 8 ; ; ST); 沒被吃掉時引擎凍住, get_content 讀不到頁尾
|
|
487
|
+
OSC8_POST = (b'\x1b[2J\x1b[3;1H\xe7\xb6\xb2\xe5\x9d\x80 \x1b]8;;https://example.com/a?b=1\x1b\\https://example.com/a?b=1'
|
|
488
|
+
b'\x1b]8;;\x1b\\ end\x1b[4;1H\x1b]8;;https://ptt.cc\x07ptt\x1b]8;;\x07\x1b[24;1Hfooter')
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def test_osc8_hyperlink_is_stripped():
|
|
492
|
+
lines = parse(OSC8_POST)
|
|
493
|
+
assert lines[2] == '網址 https://example.com/a?b=1 end'
|
|
494
|
+
assert lines[3] == 'ptt'
|
|
495
|
+
assert lines[23] == 'footer'
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def test_osc8_hyperlink_split_across_chunks():
|
|
499
|
+
from PyPtt.screens import IncrementalScreen
|
|
500
|
+
expected = VT100Parser(OSC8_POST, 'utf-8').screen
|
|
501
|
+
for cut in range(1, len(OSC8_POST)):
|
|
502
|
+
s = IncrementalScreen('utf-8')
|
|
503
|
+
s.feed(OSC8_POST[:cut])
|
|
504
|
+
s.feed(OSC8_POST[cut:])
|
|
505
|
+
assert not s._bailed, cut
|
|
506
|
+
assert s.screen == expected, cut
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|