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.
Files changed (110) hide show
  1. {pyptt-2.3.17 → pyptt-2.3.18}/PKG-INFO +1 -1
  2. {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/__init__.py +1 -1
  3. {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/_api_get_post_list.py +27 -6
  4. {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/screens.py +7 -1
  5. {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt.egg-info/PKG-INFO +1 -1
  6. {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_get_post_list_parser.py +10 -0
  7. {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_incremental_parity.py +2 -0
  8. {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_vt100_parser.py +23 -0
  9. {pyptt-2.3.17 → pyptt-2.3.18}/LICENSE +0 -0
  10. {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/PTT.py +0 -0
  11. {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/_api_bucket.py +0 -0
  12. {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/_api_call_status.py +0 -0
  13. {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/_api_change_pw.py +0 -0
  14. {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/_api_comment.py +0 -0
  15. {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/_api_del_post.py +0 -0
  16. {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/_api_get_board_info.py +0 -0
  17. {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/_api_get_board_list.py +0 -0
  18. {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/_api_get_bottom_post_list.py +0 -0
  19. {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/_api_get_favourite_board.py +0 -0
  20. {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/_api_get_newest_index.py +0 -0
  21. {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/_api_get_post.py +0 -0
  22. {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/_api_get_post_index.py +0 -0
  23. {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/_api_get_time.py +0 -0
  24. {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/_api_get_user.py +0 -0
  25. {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/_api_get_waterball.py +0 -0
  26. {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/_api_give_money.py +0 -0
  27. {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/_api_has_new_mail.py +0 -0
  28. {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/_api_loginout.py +0 -0
  29. {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/_api_mail.py +0 -0
  30. {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/_api_mark_post.py +0 -0
  31. {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/_api_post.py +0 -0
  32. {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/_api_reply_post.py +0 -0
  33. {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/_api_search_user.py +0 -0
  34. {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/_api_set_board_title.py +0 -0
  35. {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/_api_set_signature_file.py +0 -0
  36. {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/_api_util.py +0 -0
  37. {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/_uao/LICENSE +0 -0
  38. {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/_uao/__init__.py +0 -0
  39. {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/_uao/b2u.py +0 -0
  40. {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/_uao/u2b.py +0 -0
  41. {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/api_server.py +0 -0
  42. {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/check_value.py +0 -0
  43. {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/command.py +0 -0
  44. {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/config.py +0 -0
  45. {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/connect_core.py +0 -0
  46. {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/data_type.py +0 -0
  47. {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/exceptions.py +0 -0
  48. {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/i18n.py +0 -0
  49. {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/lang_en_US.py +0 -0
  50. {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/lang_zh_TW.py +0 -0
  51. {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/lib_util.py +0 -0
  52. {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/log.py +0 -0
  53. {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/service.py +0 -0
  54. {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt/ssl_config.py +0 -0
  55. {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt.egg-info/SOURCES.txt +0 -0
  56. {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt.egg-info/dependency_links.txt +0 -0
  57. {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt.egg-info/requires.txt +0 -0
  58. {pyptt-2.3.17 → pyptt-2.3.18}/PyPtt.egg-info/top_level.txt +0 -0
  59. {pyptt-2.3.17 → pyptt-2.3.18}/README.md +0 -0
  60. {pyptt-2.3.17 → pyptt-2.3.18}/pyproject.toml +0 -0
  61. {pyptt-2.3.17 → pyptt-2.3.18}/setup.cfg +0 -0
  62. {pyptt-2.3.17 → pyptt-2.3.18}/setup.py +0 -0
  63. {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_backup_effective.py +0 -0
  64. {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_backup_param.py +0 -0
  65. {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_board_setting_flow.py +0 -0
  66. {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_bucket.py +0 -0
  67. {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_change_pw.py +0 -0
  68. {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_comment.py +0 -0
  69. {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_connect_core_timeout.py +0 -0
  70. {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_del_post.py +0 -0
  71. {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_del_post_unit.py +0 -0
  72. {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_exceptions.py +0 -0
  73. {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_fast_post.py +0 -0
  74. {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_get_board_info.py +0 -0
  75. {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_get_board_info_parser.py +0 -0
  76. {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_get_board_list.py +0 -0
  77. {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_get_bottom_post_list.py +0 -0
  78. {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_get_content_formula.py +0 -0
  79. {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_get_favourite_boards.py +0 -0
  80. {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_get_mail.py +0 -0
  81. {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_get_newest_index.py +0 -0
  82. {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_get_post.py +0 -0
  83. {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_get_post_list.py +0 -0
  84. {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_get_post_parser.py +0 -0
  85. {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_get_time.py +0 -0
  86. {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_get_user.py +0 -0
  87. {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_get_waterball.py +0 -0
  88. {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_give_money.py +0 -0
  89. {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_goto_board_same_board.py +0 -0
  90. {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_half_dead_connection.py +0 -0
  91. {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_i18n.py +0 -0
  92. {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_init.py +0 -0
  93. {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_logger.py +0 -0
  94. {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_login_logout.py +0 -0
  95. {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_mail_send_and_del.py +0 -0
  96. {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_main_menu_status_bar.py +0 -0
  97. {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_mark_post.py +0 -0
  98. {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_new_screen_fixes.py +0 -0
  99. {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_parse_query_post.py +0 -0
  100. {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_post.py +0 -0
  101. {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_post_anonymous.py +0 -0
  102. {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_pyte_parity.py +0 -0
  103. {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_reply_post.py +0 -0
  104. {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_search_user.py +0 -0
  105. {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_service.py +0 -0
  106. {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_set_board_title.py +0 -0
  107. {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_set_signature_file.py +0 -0
  108. {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_status_line.py +0 -0
  109. {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_uao_codec.py +0 -0
  110. {pyptt-2.3.17 → pyptt-2.3.18}/tests/test_websocket_endpoint.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyPtt
3
- Version: 2.3.17
3
+ Version: 2.3.18
4
4
  Summary: PyPtt github: https://github.com/PyPtt/PyPtt
5
5
  Author-email: CodingMan <pttcodingman@gmail.com>
6
6
  License: GNU Lesser General Public License v3 (LGPLv3)
@@ -1,4 +1,4 @@
1
- __version__ = '2.3.17'
1
+ __version__ = '2.3.18'
2
2
  __author__ = 'CodingMan'
3
3
  __email__ = 'pttcodingman@gmail.com'
4
4
 
@@ -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(line[:8].strip())
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 = line[8:10].strip()
95
- comment = line[10:12].strip()
96
- list_date = line[12:17].strip()
97
- author = line[18:31].strip()
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
- _color_sgr = re.compile(r'\x1B(?=\x1B)|\x1B\[(?!(?:\d+;\d+)?[Hs]|K|2J)[0-?]*[ -/]*[@-~]')
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 才認得
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyPtt
3
- Version: 2.3.17
3
+ Version: 2.3.18
4
4
  Summary: PyPtt github: https://github.com/PyPtt/PyPtt
5
5
  Author-email: CodingMan <pttcodingman@gmail.com>
6
6
  License: GNU Lesser General Public License v3 (LGPLv3)
@@ -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