fpx-engine 0.7.4__tar.gz → 0.7.6__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 (58) hide show
  1. {fpx_engine-0.7.4 → fpx_engine-0.7.6}/PKG-INFO +9 -5
  2. {fpx_engine-0.7.4 → fpx_engine-0.7.6}/README.md +8 -4
  3. {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/__init__.py +2 -2
  4. fpx_engine-0.7.6/fpx/_api/_client.py +173 -0
  5. {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/_parsers/__init__.py +3 -2
  6. fpx_engine-0.7.6/fpx/_parsers/_base.py +16 -0
  7. {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/_parsers/_category.py +11 -12
  8. fpx_engine-0.7.6/fpx/_parsers/_chats.py +116 -0
  9. fpx_engine-0.7.6/fpx/_parsers/_lots.py +191 -0
  10. fpx_engine-0.7.6/fpx/_parsers/_orders.py +149 -0
  11. fpx_engine-0.7.6/fpx/_parsers/_profile.py +220 -0
  12. {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/classes/account/account.py +10 -8
  13. {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/classes/account/subclasses/addons.py +8 -16
  14. {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/classes/account/subclasses/category.py +17 -19
  15. {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/classes/account/subclasses/chat.py +35 -35
  16. {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/classes/account/subclasses/editor.py +46 -69
  17. {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/classes/account/subclasses/lot.py +36 -39
  18. {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/classes/account/subclasses/order.py +25 -25
  19. {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/classes/account/subclasses/profile.py +63 -49
  20. {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/classes/account/subclasses/review.py +9 -9
  21. {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/classes/runner/runner.py +35 -34
  22. {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/classes/runner/subclasses/_category.py +12 -18
  23. {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/classes/runner/subclasses/_chat.py +69 -55
  24. {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/classes/runner/subclasses/_order.py +30 -29
  25. {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/classes/runner/subclasses/_review.py +13 -12
  26. {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/classes/runner/subclasses/router.py +98 -91
  27. {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/main.py +22 -12
  28. {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/middlewares/_request_engine.py +18 -17
  29. {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/models/__init__.py +1 -1
  30. {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/models/account.py +26 -20
  31. {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/models/chat.py +8 -9
  32. {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/models/lots.py +58 -49
  33. {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/services/__init__.py +8 -8
  34. {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/utils/errors.py +104 -41
  35. {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/utils/storage/base.py +5 -5
  36. {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/utils/storage/file.py +6 -6
  37. {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/utils/storage/memory.py +5 -5
  38. {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/utils/storage/redis.py +7 -8
  39. {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx_engine.egg-info/PKG-INFO +9 -5
  40. {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx_engine.egg-info/SOURCES.txt +1 -2
  41. {fpx_engine-0.7.4 → fpx_engine-0.7.6}/pyproject.toml +1 -1
  42. fpx_engine-0.7.4/fpx/_api/_client.py +0 -211
  43. fpx_engine-0.7.4/fpx/_parsers/_base.py +0 -16
  44. fpx_engine-0.7.4/fpx/_parsers/_chats.py +0 -113
  45. fpx_engine-0.7.4/fpx/_parsers/_lots.py +0 -194
  46. fpx_engine-0.7.4/fpx/_parsers/_orders.py +0 -152
  47. fpx_engine-0.7.4/fpx/_parsers/_profile.py +0 -214
  48. fpx_engine-0.7.4/tests/test_happy_path.py +0 -43
  49. {fpx_engine-0.7.4 → fpx_engine-0.7.6}/LICENSE +0 -0
  50. {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/fsm/__init__.py +0 -0
  51. {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/fsm/fsm.py +0 -0
  52. {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/py.typed +0 -0
  53. {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/utils/dependencies.py +0 -0
  54. {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/utils/storage/__init__.py +0 -0
  55. {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx_engine.egg-info/dependency_links.txt +0 -0
  56. {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx_engine.egg-info/requires.txt +0 -0
  57. {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx_engine.egg-info/top_level.txt +0 -0
  58. {fpx_engine-0.7.4 → fpx_engine-0.7.6}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fpx-engine
3
- Version: 0.7.4
3
+ Version: 0.7.6
4
4
  Summary: Автоматизация работы с FunPay
5
5
  Author-email: Be My Code <gettofarmila@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/bymyforge/fpx
@@ -76,19 +76,23 @@ pip install -U fpx-engine
76
76
  import asyncio
77
77
  from fpx import FunPayTools, types
78
78
 
79
+
79
80
  async def main():
80
81
  # инициализируем аккаунт (golden_key и golden_seal - куки твоего аккаунта на funpay.com)
81
- fp = FunPayTools('golden_key', 'golden_seal')
82
+ fp = FunPayTools("golden_key", "golden_seal")
83
+
82
84
  # ловим сообщение
83
85
  @fp.router.on_message()
84
86
  async def answer_message(message: types.Message):
85
87
  # отвечаем на сообщение
86
- await message.answer('Привет')
87
- #запускаем приём событий
88
+ await message.answer("Привет")
89
+
90
+ # запускаем приём событий
88
91
  await fp.runner.start_polling(3, is_background=True)
89
92
  await fp.runner.idle()
90
93
 
91
- if __name__ == '__main__':
94
+
95
+ if __name__ == "__main__":
92
96
  asyncio.run(main())
93
97
  ```
94
98
 
@@ -51,19 +51,23 @@ pip install -U fpx-engine
51
51
  import asyncio
52
52
  from fpx import FunPayTools, types
53
53
 
54
+
54
55
  async def main():
55
56
  # инициализируем аккаунт (golden_key и golden_seal - куки твоего аккаунта на funpay.com)
56
- fp = FunPayTools('golden_key', 'golden_seal')
57
+ fp = FunPayTools("golden_key", "golden_seal")
58
+
57
59
  # ловим сообщение
58
60
  @fp.router.on_message()
59
61
  async def answer_message(message: types.Message):
60
62
  # отвечаем на сообщение
61
- await message.answer('Привет')
62
- #запускаем приём событий
63
+ await message.answer("Привет")
64
+
65
+ # запускаем приём событий
63
66
  await fp.runner.start_polling(3, is_background=True)
64
67
  await fp.runner.idle()
65
68
 
66
- if __name__ == '__main__':
69
+
70
+ if __name__ == "__main__":
67
71
  asyncio.run(main())
68
72
  ```
69
73
 
@@ -7,7 +7,7 @@ from .main import FunPayTools as FunPayTools
7
7
  from .utils import errors as errors
8
8
  from .utils.dependencies import Dependency as Dependency
9
9
 
10
- logger = logging.getLogger('fpx')
10
+ logger = logging.getLogger("fpx")
11
11
  logger.addHandler(logging.NullHandler())
12
12
 
13
- __all__ = ['Runner', 'Router', 'FunPayTools', 'types', 'errors', 'Dependency']
13
+ __all__ = ["Runner", "Router", "FunPayTools", "types", "errors", "Dependency"]
@@ -0,0 +1,173 @@
1
+ import json
2
+
3
+
4
+ class FunPayClient:
5
+ def __init__(self, account, http_client):
6
+ self._account = account
7
+ self.client = http_client
8
+
9
+ async def get_chats_page(self) -> str:
10
+ r = await self._account._request_engine.execute("GET", "/chat/")
11
+ return r.text
12
+
13
+ async def get_finance_page(self) -> str:
14
+ r = await self._account._request_engine.execute("GET", "/account/balance")
15
+ return r.text
16
+
17
+ async def send_message_request(self, node_name, last_msg, text):
18
+ request_data = {
19
+ "action": "chat_message",
20
+ "data": {"node": node_name, "last_message": last_msg, "content": text},
21
+ }
22
+ payload = {"request": json.dumps(request_data)}
23
+ headers = {
24
+ "X-Requested-With": "XMLHttpRequest",
25
+ "Referer": f"https://funpay.com/chat/?node={node_name.split('-')[-1]}",
26
+ }
27
+ r = await self._account._request_engine.execute("POST", "/runner/", data=payload, headers=headers)
28
+ return r.json()
29
+
30
+ async def send_image_request(self, node_name, last_msg, image_id):
31
+ request_data = {
32
+ "action": "chat_message",
33
+ "data": {"node": node_name, "last_message": last_msg, "content": "", "image_id": image_id},
34
+ }
35
+ payload = {"request": json.dumps(request_data)}
36
+ headers = {
37
+ "X-Requested-With": "XMLHttpRequest",
38
+ "Referer": f"https://funpay.com/chat/?node={node_name.split('-')[-1]}",
39
+ }
40
+ r = await self._account._request_engine.execute("POST", "/runner/", data=payload, headers=headers)
41
+ return r.json()
42
+
43
+ async def get_current_chat(self, chat_id):
44
+ r = await self._account._request_engine.execute("GET", f"/chat/?node={chat_id}")
45
+ return r.text
46
+
47
+ async def get_user_profile(self, user_id):
48
+ r = await self._account._request_engine.execute("GET", f"/users/{user_id}/")
49
+ return r.text
50
+
51
+ async def lot_menu_by_category(self, category_id):
52
+ r = await self._account._request_engine.execute("GET", f"/lots/{category_id}/trade")
53
+ return r.text
54
+
55
+ async def get_main_menu(self):
56
+ r = await self._account._request_engine.execute("GET", "/")
57
+ return r.text
58
+
59
+ async def raise_lot(self, node_id, game_id):
60
+ payload = {"game_id": game_id, "node_id": node_id}
61
+ headers = {"X-Requested-With": "XMLHttpRequest"}
62
+ r = await self._account._request_engine.execute("POST", "/lots/raise", data=payload, headers=headers)
63
+ if "application/json" in r.headers.get("Content-Type", ""):
64
+ response = r.json()
65
+ return response.get("msg")
66
+ else:
67
+ return {"error": "not_json", "status": r.status_code}
68
+
69
+ async def get_lot_info(self, lot_id):
70
+ r = await self._account._request_engine.execute("GET", f"/lots/offer?id={lot_id}")
71
+ return r.text
72
+
73
+ async def get_my_sells(self):
74
+ r = await self._account._request_engine.execute("GET", "/orders/trade")
75
+ return r.text
76
+
77
+ async def refund_order(self, order_id):
78
+ url = "/orders/refund"
79
+ payload = {"id": order_id}
80
+ r = await self._account._request_engine.execute("POST", url, data=payload)
81
+ return r
82
+
83
+ async def get_order_info(self, order_id):
84
+ r = await self._account._request_engine.execute("GET", f"/orders/{order_id}/")
85
+ return r.text
86
+
87
+ async def get_lot_editor_data(self, lot_id):
88
+ r = await self._account._request_engine.execute("GET", f"/lots/offerEdit?offer={lot_id}")
89
+ return r.text
90
+
91
+ async def get_node_editor_data(self, node_id):
92
+ r = await self._account._request_engine.execute("GET", f"/lots/offerEdit?node={node_id}")
93
+ return r.text
94
+
95
+ async def edit_lot(self, lot, active=None):
96
+ payload = {
97
+ "form_created_at": lot.form_created_at,
98
+ "offer_id": lot.offer_id,
99
+ "node_id": lot.node_id,
100
+ "location": lot.location if lot.location else "offer",
101
+ "deleted": lot.deleted,
102
+ }
103
+ payload.update(lot.fields)
104
+ payload.pop("query", None)
105
+ if active is not None:
106
+ if active:
107
+ payload["active"] = "on"
108
+ else:
109
+ payload.pop("active", None)
110
+ headers = {
111
+ "Accept": "application/json, text/javascript, */*; q=0.01",
112
+ "X-Requested-With": "XMLHttpRequest",
113
+ "Referer": f"https://funpay.com/lots/offerEdit?node={lot.node_id}&offer={lot.offer_id}&location=offer",
114
+ }
115
+ r = await self._account._request_engine.execute("POST", "/lots/offerSave", data=payload, headers=headers)
116
+ return r
117
+
118
+ async def create_lot(self, lot):
119
+ payload = {
120
+ "form_created_at": lot._form_created_at,
121
+ "offer_id": lot._offer_id,
122
+ "node_id": lot._node_id,
123
+ "location": lot._location,
124
+ "deleted": lot._deleted,
125
+ "active": "on",
126
+ }
127
+ fields = {f.key: f.value for f in lot.fields}
128
+ payload.update(fields)
129
+ payload.pop("query", None)
130
+ headers = {
131
+ "Accept": "application/json, text/javascript, */*; q=0.01",
132
+ "X-Requested-With": "XMLHttpRequest",
133
+ "Referer": f"https://funpay.com/lots/offerEdit?node={lot._node_id}",
134
+ "Origin": "https://funpay.com",
135
+ }
136
+ r = await self._account._request_engine.execute("POST", "/lots/offerSave", data=payload, headers=headers)
137
+ return r
138
+
139
+ async def answer_review(self, authorid: str, text: str, orderid: str):
140
+ payload = {"authorId": authorid, "text": text, "rating": "", "orderId": orderid}
141
+ headers = {"X-Requested-With": "XMLHttpRequest"}
142
+ response = await self._account._request_engine.execute("POST", "/orders/review", data=payload, headers=headers)
143
+ return response
144
+
145
+ async def get_chip_category(self, chip_category_id):
146
+ r = await self._account._request_engine.execute("GET", f"/chips/{chip_category_id}/")
147
+ return r.text
148
+
149
+ async def get_lot_category(self, lot_category_id):
150
+ r = await self._account._request_engine.execute("GET", f"/lots/{lot_category_id}/")
151
+ return r.text
152
+
153
+ async def upload_image(self, file_bytes):
154
+ headers = {"X-Requested-With": "XMLHttpRequest"}
155
+ r = await self.client.request(
156
+ "POST", "/file/addChatImage", files={"file": ("image.png", file_bytes, "image/png")}, headers=headers
157
+ )
158
+ return r.json()
159
+
160
+ async def find_category(self, target):
161
+ headers = {"X-Requested-With": "XMLHttpRequest"}
162
+ r = await self.client.request("POST", "/games/promoFilter", data={"query": target}, headers=headers)
163
+ return r.json()
164
+
165
+ async def calc_category_price(self, price, node_id):
166
+ headers = {"X-Requested-With": "XMLHttpRequest"}
167
+ r = await self.client.request("POST", "/lots/calc", data={"nodeId": node_id, "price": price}, headers=headers)
168
+ return r.json()
169
+
170
+ async def get_next_sells(self, next_page_id):
171
+ headers = {"X-Requested-With": "XMLHttpRequest"}
172
+ r = await self.client.request("POST", "/orders/trade", data={"continue": next_page_id}, headers=headers)
173
+ return r.text
@@ -6,8 +6,9 @@ from ._profile import ProfileParser
6
6
 
7
7
 
8
8
  class FpxParser(ChatParser, LotParser, OrderParser, ProfileParser, CategoryParser):
9
- '''
9
+ """
10
10
  Главный парсер fpx
11
11
  Собирает внутри себя методы из отдельных модулей.
12
- '''
12
+ """
13
+
13
14
  pass
@@ -0,0 +1,16 @@
1
+ import re
2
+
3
+ from bs4 import BeautifulSoup
4
+
5
+
6
+ class BaseParser:
7
+ @staticmethod
8
+ def _safe_parse_links(html_content: str, pattern: str):
9
+ """Fallback метод"""
10
+ soup = BeautifulSoup(html_content, "html.parser")
11
+ return soup.find_all("a", href=re.compile(pattern))
12
+
13
+ @staticmethod
14
+ def clean_text(element) -> str:
15
+ """Безопасный сбор текста, если элемент не найден, вернет пустую строку"""
16
+ return element.text.strip() if element else ""
@@ -10,24 +10,23 @@ logger = logging.getLogger("fpx.category_parser")
10
10
 
11
11
 
12
12
  class CategoryParser(BaseParser):
13
-
14
13
  @classmethod
15
14
  def parse_all_categories(cls, html_content):
16
- '''Парсит главную страницу фп на лоты'''
17
- soup = BeautifulSoup(html_content, 'html.parser')
18
- items = soup.find_all('div', class_='promo-game-item')
15
+ """Парсит главную страницу фп на лоты"""
16
+ soup = BeautifulSoup(html_content, "html.parser")
17
+ items = soup.find_all("div", class_="promo-game-item")
19
18
  result = []
20
19
  for item in items:
21
- game_title = item.find('div', class_='game-title')
20
+ game_title = item.find("div", class_="game-title")
22
21
  game_name = game_title.get_text(strip=True)
23
- data_id = game_title.get('data-id')
24
- subcategory_str = item.find('ul', class_='list-inline')
25
- subcategory_list = subcategory_str.find_all('li')
22
+ data_id = game_title.get("data-id")
23
+ subcategory_str = item.find("ul", class_="list-inline")
24
+ subcategory_list = subcategory_str.find_all("li")
26
25
  subcategories = []
27
26
  for s in subcategory_list:
28
- a = s.find('a')
29
- href = a.get('href')
30
- category_id = href.split('/')[-2]
27
+ a = s.find("a")
28
+ href = a.get("href")
29
+ category_id = href.split("/")[-2]
31
30
  name = s.get_text()
32
31
  subcategories.append(GameSubCategory(id=category_id, sub_name=name))
33
32
  title = GameTitle(id=data_id, name=game_name)
@@ -36,4 +35,4 @@ class CategoryParser(BaseParser):
36
35
 
37
36
  @classmethod
38
37
  def parse_category_browser(cls, html_content):
39
- BeautifulSoup(html_content, 'html.parser')
38
+ BeautifulSoup(html_content, "html.parser")
@@ -0,0 +1,116 @@
1
+ import json
2
+ import logging
3
+ import re
4
+ from typing import Any
5
+
6
+ from bs4 import BeautifulSoup
7
+
8
+ from fpx.models.chat import Chat
9
+ from fpx.utils import errors as fpx_err
10
+
11
+ from ._base import BaseParser
12
+
13
+ logger = logging.getLogger("fpx.chat_parser")
14
+
15
+
16
+ class ChatParser(BaseParser):
17
+ @classmethod
18
+ def parse_chats_list(cls, html_content: str) -> list[Chat]:
19
+ """Парсит страницу https://funpay.com/chat/"""
20
+ soup = BeautifulSoup(html_content, "html.parser")
21
+ items = soup.find_all("a", class_="contact-item")
22
+ if not items:
23
+ items = cls._safe_parse_links(html_content, r"node=\d+")
24
+ if not items:
25
+ raise fpx_err.FpxNullDataError("На странице не найдено ни одного чата")
26
+ chats = []
27
+ for item in items:
28
+ try:
29
+ href = str(item.get("href", ""))
30
+ node_msg_id = int(str(item.get("data-node-msg", "0")))
31
+ chat_id = href.split("node=")[-1] if "node=" in href else ""
32
+ username = cls.clean_text(item.find("div", class_="media-user-name"))
33
+ last_msg = cls.clean_text(item.find("div", class_="contact-item-message"))
34
+ date = cls.clean_text(item.find("div", class_="contact-item-time"))
35
+ is_unread = "unread" in (item.get("class", "") or [])
36
+ chats.append(
37
+ Chat(
38
+ id=chat_id,
39
+ node_msg_id=node_msg_id,
40
+ username=username,
41
+ last_msg=last_msg,
42
+ date=date,
43
+ link=href,
44
+ is_unread=is_unread,
45
+ )
46
+ )
47
+ except Exception as e:
48
+ logger.debug(f"Ошибка парсинга отдельного чата: {e}. Пропускаем элемент.")
49
+ continue
50
+ if not chats:
51
+ raise fpx_err.FpxParseError(
52
+ "Не удалось распарсить ни один чат, верстка полностью изменилась, или что-то сломалось."
53
+ )
54
+ return chats
55
+
56
+ @classmethod
57
+ def parse_chat(cls, html_content: str):
58
+ """Парсит страницу https://funpay.com/chat/?node=..."""
59
+ soup = BeautifulSoup(html_content, "html.parser")
60
+ result: dict[str, Any] = {}
61
+ chat_div = soup.find("div", class_="chat")
62
+ if not chat_div:
63
+ chat_div = soup.find("div", attrs={"data-id": re.compile(r"^\d+$")})
64
+ body = soup.find("body")
65
+ if not chat_div or not body:
66
+ raise fpx_err.FpxNullDataError("На странице чата не найден блок переписки или тег body")
67
+ chats = soup.find_all("div", class_="chat-msg-item")
68
+ result["messages"] = []
69
+ if chats:
70
+ for chat in chats:
71
+ try:
72
+ res: dict[str, Any] = {"is_system": False, "node_id": chat.get("id").split("-")[-1]}
73
+ msg_tag = chat.find("div", class_="chat-msg-text")
74
+ if msg_tag:
75
+ message = msg_tag.get_text(separator="\n").strip() if msg_tag else ""
76
+ if not message:
77
+ img_link = msg_tag.find("a", class_="chat-img-link")
78
+ message = img_link.get("href", "") if img_link else "" # type: ignore[assignment]
79
+ res["message"] = message
80
+ else:
81
+ res["message"] = ""
82
+ author_block = None
83
+ current_node = chat
84
+ while current_node:
85
+ author_block = current_node.find("div", class_="media-user-name")
86
+ if author_block:
87
+ break
88
+ current_node = current_node.find_previous_sibling("div", class_="chat-msg-item") # type: ignore[assignment]
89
+ if author_block:
90
+ author = author_block.find("a", class_="chat-msg-author-link")
91
+ if not author:
92
+ sender_lbl = author_block.find("span", class_="chat-msg-author-label")
93
+ res["sender"] = cls.clean_text(sender_lbl) if sender_lbl else "FunPay"
94
+ if res["sender"] and res["sender"].lower() == "оповещение":
95
+ res["sender"] = "FunPay" # type: ignore[assignment]
96
+ res["is_system"] = True
97
+ else:
98
+ res["sender"] = author.get_text(strip=True) # type: ignore[assignment]
99
+ else:
100
+ res["sender"] = "Unknown" # type: ignore[assignment]
101
+ result["messages"].append(res)
102
+ except Exception as e:
103
+ logger.debug(f"Не удалось распарсить N сообщение в чате: {e}")
104
+ else:
105
+ logger.debug("Сообщений не найдено!")
106
+ # парсинг тех.данных
107
+ try:
108
+ result["data-name"] = chat_div.get("data-name", "") # type: ignore[assignment]
109
+ app_data_str = str(body.get("data-app-data", "{}") or "{}")
110
+ app_data = json.loads(app_data_str)
111
+ result["csrf-token"] = app_data.get("csrf-token", "")
112
+ result["user-id"] = app_data.get("userId", "")
113
+ except Exception as e:
114
+ logger.debug(f"Ошибка извлечения системных данных чата: {e}")
115
+ raise fpx_err.FpxParseError("Не удалось распарсить системные метаданные чата (CSRF/User ID)")
116
+ return result
@@ -0,0 +1,191 @@
1
+ import logging
2
+
3
+ from bs4 import BeautifulSoup
4
+
5
+ from fpx.utils import errors as fpx_err
6
+
7
+ from ._base import BaseParser
8
+
9
+ logger = logging.getLogger("fpx.lot_parser")
10
+
11
+
12
+ class LotParser(BaseParser):
13
+ @classmethod
14
+ def parse_lot_menu(cls, html_content: str):
15
+ """Парсит страницу pay.com/lots/.../trade"""
16
+ soup = BeautifulSoup(html_content, "html.parser")
17
+ button = soup.find("button", class_="js-lot-raise")
18
+ if not button:
19
+ button = soup.find("button", attrs={"data-game": True})
20
+ if button:
21
+ data_game = button.get("data-game")
22
+ if data_game:
23
+ return data_game
24
+ raise fpx_err.FpxParseError("Атрибут data-game не найден внутри кнопки поднятия")
25
+ raise fpx_err.FpxNullDataError(
26
+ "На странице лотов не найдена кнопка для поднятия.Возможно у вас нет созданных лотов в этой категории"
27
+ )
28
+
29
+ @classmethod
30
+ def parse_current_lot_menu(cls, html_content):
31
+ """https://funpay.com/lots/offer?id=..."""
32
+ result = {}
33
+ soup = BeautifulSoup(html_content, "html.parser")
34
+ param_items = soup.find_all("div", class_="param-item")
35
+ if not param_items:
36
+ param_items = [h5.find_parent("div") for h5 in soup.find_all("h5") if h5.find_parent("div")]
37
+ if not param_items:
38
+ raise fpx_err.FpxNullDataError(
39
+ "Страница лота не найдена. Возможно, указана инвалидная ссылка или лот был удалён."
40
+ )
41
+ else:
42
+ descriptions = {}
43
+ for item in param_items:
44
+ try:
45
+ header_tag = item.find(["h5", "h4", "label"])
46
+ content_tag = item.find("div") or item.find("textarea")
47
+ if header_tag and content_tag:
48
+ header = header_tag.get_text(strip=True)
49
+ text = content_tag.get_text(separator="\n", strip=True)
50
+ descriptions[header] = text
51
+ except Exception as e:
52
+ logger.debug(f"При парсинге конкретного объекта произошла ошибка: {e}")
53
+ result["short_desc"] = (
54
+ descriptions.get("Краткое описание")
55
+ or descriptions.get("Short description")
56
+ or descriptions.get("Короткий опис")
57
+ )
58
+ result["description"] = (
59
+ descriptions.get("Подробное описание")
60
+ or descriptions.get("Detailed description")
61
+ or descriptions.get("Докладний опис")
62
+ )
63
+ option = soup.find("option", value="21") or soup.find("option", attrs={"data-content": True})
64
+ if option:
65
+ inner_html = option.get("data-content")
66
+ if inner_html:
67
+ inner_soup = BeautifulSoup(inner_html, "html.parser")
68
+ price_span = inner_soup.find("span", class_="payment-value")
69
+ raw_price = price_span.get_text(strip=True) if price_span else inner_soup.get_text(strip=True)
70
+ raw_price = raw_price.replace(",", ".")
71
+ cleaned_price = "".join(c for c in raw_price if c.isdigit() or c == ".")
72
+ try:
73
+ result["price"] = float(cleaned_price) if cleaned_price else 0.0
74
+ except ValueError:
75
+ result["price"] = 0.0
76
+ return result
77
+ raise fpx_err.FpxNullDataError("Не удалось найти цену в скрытых атрибутах выбора оплаты.")
78
+
79
+ @classmethod
80
+ def parse_edit_lot_page(cls, html_content):
81
+ """https://funpay.com/lots/offerEdit?node=...&offer=..."""
82
+ soup = BeautifulSoup(html_content, "html.parser")
83
+ result = {}
84
+ hidden_inputs = soup.find_all("input", type="hidden")
85
+ if not hidden_inputs:
86
+ raise fpx_err.FpxNullDataError("Не найдено вводных данных в редакторе лота. Возможно слетела сессия")
87
+ result = {tag.get("name"): tag.get("value", "") for tag in hidden_inputs if tag.get("name")}
88
+ selects = soup.find_all("select")
89
+ if not selects:
90
+ raise fpx_err.FpxNullDataError(
91
+ "Ни одна выборка в редакторе лотов не найдена. Проверьте актуальность сессии"
92
+ )
93
+ for s in selects:
94
+ try:
95
+ name = s.get("name")
96
+ if not name:
97
+ continue
98
+ selected_option = s.find("option", selected=True)
99
+ if selected_option:
100
+ result[name] = selected_option.get("value", "")
101
+ else:
102
+ first_opt = s.find("option")
103
+ result[name] = first_opt.get("value", "") if first_opt else ""
104
+ except Exception as e:
105
+ logger.debug(f"При парсинге конкретной выборки произошла ошибка: {e}")
106
+ inputs = soup.find_all("input", class_="form-control")
107
+ if not inputs:
108
+ inputs = [
109
+ i
110
+ for i in soup.find_all("input")
111
+ if i.get("name") and i.get("type") in ["text", "number", None] and i.get("type") != "hidden"
112
+ ]
113
+ if not inputs:
114
+ logger.debug("Ни одно поле для ввода в редакторе лотов не найдено. Возможно всё в порядке")
115
+ else:
116
+ for i in inputs:
117
+ try:
118
+ name = i.get("name")
119
+ if name:
120
+ result[name] = i.get("value", "")
121
+ except Exception as e:
122
+ logger.debug(f"При парсинге конкретного поля для ввода произошла ошибка: {e}")
123
+ textareas = soup.find_all("textarea")
124
+ if not textareas:
125
+ logger.debug("При парсинге редактора лотов не найдено ни одного текстового поля. Возможно всё в порядке")
126
+ else:
127
+ for t in textareas:
128
+ try:
129
+ name = t.get("name")
130
+ if name:
131
+ result[name] = t.get_text().strip()
132
+ except Exception as e:
133
+ logger.debug(f"При парсинге конкретного текстового поля произошла ошибка: {e}")
134
+ return result
135
+
136
+ @classmethod
137
+ def parse_create_lot_page(cls, html_content):
138
+ """https://funpay.com/lots/offerEdit?node=..."""
139
+ soup = BeautifulSoup(html_content, "html.parser")
140
+ result = {}
141
+ hidden_inputs = soup.find_all("input", type="hidden")
142
+ if not hidden_inputs:
143
+ raise fpx_err.FpxNullDataError("Не найдено вводных данных в редакторе лота. Возможно слетела сессия")
144
+ result = {tag.get("name"): tag.get("value", "") for tag in hidden_inputs if tag.get("name")}
145
+ selects = soup.find_all("select")
146
+ if not selects:
147
+ raise fpx_err.FpxNullDataError(
148
+ "Ни одна выборка в редакторе лотов не найдена. Проверьте актуальность сессии"
149
+ )
150
+ for s in selects:
151
+ try:
152
+ name = s.get("name", "")
153
+ options = s.find_all("option")
154
+ value_list = []
155
+ for option in options:
156
+ value_name = option.get_text(strip=True)
157
+ value = option.get("value", "")
158
+ if value_name and value:
159
+ value_list.append({value_name: value})
160
+ result[name] = value_list
161
+ except Exception as e:
162
+ logger.debug(f"При парсинге конкретной выборки произошла ошибка: {e}")
163
+ inputs = soup.find_all("input", class_="form-control")
164
+ if not inputs:
165
+ inputs = [
166
+ i
167
+ for i in soup.find_all("input")
168
+ if i.get("name") and i.get("type") in ["text", "number", None] and i.get("type") != "hidden"
169
+ ]
170
+ if not inputs:
171
+ logger.debug("Ни одно поле для ввода в редакторе лотов не найдено. Возможно всё в порядке")
172
+ else:
173
+ for i in inputs:
174
+ try:
175
+ name = i.get("name")
176
+ if name:
177
+ result[name] = i.get("value", "")
178
+ except Exception as e:
179
+ logger.debug(f"При парсинге конкретного поля для ввода произошла ошибка: {e}")
180
+ textareas = soup.find_all("textarea")
181
+ if not textareas:
182
+ logger.debug("При парсинге редактора лотов не найдено ни одного текстового поля. Возможно всё в порядке")
183
+ else:
184
+ for t in textareas:
185
+ try:
186
+ name = t.get("name")
187
+ if name:
188
+ result[name] = t.get_text().strip()
189
+ except Exception as e:
190
+ logger.debug(f"При парсинге конкретного текстового поля произошла ошибка: {e}")
191
+ return result