PyPtt 2.1.0__tar.gz → 2.1.2__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 (91) hide show
  1. {pyptt-2.1.0 → pyptt-2.1.2}/PKG-INFO +4 -4
  2. {pyptt-2.1.0 → pyptt-2.1.2}/PyPtt/PTT.py +5 -0
  3. {pyptt-2.1.0 → pyptt-2.1.2}/PyPtt/__init__.py +1 -1
  4. {pyptt-2.1.0 → pyptt-2.1.2}/PyPtt/_api_get_board_list.py +4 -0
  5. {pyptt-2.1.0 → pyptt-2.1.2}/PyPtt/_api_get_favourite_board.py +1 -0
  6. pyptt-2.1.2/PyPtt/_api_get_post_list.py +111 -0
  7. {pyptt-2.1.0 → pyptt-2.1.2}/PyPtt/_api_get_waterball.py +45 -44
  8. pyptt-2.1.2/PyPtt/_api_has_new_mail.py +77 -0
  9. {pyptt-2.1.0 → pyptt-2.1.2}/PyPtt/_api_util.py +167 -136
  10. {pyptt-2.1.0 → pyptt-2.1.2}/PyPtt/config.py +5 -0
  11. {pyptt-2.1.0 → pyptt-2.1.2}/PyPtt/connect_core.py +23 -2
  12. {pyptt-2.1.0 → pyptt-2.1.2}/PyPtt/screens.py +2 -2
  13. pyptt-2.1.2/PyPtt/ssl_config.py +24 -0
  14. {pyptt-2.1.0 → pyptt-2.1.2}/PyPtt.egg-info/PKG-INFO +4 -4
  15. {pyptt-2.1.0 → pyptt-2.1.2}/PyPtt.egg-info/SOURCES.txt +1 -0
  16. {pyptt-2.1.0 → pyptt-2.1.2}/PyPtt.egg-info/requires.txt +3 -3
  17. {pyptt-2.1.0 → pyptt-2.1.2}/pyproject.toml +3 -3
  18. pyptt-2.1.2/tests/test_get_content_formula.py +431 -0
  19. {pyptt-2.1.0 → pyptt-2.1.2}/tests/test_get_post_list_parser.py +54 -0
  20. pyptt-2.1.0/PyPtt/_api_get_post_list.py +0 -104
  21. pyptt-2.1.0/PyPtt/_api_has_new_mail.py +0 -76
  22. pyptt-2.1.0/PyPtt/ssl_config.py +0 -24
  23. {pyptt-2.1.0 → pyptt-2.1.2}/LICENSE +0 -0
  24. {pyptt-2.1.0 → pyptt-2.1.2}/PyPtt/_api_bucket.py +0 -0
  25. {pyptt-2.1.0 → pyptt-2.1.2}/PyPtt/_api_call_status.py +0 -0
  26. {pyptt-2.1.0 → pyptt-2.1.2}/PyPtt/_api_change_pw.py +0 -0
  27. {pyptt-2.1.0 → pyptt-2.1.2}/PyPtt/_api_comment.py +0 -0
  28. {pyptt-2.1.0 → pyptt-2.1.2}/PyPtt/_api_del_post.py +0 -0
  29. {pyptt-2.1.0 → pyptt-2.1.2}/PyPtt/_api_get_board_info.py +0 -0
  30. {pyptt-2.1.0 → pyptt-2.1.2}/PyPtt/_api_get_bottom_post_list.py +0 -0
  31. {pyptt-2.1.0 → pyptt-2.1.2}/PyPtt/_api_get_newest_index.py +0 -0
  32. {pyptt-2.1.0 → pyptt-2.1.2}/PyPtt/_api_get_post.py +0 -0
  33. {pyptt-2.1.0 → pyptt-2.1.2}/PyPtt/_api_get_post_index.py +0 -0
  34. {pyptt-2.1.0 → pyptt-2.1.2}/PyPtt/_api_get_time.py +0 -0
  35. {pyptt-2.1.0 → pyptt-2.1.2}/PyPtt/_api_get_user.py +0 -0
  36. {pyptt-2.1.0 → pyptt-2.1.2}/PyPtt/_api_give_money.py +0 -0
  37. {pyptt-2.1.0 → pyptt-2.1.2}/PyPtt/_api_loginout.py +0 -0
  38. {pyptt-2.1.0 → pyptt-2.1.2}/PyPtt/_api_mail.py +0 -0
  39. {pyptt-2.1.0 → pyptt-2.1.2}/PyPtt/_api_mark_post.py +0 -0
  40. {pyptt-2.1.0 → pyptt-2.1.2}/PyPtt/_api_post.py +0 -0
  41. {pyptt-2.1.0 → pyptt-2.1.2}/PyPtt/_api_reply_post.py +0 -0
  42. {pyptt-2.1.0 → pyptt-2.1.2}/PyPtt/_api_search_user.py +0 -0
  43. {pyptt-2.1.0 → pyptt-2.1.2}/PyPtt/_api_set_board_title.py +0 -0
  44. {pyptt-2.1.0 → pyptt-2.1.2}/PyPtt/api_server.py +0 -0
  45. {pyptt-2.1.0 → pyptt-2.1.2}/PyPtt/check_value.py +0 -0
  46. {pyptt-2.1.0 → pyptt-2.1.2}/PyPtt/command.py +0 -0
  47. {pyptt-2.1.0 → pyptt-2.1.2}/PyPtt/data_type.py +0 -0
  48. {pyptt-2.1.0 → pyptt-2.1.2}/PyPtt/exceptions.py +0 -0
  49. {pyptt-2.1.0 → pyptt-2.1.2}/PyPtt/i18n.py +0 -0
  50. {pyptt-2.1.0 → pyptt-2.1.2}/PyPtt/lang_en_US.py +0 -0
  51. {pyptt-2.1.0 → pyptt-2.1.2}/PyPtt/lang_zh_TW.py +0 -0
  52. {pyptt-2.1.0 → pyptt-2.1.2}/PyPtt/lib_util.py +0 -0
  53. {pyptt-2.1.0 → pyptt-2.1.2}/PyPtt/log.py +0 -0
  54. {pyptt-2.1.0 → pyptt-2.1.2}/PyPtt/service.py +0 -0
  55. {pyptt-2.1.0 → pyptt-2.1.2}/PyPtt.egg-info/dependency_links.txt +0 -0
  56. {pyptt-2.1.0 → pyptt-2.1.2}/PyPtt.egg-info/top_level.txt +0 -0
  57. {pyptt-2.1.0 → pyptt-2.1.2}/README.md +0 -0
  58. {pyptt-2.1.0 → pyptt-2.1.2}/setup.cfg +0 -0
  59. {pyptt-2.1.0 → pyptt-2.1.2}/setup.py +0 -0
  60. {pyptt-2.1.0 → pyptt-2.1.2}/tests/test_bucket.py +0 -0
  61. {pyptt-2.1.0 → pyptt-2.1.2}/tests/test_change_pw.py +0 -0
  62. {pyptt-2.1.0 → pyptt-2.1.2}/tests/test_comment.py +0 -0
  63. {pyptt-2.1.0 → pyptt-2.1.2}/tests/test_del_post.py +0 -0
  64. {pyptt-2.1.0 → pyptt-2.1.2}/tests/test_exceptions.py +0 -0
  65. {pyptt-2.1.0 → pyptt-2.1.2}/tests/test_fast_post.py +0 -0
  66. {pyptt-2.1.0 → pyptt-2.1.2}/tests/test_get_board_info.py +0 -0
  67. {pyptt-2.1.0 → pyptt-2.1.2}/tests/test_get_board_list.py +0 -0
  68. {pyptt-2.1.0 → pyptt-2.1.2}/tests/test_get_bottom_post_list.py +0 -0
  69. {pyptt-2.1.0 → pyptt-2.1.2}/tests/test_get_favourite_boards.py +0 -0
  70. {pyptt-2.1.0 → pyptt-2.1.2}/tests/test_get_mail.py +0 -0
  71. {pyptt-2.1.0 → pyptt-2.1.2}/tests/test_get_newest_index.py +0 -0
  72. {pyptt-2.1.0 → pyptt-2.1.2}/tests/test_get_post.py +0 -0
  73. {pyptt-2.1.0 → pyptt-2.1.2}/tests/test_get_post_list.py +0 -0
  74. {pyptt-2.1.0 → pyptt-2.1.2}/tests/test_get_post_parser.py +0 -0
  75. {pyptt-2.1.0 → pyptt-2.1.2}/tests/test_get_time.py +0 -0
  76. {pyptt-2.1.0 → pyptt-2.1.2}/tests/test_get_user.py +0 -0
  77. {pyptt-2.1.0 → pyptt-2.1.2}/tests/test_get_waterball.py +0 -0
  78. {pyptt-2.1.0 → pyptt-2.1.2}/tests/test_give_money.py +0 -0
  79. {pyptt-2.1.0 → pyptt-2.1.2}/tests/test_i18n.py +0 -0
  80. {pyptt-2.1.0 → pyptt-2.1.2}/tests/test_init.py +0 -0
  81. {pyptt-2.1.0 → pyptt-2.1.2}/tests/test_logger.py +0 -0
  82. {pyptt-2.1.0 → pyptt-2.1.2}/tests/test_mail_send_and_del.py +0 -0
  83. {pyptt-2.1.0 → pyptt-2.1.2}/tests/test_mark_post.py +0 -0
  84. {pyptt-2.1.0 → pyptt-2.1.2}/tests/test_parse_query_post.py +0 -0
  85. {pyptt-2.1.0 → pyptt-2.1.2}/tests/test_post.py +0 -0
  86. {pyptt-2.1.0 → pyptt-2.1.2}/tests/test_pyte_parity.py +0 -0
  87. {pyptt-2.1.0 → pyptt-2.1.2}/tests/test_reply_post.py +0 -0
  88. {pyptt-2.1.0 → pyptt-2.1.2}/tests/test_search_user.py +0 -0
  89. {pyptt-2.1.0 → pyptt-2.1.2}/tests/test_service.py +0 -0
  90. {pyptt-2.1.0 → pyptt-2.1.2}/tests/test_set_board_title.py +0 -0
  91. {pyptt-2.1.0 → pyptt-2.1.2}/tests/test_vt100_parser.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyPtt
