PyPtt 2.0.7__tar.gz → 2.0.8__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 (80) hide show
  1. {pyptt-2.0.7 → pyptt-2.0.8}/PKG-INFO +1 -1
  2. {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/PTT.py +1 -0
  3. {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/__init__.py +1 -1
  4. {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/_api_bucket.py +1 -1
  5. {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/_api_call_status.py +11 -8
  6. {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/_api_comment.py +1 -1
  7. {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/_api_get_board_info.py +5 -0
  8. {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/_api_get_user.py +18 -6
  9. {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/_api_give_money.py +1 -2
  10. {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/_api_mail.py +2 -2
  11. {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/_api_mark_post.py +1 -0
  12. {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/_api_reply_post.py +2 -0
  13. {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/_api_util.py +7 -2
  14. {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/connect_core.py +17 -6
  15. {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/data_type.py +8 -0
  16. {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/screens.py +3 -3
  17. {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/service.py +32 -3
  18. pyptt-2.0.8/PyPtt/ssl_config.py +24 -0
  19. {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt.egg-info/PKG-INFO +1 -1
  20. {pyptt-2.0.7 → pyptt-2.0.8}/tests/test_get_user.py +17 -2
  21. {pyptt-2.0.7 → pyptt-2.0.8}/tests/test_service.py +47 -0
  22. pyptt-2.0.7/PyPtt/ssl_config.py +0 -24
  23. {pyptt-2.0.7 → pyptt-2.0.8}/LICENSE +0 -0
  24. {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/_api_change_pw.py +0 -0
  25. {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/_api_del_post.py +0 -0
  26. {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/_api_get_board_list.py +0 -0
  27. {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/_api_get_bottom_post_list.py +0 -0
  28. {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/_api_get_favourite_board.py +0 -0
  29. {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/_api_get_newest_index.py +0 -0
  30. {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/_api_get_post.py +0 -0
  31. {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/_api_get_post_index.py +0 -0
  32. {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/_api_get_post_list.py +0 -0
  33. {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/_api_get_time.py +0 -0
  34. {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/_api_get_waterball.py +0 -0
  35. {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/_api_has_new_mail.py +0 -0
  36. {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/_api_loginout.py +0 -0
  37. {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/_api_post.py +0 -0
  38. {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/_api_search_user.py +0 -0
  39. {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/_api_set_board_title.py +0 -0
  40. {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/api_server.py +0 -0
  41. {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/check_value.py +0 -0
  42. {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/command.py +0 -0
  43. {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/config.py +0 -0
  44. {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/exceptions.py +0 -0
  45. {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/i18n.py +0 -0
  46. {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/lang_en_US.py +0 -0
  47. {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/lang_zh_TW.py +0 -0
  48. {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/lib_util.py +0 -0
  49. {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/log.py +0 -0
  50. {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt.egg-info/SOURCES.txt +0 -0
  51. {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt.egg-info/dependency_links.txt +0 -0
  52. {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt.egg-info/requires.txt +0 -0
  53. {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt.egg-info/top_level.txt +0 -0
  54. {pyptt-2.0.7 → pyptt-2.0.8}/README.md +0 -0
  55. {pyptt-2.0.7 → pyptt-2.0.8}/pyproject.toml +0 -0
  56. {pyptt-2.0.7 → pyptt-2.0.8}/setup.cfg +0 -0
  57. {pyptt-2.0.7 → pyptt-2.0.8}/setup.py +0 -0
  58. {pyptt-2.0.7 → pyptt-2.0.8}/tests/test_change_pw.py +0 -0
  59. {pyptt-2.0.7 → pyptt-2.0.8}/tests/test_comment.py +0 -0
  60. {pyptt-2.0.7 → pyptt-2.0.8}/tests/test_del_post.py +0 -0
  61. {pyptt-2.0.7 → pyptt-2.0.8}/tests/test_exceptions.py +0 -0
  62. {pyptt-2.0.7 → pyptt-2.0.8}/tests/test_get_board_info.py +0 -0
  63. {pyptt-2.0.7 → pyptt-2.0.8}/tests/test_get_board_list.py +0 -0
  64. {pyptt-2.0.7 → pyptt-2.0.8}/tests/test_get_bottom_post_list.py +0 -0
  65. {pyptt-2.0.7 → pyptt-2.0.8}/tests/test_get_favourite_boards.py +0 -0
  66. {pyptt-2.0.7 → pyptt-2.0.8}/tests/test_get_mail.py +0 -0
  67. {pyptt-2.0.7 → pyptt-2.0.8}/tests/test_get_newest_index.py +0 -0
  68. {pyptt-2.0.7 → pyptt-2.0.8}/tests/test_get_post.py +0 -0
  69. {pyptt-2.0.7 → pyptt-2.0.8}/tests/test_get_post_list.py +0 -0
  70. {pyptt-2.0.7 → pyptt-2.0.8}/tests/test_get_post_parser.py +0 -0
  71. {pyptt-2.0.7 → pyptt-2.0.8}/tests/test_get_time.py +0 -0
  72. {pyptt-2.0.7 → pyptt-2.0.8}/tests/test_get_waterball.py +0 -0
  73. {pyptt-2.0.7 → pyptt-2.0.8}/tests/test_give_money.py +0 -0
  74. {pyptt-2.0.7 → pyptt-2.0.8}/tests/test_i18n.py +0 -0
  75. {pyptt-2.0.7 → pyptt-2.0.8}/tests/test_init.py +0 -0
  76. {pyptt-2.0.7 → pyptt-2.0.8}/tests/test_logger.py +0 -0
  77. {pyptt-2.0.7 → pyptt-2.0.8}/tests/test_parse_query_post.py +0 -0
  78. {pyptt-2.0.7 → pyptt-2.0.8}/tests/test_post.py +0 -0
  79. {pyptt-2.0.7 → pyptt-2.0.8}/tests/test_reply_post.py +0 -0
  80. {pyptt-2.0.7 → pyptt-2.0.8}/tests/test_search_user.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyPtt
3
- Version: 2.0.7
3
+ Version: 2.0.8
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)
@@ -147,6 +147,7 @@ class API:
147
147
 
148
148
  self.connect_core = connect_core.API(self)
149
149
  self._exist_board_list = set()
150
+ self._exist_user_list = set()
150
151
  self._moderators = dict()
151
152
  self._thread_id = threading.get_ident()
152
153
  self._goto_board_list = set()
@@ -1,4 +1,4 @@
1
- __version__ = '2.0.7'
1
+ __version__ = '2.0.8'
2
2
  __author__ = 'CodingMan'
3
3
  __email__ = 'pttcodingman@gmail.com'
4
4
 
@@ -22,7 +22,7 @@ def bucket(api, board: str, bucket_days: int, reason: str, ptt_id: str) -> None:
22
22
  check_value.check_type(reason, str, 'reason')
23
23
  check_value.check_type(ptt_id, str, 'ptt_id')
24
24
 
25
- api.get_user(ptt_id)
25
+ _api_util.check_user_exist(api, ptt_id)
26
26
 
27
27
  _api_util.check_board(api, board, check_moderator=True)
28
28
 
@@ -6,8 +6,7 @@ from . import log
6
6
  from . import screens
7
7
 
8
8
 
9
- def get_call_status(api) -> None:
10
- # log.py = DefaultLogger('api', api.config.log_level)
9
+ def get_call_status(api) -> data_type.CallStatus:
11
10
 
12
11
  cmd_list = []
13
12
  cmd_list.append(command.go_main_menu)
@@ -34,15 +33,15 @@ def get_call_status(api) -> None:
34
33
  raise exceptions.UnknownError('UnknownError')
35
34
 
36
35
  if index == 0:
37
- return data_type.call_status.ON
36
+ return data_type.CallStatus.ON
38
37
  if index == 1:
39
- return data_type.call_status.UNPLUG
38
+ return data_type.CallStatus.UNPLUG
40
39
  if index == 2:
41
- return data_type.call_status.WATERPROOF
40
+ return data_type.CallStatus.WATERPROOF
42
41
  if index == 3:
43
- return data_type.call_status.FRIEND
42
+ return data_type.CallStatus.FRIEND
44
43
  if index == 4:
45
- return data_type.call_status.OFF
44
+ return data_type.CallStatus.OFF
46
45
 
47
46
  ori_screen = api.connect_core.get_screen_queue()[-1]
48
47
  raise exceptions.UnknownError(ori_screen)
@@ -63,10 +62,14 @@ def set_call_status(api, call_status) -> None:
63
62
  target_list = [
64
63
  connect_core.TargetUnit(screens.Target.InUserList, break_detect=True)]
65
64
 
66
- while current_call_status != call_status:
65
+ for _ in range(6):
66
+ if current_call_status == call_status:
67
+ break
67
68
  api.connect_core.send(
68
69
  cmd,
69
70
  target_list,
70
71
  screen_timeout=api.config.screen_long_timeout)
71
72
 
72
73
  current_call_status = api._get_call_status()
74
+ else:
75
+ raise exceptions.UnknownError('could not set call status')
@@ -67,7 +67,7 @@ def _comment(api,
67
67
 
68
68
  cmd_list = []
69
69
 
70
- if index == 0 or index == 1:
70
+ if index in (0, 1, 2):
71
71
  push_option_line = api.connect_core.get_screen_queue()[-1]
72
72
  push_option_line = push_option_line.split('\n')[-1]
73
73
 
@@ -69,22 +69,27 @@ def get_board_info(api, board: str, get_post_kind: bool, call_by_others: bool) -
69
69
  ori_screen = api.connect_core.get_screen_queue()[-1]
70
70
  # print(ori_screen)
71
71
 
72
+ boardname = None
72
73
  p = re.compile(r'《(.+)》看板設定')
73
74
  r = p.search(ori_screen)
74
75
  if r is not None:
75
76
  boardname = r.group(0)[1:-5].strip()
76
77
 
78
+ if boardname is None:
79
+ raise exceptions.NoSuchBoard(api.config, board)
77
80
  logger.debug('看板名稱', boardname, board)
78
81
 
79
82
  if boardname.lower() != board.lower():
80
83
  raise exceptions.NoSuchBoard(api.config, board)
81
84
 
85
+ chinese_des = ''
82
86
  p = re.compile(r'中文敘述: (.+)')
83
87
  r = p.search(ori_screen)
84
88
  if r is not None:
85
89
  chinese_des = r.group(0)[5:].strip()
86
90
  logger.debug('中文敘述', chinese_des)
87
91
 
92
+ moderators = []
88
93
  p = re.compile(r'板主名單: (.+)')
89
94
  r = p.search(ori_screen)
90
95
  if r is not None:
@@ -30,12 +30,21 @@ def get_user(api, ptt_id: str) -> Dict:
30
30
  if len(ptt_id) < 2:
31
31
  raise exceptions.ParameterError(f'wrong parameter user_id: {ptt_id}')
32
32
 
33
- cmd_list = []
34
- cmd_list.append(command.go_main_menu)
35
- cmd_list.append('T')
36
- cmd_list.append(command.enter)
33
+ # Step 1: Navigate to Talk menu.
34
+ # AnyKey auto-dismiss is safe here because no query commands are pending.
35
+ cmd = command.go_main_menu + 'T' + command.enter
37
36
 
38
- cmd = ''.join(cmd_list)
37
+ target_list = [
38
+ connect_core.TargetUnit(screens.Target.InTalk, break_detect=True),
39
+ connect_core.TargetUnit(screens.Target.AnyKey, response=' '),
40
+ ]
41
+
42
+ nav_index = api.connect_core.send(cmd, target_list)
43
+
44
+ # Step 2: Query user.
45
+ # Use AnyKey as break target — it's at the very bottom of the user info
46
+ # screen, so matching it confirms the screen is fully transmitted.
47
+ cmd = 'Q' + command.enter + ptt_id + command.enter
39
48
 
40
49
  # Use InTalk as a non-break target that auto-responds with the
41
50
  # query command, and max_match=1 to prevent re-matching on
@@ -113,7 +122,8 @@ def get_user(api, ptt_id: str) -> Dict:
113
122
 
114
123
  # PTT2 沒有退文
115
124
  if api.config.host == data_type.HOST.PTT1:
116
- illegal_post = int(re.findall(r'\d+', buff_1)[1])
125
+ int_list = re.findall(r'\d+', buff_1)
126
+ illegal_post = int(int_list[1]) if len(int_list) > 1 else 0
117
127
  else:
118
128
  illegal_post = None
119
129
 
@@ -152,5 +162,7 @@ def get_user(api, ptt_id: str) -> Dict:
152
162
  UserField.chess: chess,
153
163
  UserField.signature_file: signature_file,
154
164
  }
165
+ api._exist_user_list.add(ptt_id.lower())
166
+
155
167
  user = json.dumps(user, cls=AutoJsonEncoder)
156
168
  return json.loads(user)
@@ -33,8 +33,7 @@ def give_money(api, ptt_id: str, money: int, red_bag_title: str, red_bag_content
33
33
  log.logger.info(
34
34
  i18n.replace(i18n.give_money_to, ptt_id, money))
35
35
 
36
- # Check data_type.user
37
- api.get_user(ptt_id)
36
+ _api_util.check_user_exist(api, ptt_id)
38
37
 
39
38
  cmd_list = []
40
39
  cmd_list.append(command.go_main_menu)
@@ -35,7 +35,7 @@ def mail(api,
35
35
  check_value.check_type(title, str, 'title')
36
36
  check_value.check_type(content, str, 'content')
37
37
 
38
- api.get_user(ptt_id)
38
+ _api_util.check_user_exist(api, ptt_id)
39
39
 
40
40
  check_sign_file = False
41
41
  for i in range(0, 10):
@@ -277,7 +277,7 @@ def del_mail(api, index) -> None:
277
277
  raise exceptions.UnregisteredUser(lib_util.get_current_func_name())
278
278
 
279
279
  current_index = api.get_newest_index(data_type.NewIndex.MAIL)
280
- check_value.check_index(index, current_index)
280
+ check_value.check_index('index', index, current_index)
281
281
 
282
282
  cmd_list = []
283
283
  # 進入主選單
@@ -75,6 +75,7 @@ def mark_post(api, mark_type: int, board: str, post_aid: str, post_index: int, s
75
75
  raise exceptions.HostNotSupport(lib_util.get_current_func_name())
76
76
 
77
77
  _api_util.check_board(
78
+ api,
78
79
  board,
79
80
  check_moderator=True)
80
81
 
@@ -68,6 +68,8 @@ def reply_post(api, reply_to: data_type.ReplyTo, board: str, content: str, sign_
68
68
  elif reply_to == data_type.ReplyTo.BOARD_MAIL:
69
69
  reply_msg = i18n.reply_board_mail
70
70
  reply_target_unit = connect_core.TargetUnit('▲ 回應至', log_level=log.INFO, response='B' + command.enter)
71
+ else:
72
+ raise exceptions.ParameterError(f'unsupported reply_to: {reply_to}')
71
73
  log.logger.info(reply_msg)
72
74
 
73
75
  cmd = ''.join(cmd_list)
@@ -1,6 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- import functools
4
3
  import re
5
4
  import threading
6
5
  from dataclasses import dataclass
@@ -62,6 +61,8 @@ def get_content(api, post_mode: bool = True):
62
61
  first_page = True
63
62
  origin_post = []
64
63
  stop_dict = dict()
64
+ new_content_part = ''
65
+ get_line_b = 0
65
66
 
66
67
  while True:
67
68
  index = api.connect_core.send(cmd, target_list)
@@ -445,7 +446,11 @@ def one_thread(api):
445
446
  raise exceptions.MultiThreadOperated()
446
447
 
447
448
 
448
- @functools.lru_cache(maxsize=64)
449
+ def check_user_exist(api, ptt_id: str) -> None:
450
+ if ptt_id.lower() not in api._exist_user_list:
451
+ api.get_user(ptt_id)
452
+
453
+
449
454
  def check_board(api, board: str, check_moderator: bool = False) -> Dict:
450
455
  if board.lower() not in api._exist_board_list:
451
456
  board_info = _api_get_board_info.get_board_info(api, board, get_post_kind=False, call_by_others=False)
@@ -249,6 +249,8 @@ class API(object):
249
249
  self._RDQ.add(screen)
250
250
  if target == self._UseTooManyResources:
251
251
  use_too_many_res = True
252
+ find_target = True
253
+ log.logger.info(i18n.use_too_many_resources)
252
254
  break
253
255
  target.raise_exception()
254
256
 
@@ -327,19 +329,22 @@ class API(object):
327
329
  raise exceptions.UseTooManyResources()
328
330
  raise exceptions.ConnectionClosed()
329
331
 
332
+ if isinstance(data_chunk, str):
333
+ data_chunk = data_chunk.encode('utf-8')
330
334
  receive_data_buffer += data_chunk
331
335
 
332
336
  screen, find_target, is_secret, break_detect_after_send, use_too_many_res, msg, target_index = \
333
337
  self._decode_screen(receive_data_buffer, start_time, target_list, is_secret, refresh, msg)
334
338
 
335
- if self.current_encoding == 'big5uao' and not find_target:
336
- self.current_encoding = 'utf-8'
339
+ if not find_target:
340
+ original_encoding = self.current_encoding
341
+ self.current_encoding = 'big5uao' if original_encoding == 'utf-8' else 'utf-8'
337
342
  screen_, find_target, is_secret, break_detect_after_send, use_too_many_res, msg, target_index = \
338
343
  self._decode_screen(receive_data_buffer, start_time, target_list, is_secret, refresh, msg)
339
344
  if find_target:
340
345
  screen = screen_
341
346
  else:
342
- self.current_encoding = 'big5uao'
347
+ self.current_encoding = original_encoding
343
348
 
344
349
  if find_target:
345
350
  break
@@ -348,13 +353,15 @@ class API(object):
348
353
  vt100_p = screens.VT100Parser(receive_data_buffer, self.current_encoding)
349
354
  screens.show(self.config, vt100_p.screen)
350
355
  self._RDQ.add(vt100_p.screen)
356
+ if use_too_many_res:
357
+ raise exceptions.UseTooManyResources()
351
358
  return -1
352
359
 
353
360
  if target_index != -1:
354
361
  return target_index
355
362
 
356
363
  if use_too_many_res:
357
- continue
364
+ raise exceptions.UseTooManyResources()
358
365
 
359
366
  if not find_target:
360
367
  return -1
@@ -366,7 +373,7 @@ class API(object):
366
373
  raise exceptions.ParameterError('Item of TargetList must be TargetUnit')
367
374
 
368
375
  if self._UseTooManyResources not in target_list:
369
- target_list.append(self._UseTooManyResources)
376
+ target_list = target_list + [self._UseTooManyResources]
370
377
 
371
378
  if self.config.connect_mode == data_type.ConnectMode.TELNET:
372
379
  # Original Telnet logic remains, as it doesn't use asyncio
@@ -428,7 +435,11 @@ class API(object):
428
435
 
429
436
  def close(self):
430
437
  if self.config.connect_mode == data_type.ConnectMode.WEBSOCKETS:
431
- if self._core and self._core.open:
438
+ try:
439
+ is_open = not self._core.closed
440
+ except AttributeError:
441
+ is_open = getattr(self._core, 'open', False)
442
+ if self._core and is_open:
432
443
  loop = self._get_event_loop()
433
444
  try:
434
445
  loop.run_until_complete(asyncio.wait_for(self._core.close(), timeout=2.0))
@@ -98,6 +98,14 @@ class PostField(AutoStrEnum):
98
98
  is_unconfirmed = auto()
99
99
 
100
100
 
101
+ class CallStatus(AutoStrEnum):
102
+ ON = auto()
103
+ UNPLUG = auto()
104
+ WATERPROOF = auto()
105
+ FRIEND = auto()
106
+ OFF = auto()
107
+
108
+
101
109
  class WaterballType(AutoStrEnum):
102
110
  SEND = auto()
103
111
  CATCH = auto()
@@ -151,7 +151,7 @@ class Target:
151
151
  ]
152
152
 
153
153
  use_too_many_resources = [
154
- '程式耗用過多計算資源'
154
+ '程式耗用過多'
155
155
  ]
156
156
 
157
157
  Animation = [
@@ -217,11 +217,11 @@ class VT100Parser:
217
217
 
218
218
  def _move(self, x, y):
219
219
  self._cursor_x = x
220
- self._cursor_y = y
220
+ self._cursor_y = min(y, len(self.screen) - 1)
221
221
 
222
222
  def _newline(self):
223
223
  self._cursor_x = 0
224
- self._cursor_y += 1
224
+ self._cursor_y = min(self._cursor_y + 1, len(self.screen) - 1)
225
225
 
226
226
  def _k(self):
227
227
  if self._cursor_x == 0:
@@ -13,7 +13,7 @@ from . import log
13
13
 
14
14
  class Service:
15
15
 
16
- def __init__(self, pyptt_init_config: Optional[dict] = None):
16
+ def __init__(self, pyptt_init_config: Optional[dict] = None, call_interval: float = 0):
17
17
 
18
18
  """
19
19
 
@@ -24,6 +24,7 @@ class Service:
24
24
 
25
25
  Args:
26
26
  pyptt_init_config (dict): PyPtt 初始化設定,請參考 :ref:`初始化設定 <api-init>`。
27
+ call_interval (float): API 呼叫之間的最小時間間隔(秒),預設為 0(不限制)。
27
28
 
28
29
  Returns:
29
30
  None
@@ -48,7 +49,7 @@ class Service:
48
49
  # 'language': PyPtt.Language.ENGLISH,
49
50
  }
50
51
 
51
- service = Service(pyptt_init_config)
52
+ service = Service(pyptt_init_config, call_interval=0.5)
52
53
 
53
54
  try:
54
55
  service.call('login', {'ptt_id': 'YOUR_PTT_ID', 'ptt_pw': 'YOUR_PTT_PW'})
@@ -69,6 +70,10 @@ class Service:
69
70
  if pyptt_init_config is None:
70
71
  pyptt_init_config = {}
71
72
 
73
+ check_value.check_type(call_interval, (int, float), 'call_interval')
74
+ if call_interval < 0:
75
+ raise exceptions.ParameterError('call_interval must be >= 0')
76
+
72
77
  log_level = pyptt_init_config.get('log_level', log.INFO)
73
78
  self.logger = log.init(log_level, 'service')
74
79
 
@@ -77,6 +82,9 @@ class Service:
77
82
  self._api = None
78
83
  self._api_init_config = pyptt_init_config
79
84
 
85
+ self._call_interval = call_interval
86
+ self._last_call_time = 0.0
87
+
80
88
  self._call_queue = Queue()
81
89
  self._call_result = {}
82
90
  self._call_result_cv = Condition()
@@ -106,6 +114,11 @@ class Service:
106
114
  except queue.Empty:
107
115
  continue
108
116
 
117
+ if self._call_interval > 0:
118
+ elapsed = time.monotonic() - self._last_call_time
119
+ if elapsed < self._call_interval:
120
+ time.sleep(self._call_interval - elapsed)
121
+
109
122
  func = getattr(self._api, call['api'])
110
123
 
111
124
  api_result = None
@@ -115,12 +128,14 @@ class Service:
115
128
  except Exception as e:
116
129
  api_exception = e
117
130
 
131
+ self._last_call_time = time.monotonic()
132
+
118
133
  with self._call_result_cv:
119
134
  self._call_result[call['id']] = {
120
135
  'result': api_result,
121
136
  'exception': api_exception
122
137
  }
123
- self._call_result_cv.notify()
138
+ self._call_result_cv.notify_all()
124
139
 
125
140
  def _get_call_id(self):
126
141
 
@@ -160,6 +175,20 @@ class Service:
160
175
 
161
176
  return call_result['result']
162
177
 
178
+ def set_call_interval(self, call_interval: float):
179
+ """設定 API 呼叫之間的最小時間間隔。
180
+
181
+ Args:
182
+ call_interval (float): 最小時間間隔(秒),必須 >= 0。
183
+
184
+ Returns:
185
+ None
186
+ """
187
+ check_value.check_type(call_interval, (int, float), 'call_interval')
188
+ if call_interval < 0:
189
+ raise exceptions.ParameterError('call_interval must be >= 0')
190
+ self._call_interval = call_interval
191
+
163
192
  def close(self):
164
193
  self.logger.info('close')
165
194
  self._close = True
@@ -0,0 +1,24 @@
1
+ key = """-----BEGIN EC PARAMETERS-----
2
+ BggqhkjOPQMBBw==
3
+ -----END EC PARAMETERS-----
4
+ -----BEGIN EC PRIVATE KEY-----
5
+ MHcCAQEEIERB+UA+r+F9qEtBJ52f9p3sWAQkNIl0UZJv6O3vj3qtoAoGCCqGSM49
6
+ AwEHoUQDQgAEgTo7bZCEZpOdQa2o6P6KnSyVhfsFpvnEK0FGiU/V6xz6XWUMIVSy
7
+ +7dG5zGx7/OHa3iK6txCh11mPb+w/DYe8w==
8
+ -----END EC PRIVATE KEY-----
9
+ """
10
+
11
+ cert = """-----BEGIN CERTIFICATE-----
12
+ MIIB6TCCAY8CFCwmWlM+i2IbtHKF0gLrHa2/PqvPMAoGCCqGSM49BAMCMHcxCzAJ
13
+ BgNVBAYTAlRXMQ4wDAYDVQQIDAVTdGF0ZTENMAsGA1UEBwwEQ2l0eTEVMBMGA1UE
14
+ CgwMT3JnYW5pemF0aW9uMRwwGgYDVQQLDBNPcmdhbml6YXRpb25hbCBVbml0MRQw
15
+ EgYDVQQDDAtDb21tb24gTmFtZTAeFw0yNjA0MDUxNTIyNTZaFw0zNjA0MDIxNTIy
16
+ NTZaMHcxCzAJBgNVBAYTAlRXMQ4wDAYDVQQIDAVTdGF0ZTENMAsGA1UEBwwEQ2l0
17
+ eTEVMBMGA1UECgwMT3JnYW5pemF0aW9uMRwwGgYDVQQLDBNPcmdhbml6YXRpb25h
18
+ bCBVbml0MRQwEgYDVQQDDAtDb21tb24gTmFtZTBZMBMGByqGSM49AgEGCCqGSM49
19
+ AwEHA0IABIE6O22QhGaTnUGtqOj+ip0slYX7Bab5xCtBRolP1esc+l1lDCFUsvu3
20
+ Rucxse/zh2t4iurcQoddZj2/sPw2HvMwCgYIKoZIzj0EAwIDSAAwRQIgc2K0N6II
21
+ gvywv1b/rDjIhQdXK6/TF62MWyk5GtrX7N0CIQCMyhwOeA+HYyh4yBbrbP5pA70b
22
+ n6j6snhIR6x2l0HWSw==
23
+ -----END CERTIFICATE-----
24
+ """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyPtt
3
- Version: 2.0.7
3
+ Version: 2.0.8
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)
@@ -17,12 +17,27 @@ def test_get_user(ptt_bots):
17
17
  """Tests getting user information from PTT."""
18
18
  test_users = [
19
19
  'CodingMan',
20
+ 'codingman',
21
+ 'CODINGMAN',
22
+ 'cOdInGmAn',
20
23
  ]
21
24
 
22
- for _ in range(100):
25
+ for _ in range(3):
23
26
  for test_user in test_users:
24
27
  user_info = ptt_bots[0].get_user(test_user)
25
28
  _check_user_info(user_info, test_user)
26
29
 
27
- time.sleep(1)
28
30
 
31
+ def test_get_user_no_such_user(ptt_bots):
32
+ """Tests that querying a non-existent user raises NoSuchUser."""
33
+ import PyPtt
34
+ import pytest
35
+
36
+ no_such_users = [
37
+ 'zzznotexist999',
38
+ 'qqzzxxyy123',
39
+ ]
40
+
41
+ for user in no_such_users:
42
+ with pytest.raises(PyPtt.exceptions.NoSuchUser):
43
+ ptt_bots[0].get_user(user)
@@ -5,6 +5,53 @@ import time
5
5
  from PyPtt import Service, PostField
6
6
  from tests import config # Assuming config.py has PTT1_ID and PTT1_PW
7
7
 
8
+ class TestCallInterval:
9
+ """Tests for call_interval feature (no network required)."""
10
+
11
+ def test_default_call_interval(self):
12
+ service = Service()
13
+ assert service._call_interval == 0
14
+ service.close()
15
+
16
+ def test_init_with_call_interval(self):
17
+ service = Service(call_interval=1.5)
18
+ assert service._call_interval == 1.5
19
+ service.close()
20
+
21
+ def test_init_with_int_call_interval(self):
22
+ service = Service(call_interval=2)
23
+ assert service._call_interval == 2
24
+ service.close()
25
+
26
+ def test_set_call_interval(self):
27
+ service = Service()
28
+ service.set_call_interval(0.5)
29
+ assert service._call_interval == 0.5
30
+ service.set_call_interval(0)
31
+ assert service._call_interval == 0
32
+ service.close()
33
+
34
+ def test_negative_call_interval_init(self):
35
+ with pytest.raises(PyPtt.exceptions.ParameterError):
36
+ Service(call_interval=-1)
37
+
38
+ def test_negative_call_interval_setter(self):
39
+ service = Service()
40
+ with pytest.raises(PyPtt.exceptions.ParameterError):
41
+ service.set_call_interval(-1)
42
+ service.close()
43
+
44
+ def test_invalid_type_call_interval_init(self):
45
+ with pytest.raises(TypeError):
46
+ Service(call_interval='fast')
47
+
48
+ def test_invalid_type_call_interval_setter(self):
49
+ service = Service()
50
+ with pytest.raises(TypeError):
51
+ service.set_call_interval('fast')
52
+ service.close()
53
+
54
+
8
55
  @pytest.fixture(scope="module", autouse=True)
9
56
  def service_instance():
10
57
  import time
@@ -1,24 +0,0 @@
1
- key = """-----BEGIN EC PARAMETERS-----
2
- BggqhkjOPQMBBw==
3
- -----END EC PARAMETERS-----
4
- -----BEGIN EC PRIVATE KEY-----
5
- MHcCAQEEIFvkfPbkl4Ke99iXWTXG1LJnhFeMFREJWW9d4I76PZmGoAoGCCqGSM49
6
- AwEHoUQDQgAErWNfoKcmEmi+0tNm2CCne7MhF0+4iKZYJvtrn5kxRG73XWhoR79c
7
- 7tfex7gLuFn/VXiP+rgDOe4MLT1LGrtD4A==
8
- -----END EC PRIVATE KEY-----
9
- """
10
-
11
- cert = """-----BEGIN CERTIFICATE-----
12
- MIIB6TCCAY8CFFzZNObjJZqIs69GFDYbSXJqNpBIMAoGCCqGSM49BAMCMHcxCzAJ
13
- BgNVBAYTAlRXMQ4wDAYDVQQIDAVTdGF0ZTENMAsGA1UEBwwEQ2l0eTEVMBMGA1UE
14
- CgwMT3JnYW5pemF0aW9uMRwwGgYDVQQLDBNPcmdhbml6YXRpb25hbCBVbml0MRQw
15
- EgYDVQQDDAtDb21tb24gTmFtZTAeFw0yNjA0MDMxNDM1MzRaFw0zNjAzMzExNDM1
16
- MzRaMHcxCzAJBgNVBAYTAlRXMQ4wDAYDVQQIDAVTdGF0ZTENMAsGA1UEBwwEQ2l0
17
- eTEVMBMGA1UECgwMT3JnYW5pemF0aW9uMRwwGgYDVQQLDBNPcmdhbml6YXRpb25h
18
- bCBVbml0MRQwEgYDVQQDDAtDb21tb24gTmFtZTBZMBMGByqGSM49AgEGCCqGSM49
19
- AwEHA0IABK1jX6CnJhJovtLTZtggp3uzIRdPuIimWCb7a5+ZMURu911oaEe/XO7X
20
- 3se4C7hZ/1V4j/q4AznuDC09Sxq7Q+AwCgYIKoZIzj0EAwIDSAAwRQIhAOAM8IPM
21
- 5D4BoaWYf3SaSVzrm8v6FtSIhWrSYJatYjwoAiAW3NggrC+qYY3wYRDq8UmaiHLv
22
- pkJCEsbjl+remcwPbg==
23
- -----END CERTIFICATE-----
24
- """
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