3
- Version: 2.1.0
3
+ Version: 2.1.2
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)
@@ -26,9 +26,9 @@ Requires-Python: >=3.11
26
26
  Description-Content-Type: text/markdown
27
27
  License-File: LICENSE
28
28
  Requires-Dist: progressbar2
29
- Requires-Dist: websockets==12.0
29
+ Requires-Dist: websockets>=12
30
30
  Requires-Dist: uao
31
- Requires-Dist: requests==2.33.0
31
+ Requires-Dist: requests>=2.32
32
32
  Requires-Dist: AutoStrEnum
33
33
  Requires-Dist: PyYAML
34
34
  Provides-Extra: api
@@ -41,7 +41,7 @@ Requires-Dist: flake8; extra == "dev"
41
41
  Requires-Dist: pyte; extra == "dev"
42
42
  Requires-Dist: sphinx; extra == "dev"
43
43
  Requires-Dist: sphinx-copybutton; extra == "dev"
44
- Requires-Dist: pygments==2.15.0; extra == "dev"
44
+ Requires-Dist: pygments>=2.15; extra == "dev"
45
45
  Requires-Dist: Furo; extra == "dev"
46
46
  Requires-Dist: sphinx-sitemap; extra == "dev"
47
47
  Dynamic: license-file
@@ -150,6 +150,11 @@ class API:
150
150
  check_value.check_type(verify_ssl, bool, 'verify_ssl')
151
151
  self.config.verify_ssl = verify_ssl
152
152
 
153
+ screen_height = kwargs.get('screen_height', 24)
154
+ check_value.check_type(screen_height, int, 'screen_height')
155
+ check_value.check_range(screen_height, 24, 254, 'screen_height')
156
+ self.config.screen_height = screen_height
157
+
153
158
  self.connect_core = connect_core.API(self)
154
159
  self._exist_board_list = set()
155
160
  self._exist_user_list = set()
@@ -1,4 +1,4 @@
1
- __version__ = '2.1.0'
1
+ __version__ = '2.1.2'
2
2
  __author__ = 'CodingMan'
3
3
  __email__ = 'pttcodingman@gmail.com'
4
4
 
@@ -65,6 +65,10 @@ def get_board_list(api) -> list:
65
65
  cmd = ''.join(cmd_list)
66
66
 
67
67
  board_list = []
68
+ # NOTE: expanded_screen is intentionally NOT used here.
69
+ # get_board_list traverses all boards sequentially; a mid-session NAWS resize
70
+ # causes PTT to render the first page at the old size before the resize takes
71
+ # effect, producing inconsistent page boundaries and skipping boards.
68
72
  while True:
69
73
 
70
74
  api.connect_core.send(
@@ -24,6 +24,7 @@ def get_favourite_board(api) -> list:
24
24
 
25
25
  board_list = []
26
26
  favourite_board_list = []
27
+ # NOTE: expanded_screen intentionally not used — same risk as get_board_list.
27
28
  while True:
28
29
 
29
30
  api.connect_core.send(
@@ -0,0 +1,111 @@
1
+ from typing import Dict, List
2
+
3
+ from . import _api_util
4
+ from . import check_value
5
+ from . import command
6
+ from . import connect_core
7
+ from . import data_type
8
+ from . import exceptions
9
+ from . import i18n
10
+ from . import log
11
+ from . import screens
12
+
13
+
14
+ def get_post_list(api, board: str, limit: int = 20, offset: int = 0) -> List[Dict]:
15
+ _api_util.one_thread(api)
16
+
17
+ if not api._is_login:
18
+ raise exceptions.RequireLogin(i18n.require_login)
19
+
20
+ check_value.check_type(board, str, 'board')
21
+ if limit is not None:
22
+ check_value.check_type(limit, int, 'limit')
23
+ if offset is not None:
24
+ check_value.check_type(offset, int, 'offset')
25
+
26
+ max_index = api.get_newest_index(data_type.NewIndex.BOARD, board=board)
27
+
28
+ check_value.check_range(limit, min_value=1, max_value=max_index, name='limit')
29
+ check_value.check_range(offset, min_value=0, max_value=max_index - 1, name='offset')
30
+
31
+ start_index = max_index - limit - offset + 1
32
+ end_index = start_index + limit - 1
33
+
34
+ # the first page shows 10 posts, each next page shows 20 posts
35
+ if limit <= 10:
36
+ max_round = 1
37
+ else:
38
+ # 1 is for the first page, +1 is for rounding up
39
+ # so +2
40
+ max_round = (limit - 10) // 20 + 2
41
+
42
+ cmd_list = []
43
+
44
+ cmd_list.append(str(max(1, start_index - 100)))
45
+ cmd_list.append(command.enter)
46
+ cmd_list.append(str(start_index))
47
+ cmd_list.append(command.enter)
48
+
49
+ cmd = ''.join(cmd_list)
50
+
51
+ target_list = [
52
+ connect_core.TargetUnit(screens.Target.InBoard, log_level=log.DEBUG, break_detect=True)
53
+ ]
54
+
55
+ post_list = []
56
+
57
+ with _api_util.expanded_screen(api):
58
+ for _ in range(max_round):
59
+
60
+ index = api.connect_core.send(cmd, target_list)
61
+ cmd = command.page_down
62
+ last_screen = api.connect_core.get_screen_queue()[-1]
63
+
64
+ is_parse_area = False
65
+
66
+ for line in last_screen.splitlines()[3:-1]:
67
+
68
+ if not line.startswith(api.cursor) and not is_parse_area:
69
+ continue
70
+ is_parse_area = True
71
+
72
+ if line.strip().startswith(api.cursor):
73
+ # replace the first cursor in the line
74
+ line = line.replace(api.cursor, ' ' * len(api.cursor.encode('big5uao')), 1)
75
+
76
+ if api.cursor == data_type.Cursor.NEW:
77
+ # Pad one leading space so the fixed-column offsets below
78
+ # apply equally to cursor and non-cursor rows. Before the
79
+ # VT100Parser cell-tracking fix, the parser inserted this
80
+ # padding implicitly via its off-by-one column quirk; the
81
+ # offsets here were tuned to that padded layout.
82
+ line = f' {line}'
83
+
84
+ try:
85
+ cur_index = int(line[:8].strip())
86
+ except ValueError:
87
+ # Pinned/announcement posts use '★' instead of a numeric
88
+ # index — they have no sequential index and cannot be
89
+ # fetched by index, so skip them.
90
+ continue
91
+ status = line[8:10].strip()
92
+ comment = line[10:12].strip()
93
+ list_date = line[12:17].strip()
94
+ author = line[18:31].strip()
95
+ title = line[31:]
96
+
97
+ cur_post = {
98
+ data_type.PostField.board: board,
99
+ data_type.PostField.index: cur_index,
100
+ data_type.PostField.list_date: list_date,
101
+ data_type.PostField.author: author,
102
+ data_type.PostField.title: title,
103
+ data_type.PostField.push_number: comment
104
+ }
105
+
106
+ post_list.append(cur_post)
107
+
108
+ if cur_index >= end_index:
109
+ return post_list
110
+
111
+ raise exceptions.UnknownError(i18n.unknown_error)
@@ -101,51 +101,52 @@ def get_waterball(api, post_action: WaterballPostAction = WaterballPostAction.KE
101
101
  seen_lines = set()
102
102
  all_content_parts = []
103
103
 
104
- while True:
105
- target_list = [
106
- connect_core.TargetUnit('◆ 暫無訊息記錄', log_level=log.DEBUG, break_detect=True),
107
- connect_core.TargetUnit(screens.Target.WaterBallListEnd, log_level=log.DEBUG, break_detect=True),
108
- connect_core.TargetUnit(screens.Target.InWaterBallList, log_level=log.DEBUG, break_detect=True),
109
- ]
110
-
111
- index = api.connect_core.send(cmd, target_list)
112
-
113
- if index == 0:
114
- # no waterball records
115
- api.connect_core.send('q' + command.go_main_menu, [
116
- connect_core.TargetUnit(screens.Target.MainMenu, log_level=log.DEBUG, break_detect=True),
117
- ])
118
- log.logger.info(i18n.get_waterball, '...', i18n.success)
119
- return []
120
-
121
- if index < 0:
122
- break
123
-
124
- screen = api.connect_core.get_screen_queue()[-1]
125
- for line in screen.split('\n'):
126
- line = line.strip()
127
- if line and line not in seen_lines:
128
- seen_lines.add(line)
129
- all_content_parts.append(line)
130
-
131
- if index == 1:
132
- # WaterBallListEnd — collect done, exit viewer; handle post-action prompt
133
- action_cmd = _post_action_cmd[post_action]
134
- target_list_exit = [
135
- connect_core.TargetUnit(screens.Target.WaterBallPostAction, log_level=log.DEBUG,
136
- response=action_cmd + command.enter),
137
- connect_core.TargetUnit(screens.Target.WaterballCleanup, log_level=log.DEBUG,
138
- response='y' + command.enter),
139
- connect_core.TargetUnit(screens.Target.MainMenu, log_level=log.DEBUG, break_detect=True),
104
+ with _api_util.expanded_screen(api):
105
+ while True:
106
+ target_list = [
107
+ connect_core.TargetUnit('◆ 暫無訊息記錄', log_level=log.DEBUG, break_detect=True),
108
+ connect_core.TargetUnit(screens.Target.WaterBallListEnd, log_level=log.DEBUG, break_detect=True),
109
+ connect_core.TargetUnit(screens.Target.InWaterBallList, log_level=log.DEBUG, break_detect=True),
140
110
  ]
141
- api.connect_core.send('q', target_list_exit)
142
- api.connect_core.send(command.go_main_menu, [
143
- connect_core.TargetUnit(screens.Target.MainMenu, log_level=log.DEBUG, break_detect=True),
144
- ])
145
- break
146
-
147
- # index == 2: InWaterBallList — next page
148
- cmd = command.ctrl_f
111
+
112
+ index = api.connect_core.send(cmd, target_list)
113
+
114
+ if index == 0:
115
+ # no waterball records
116
+ api.connect_core.send('q' + command.go_main_menu, [
117
+ connect_core.TargetUnit(screens.Target.MainMenu, log_level=log.DEBUG, break_detect=True),
118
+ ])
119
+ log.logger.info(i18n.get_waterball, '...', i18n.success)
120
+ return []
121
+
122
+ if index < 0:
123
+ break
124
+
125
+ screen = api.connect_core.get_screen_queue()[-1]
126
+ for line in screen.split('\n'):
127
+ line = line.strip()
128
+ if line and line not in seen_lines:
129
+ seen_lines.add(line)
130
+ all_content_parts.append(line)
131
+
132
+ if index == 1:
133
+ # WaterBallListEnd — collect done, exit viewer; handle post-action prompt
134
+ action_cmd = _post_action_cmd[post_action]
135
+ target_list_exit = [
136
+ connect_core.TargetUnit(screens.Target.WaterBallPostAction, log_level=log.DEBUG,
137
+ response=action_cmd + command.enter),
138
+ connect_core.TargetUnit(screens.Target.WaterballCleanup, log_level=log.DEBUG,
139
+ response='y' + command.enter),
140
+ connect_core.TargetUnit(screens.Target.MainMenu, log_level=log.DEBUG, break_detect=True),
141
+ ]
142
+ api.connect_core.send('q', target_list_exit)
143
+ api.connect_core.send(command.go_main_menu, [
144
+ connect_core.TargetUnit(screens.Target.MainMenu, log_level=log.DEBUG, break_detect=True),
145
+ ])
146
+ break
147
+
148
+ # index == 2: InWaterBallList — next page
149
+ cmd = command.ctrl_f
149
150
 
150
151
  result = []
151
152
  for line in _merge_continuation_lines(all_content_parts):
@@ -0,0 +1,77 @@
1
+ import re
2
+
3
+ from . import _api_util
4
+ from . import command
5
+ from . import connect_core
6
+ from . import log
7
+ from . import screens
8
+
9
+
10
+ def has_new_mail(api) -> int:
11
+ cmd_list = []
12
+ cmd_list.append(command.go_main_menu)
13
+ cmd_list.append(command.ctrl_z)
14
+ cmd_list.append('m')
15
+ # cmd_list.append('1')
16
+ # cmd_list.append(command.enter)
17
+ cmd = ''.join(cmd_list)
18
+ current_capacity = None
19
+ plus_count = 0
20
+ index_pattern = re.compile(r'(\d+)')
21
+ checked_index_list = []
22
+ break_detect = False
23
+
24
+ target_list = [
25
+ connect_core.TargetUnit(screens.Target.InMailBox, log_level=log.DEBUG, break_detect=True)
26
+ ]
27
+
28
+ api.connect_core.send(
29
+ cmd,
30
+ target_list,
31
+ )
32
+ current_capacity, _ = _api_util.get_mailbox_capacity(api)
33
+ if current_capacity > 20:
34
+ cmd_list = []
35
+ cmd_list.append(command.go_main_menu)
36
+ cmd_list.append(command.ctrl_z)
37
+ cmd_list.append('m')
38
+ cmd_list.append('1')
39
+ cmd_list.append(command.enter)
40
+ cmd = ''.join(cmd_list)
41
+
42
+ with _api_util.expanded_screen(api):
43
+ while True:
44
+ if current_capacity > 20:
45
+ api.connect_core.send(
46
+ cmd,
47
+ target_list,
48
+ )
49
+ last_screen = api.connect_core.get_screen_queue()[-1]
50
+
51
+ last_screen_list = last_screen.split('\n')
52
+ last_screen_list = last_screen_list[3:-1]
53
+ last_screen_list = [x[:10] for x in last_screen_list]
54
+
55
+ current_plus_count = 0
56
+ for line in last_screen_list:
57
+ if str(current_capacity) in line:
58
+ break_detect = True
59
+
60
+ index_result = index_pattern.search(line)
61
+ if index_result is None:
62
+ continue
63
+ current_index = index_result.group(0)
64
+ if current_index in checked_index_list:
65
+ continue
66
+ checked_index_list.append(current_index)
67
+ if '+' not in line:
68
+ continue
69
+
70
+ current_plus_count += 1
71
+
72
+ plus_count += current_plus_count
73
+ if break_detect:
74
+ break
75
+ cmd = command.ctrl_f
76
+
77
+ return plus_count