fpx-engine 0.1.0__py3-none-any.whl
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.
- fpx/__init__.py +6 -0
- fpx/api/client.py +134 -0
- fpx/api/parsers.py +373 -0
- fpx/classes/account/account.py +34 -0
- fpx/classes/account/subclasses/addons.py +20 -0
- fpx/classes/account/subclasses/category.py +39 -0
- fpx/classes/account/subclasses/chat.py +75 -0
- fpx/classes/account/subclasses/editor.py +66 -0
- fpx/classes/account/subclasses/lot.py +78 -0
- fpx/classes/account/subclasses/order.py +44 -0
- fpx/classes/account/subclasses/profile.py +103 -0
- fpx/classes/account/subclasses/review.py +52 -0
- fpx/classes/runner/runner.py +92 -0
- fpx/classes/runner/subclasses/category.py +54 -0
- fpx/classes/runner/subclasses/chat.py +49 -0
- fpx/classes/runner/subclasses/handler.py +13 -0
- fpx/classes/runner/subclasses/order.py +53 -0
- fpx/classes/runner/subclasses/review.py +30 -0
- fpx/classes/runner/subclasses/router.py +163 -0
- fpx/main.py +22 -0
- fpx/middlewares/request_engine.py +52 -0
- fpx/models/account.py +43 -0
- fpx/models/chat.py +33 -0
- fpx/models/lots.py +28 -0
- fpx/utils/errors.py +71 -0
- fpx_engine-0.1.0.dist-info/METADATA +72 -0
- fpx_engine-0.1.0.dist-info/RECORD +30 -0
- fpx_engine-0.1.0.dist-info/WHEEL +5 -0
- fpx_engine-0.1.0.dist-info/licenses/LICENSE +21 -0
- fpx_engine-0.1.0.dist-info/top_level.txt +1 -0
fpx/__init__.py
ADDED
fpx/api/client.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import urllib.parse
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class FunPayClient:
|
|
6
|
+
def __init__(self, account, http_client):
|
|
7
|
+
self._account = account
|
|
8
|
+
self.client = http_client
|
|
9
|
+
|
|
10
|
+
async def get_chats_page(self) -> str:
|
|
11
|
+
r = await self._account._request_engine.execute('GET', '/chat/')
|
|
12
|
+
return r.text
|
|
13
|
+
|
|
14
|
+
async def get_finance_page(self) -> str:
|
|
15
|
+
r = await self._account._request_engine.execute('GET', '/account/balance')
|
|
16
|
+
return r.text
|
|
17
|
+
|
|
18
|
+
async def send_message_request(self, node_name, last_msg, text):
|
|
19
|
+
request_data = {
|
|
20
|
+
"action": "chat_message",
|
|
21
|
+
"data": {
|
|
22
|
+
"node": node_name,
|
|
23
|
+
"last_message": last_msg,
|
|
24
|
+
"content": text
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
payload = {
|
|
28
|
+
'request': json.dumps(request_data)
|
|
29
|
+
}
|
|
30
|
+
headers = {
|
|
31
|
+
"X-Requested-With": "XMLHttpRequest",
|
|
32
|
+
"Referer": f"https://funpay.com/chat/?node={node_name.split('-')[-1]}"
|
|
33
|
+
}
|
|
34
|
+
r = await self._account._request_engine.execute('POST', '/runner/', data=payload, headers=headers)
|
|
35
|
+
return r.json()
|
|
36
|
+
|
|
37
|
+
async def get_current_chat(self, chat_id):
|
|
38
|
+
r = await self._account._request_engine.execute('GET', f'/chat/?node={chat_id}')
|
|
39
|
+
return r.text
|
|
40
|
+
|
|
41
|
+
async def get_user_profile(self, user_id):
|
|
42
|
+
r = await self._account._request_engine.execute('GET', f'/users/{user_id}/')
|
|
43
|
+
return r.text
|
|
44
|
+
|
|
45
|
+
async def lot_menu_by_category(self, category_id):
|
|
46
|
+
r = await self._account._request_engine.execute('GET', f'/lots/{category_id}/trade')
|
|
47
|
+
return r.text
|
|
48
|
+
|
|
49
|
+
async def get_main_menu(self):
|
|
50
|
+
r = await self._account._request_engine.execute('GET', '/')
|
|
51
|
+
return r.text
|
|
52
|
+
|
|
53
|
+
async def raise_lot(self, node_id, game_id):
|
|
54
|
+
payload = {
|
|
55
|
+
'game_id': game_id,
|
|
56
|
+
'node_id': node_id
|
|
57
|
+
}
|
|
58
|
+
headers = {
|
|
59
|
+
"X-Requested-With": "XMLHttpRequest"
|
|
60
|
+
}
|
|
61
|
+
r = await self._account._request_engine.execute('POST', '/lots/raise', data=payload, headers=headers)
|
|
62
|
+
if "application/json" in r.headers.get("Content-Type", ""):
|
|
63
|
+
response = r.json()
|
|
64
|
+
return response.get('msg')
|
|
65
|
+
else:
|
|
66
|
+
return {"error": "not_json", "status": r.status_code}
|
|
67
|
+
|
|
68
|
+
async def get_lot_info(self, lot_id):
|
|
69
|
+
r = await self._account._request_engine.execute('GET', f'/lots/offer?id={lot_id}')
|
|
70
|
+
return r.text
|
|
71
|
+
|
|
72
|
+
async def get_my_sells(self):
|
|
73
|
+
r = await self._account._request_engine.execute('GET', '/orders/trade')
|
|
74
|
+
return r.text
|
|
75
|
+
|
|
76
|
+
async def refund_order(self, csrf_token, order_id):
|
|
77
|
+
url = f'/orders/refund'
|
|
78
|
+
payload = {
|
|
79
|
+
'id': order_id
|
|
80
|
+
}
|
|
81
|
+
r = await self._account._request_engine.execute('POST', url, data=payload)
|
|
82
|
+
return r
|
|
83
|
+
|
|
84
|
+
async def get_order_info(self, order_id):
|
|
85
|
+
r = await self._account._request_engine.execute('GET', f'/orders/{order_id}/')
|
|
86
|
+
return r.text
|
|
87
|
+
|
|
88
|
+
async def get_lot_editor_data(self, lot_id):
|
|
89
|
+
r = await self._account._request_engine.execute('GET', f'/lots/offerEdit?offer={lot_id}')
|
|
90
|
+
return r.text
|
|
91
|
+
|
|
92
|
+
async def edit_lot(self, lot, active=None):
|
|
93
|
+
payload = {
|
|
94
|
+
'form_created_at': lot.form_created_at,
|
|
95
|
+
'offer_id': lot.offer_id,
|
|
96
|
+
'node_id': lot.node_id,
|
|
97
|
+
'location': lot.location if lot.location else 'offer',
|
|
98
|
+
'deleted': lot.deleted,
|
|
99
|
+
}
|
|
100
|
+
payload.update(lot.fields)
|
|
101
|
+
payload.pop('query', None)
|
|
102
|
+
if active is not None:
|
|
103
|
+
if active:
|
|
104
|
+
payload['active'] = 'on'
|
|
105
|
+
else:
|
|
106
|
+
payload.pop('active', None)
|
|
107
|
+
headers = {
|
|
108
|
+
'Accept': 'application/json, text/javascript, */*; q=0.01',
|
|
109
|
+
'X-Requested-With': 'XMLHttpRequest',
|
|
110
|
+
'Referer': f'https://funpay.com/lots/offerEdit?node={lot.node_id}&offer={lot.offer_id}&location=offer',
|
|
111
|
+
}
|
|
112
|
+
r = await self._account._request_engine.execute('POST', '/lots/offerSave', data=payload, headers=headers)
|
|
113
|
+
return r
|
|
114
|
+
|
|
115
|
+
async def answer_review(self, authorid: str, text: str, orderid: str):
|
|
116
|
+
payload = {
|
|
117
|
+
'authorId': authorid,
|
|
118
|
+
'text': text,
|
|
119
|
+
'rating': '',
|
|
120
|
+
'orderId': orderid
|
|
121
|
+
}
|
|
122
|
+
headers = {
|
|
123
|
+
"X-Requested-With": "XMLHttpRequest"
|
|
124
|
+
}
|
|
125
|
+
response = await self._account._request_engine.execute('POST', '/orders/review', data=payload, headers=headers)
|
|
126
|
+
return response
|
|
127
|
+
|
|
128
|
+
async def get_chip_category(self, chip_category_id):
|
|
129
|
+
r = await self._account._request_engine.execute('GET', f'/chips/{chip_category_id}/')
|
|
130
|
+
return r.text
|
|
131
|
+
|
|
132
|
+
async def get_lot_category(self, lot_category_id):
|
|
133
|
+
r = await self._account._request_engine.execute('GET', f'/lots/{lot_category_id}/')
|
|
134
|
+
return r.text
|
fpx/api/parsers.py
ADDED
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from bs4 import BeautifulSoup
|
|
6
|
+
|
|
7
|
+
from fpx.models.chat import Chat
|
|
8
|
+
from fpx.models.account import Balance
|
|
9
|
+
from fpx.utils import errors as fpx_err
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger("fpx.parser")
|
|
12
|
+
|
|
13
|
+
class FunPayParser:
|
|
14
|
+
@staticmethod
|
|
15
|
+
def parse_chats_list(html_content: str) -> list[Chat]:
|
|
16
|
+
soup = BeautifulSoup(html_content, 'html.parser')
|
|
17
|
+
items = soup.find_all('a', class_='contact-item')
|
|
18
|
+
if not items:
|
|
19
|
+
raise fpx_err.FpxNullDataError('На странице не найдено ни одного чата')
|
|
20
|
+
chats = []
|
|
21
|
+
for item in items:
|
|
22
|
+
try:
|
|
23
|
+
href = item.get('href', '')
|
|
24
|
+
node_msg_id = item.get('data-node-msg', '0')
|
|
25
|
+
chat_id = href.split('node=')[-1] if 'node=' in href else ''
|
|
26
|
+
username = item.find('div', class_='media-user-name').text.strip()
|
|
27
|
+
last_msg = item.find('div', class_='contact-item-message').text.strip()
|
|
28
|
+
date = item.find('div', class_='contact-item-time').text.strip()
|
|
29
|
+
is_unread = 'unread' in item.get('class', [])
|
|
30
|
+
chats.append(Chat(
|
|
31
|
+
id=chat_id, node_msg_id=int(node_msg_id), username=username, last_msg=last_msg,
|
|
32
|
+
date=date, link=href, is_unread=is_unread
|
|
33
|
+
))
|
|
34
|
+
except Exception as e:
|
|
35
|
+
logger.debug(f'Ошибка парсинга отдельного чата: {e}. Пропускаем элемент.')
|
|
36
|
+
continue
|
|
37
|
+
if not chats:
|
|
38
|
+
raise fpx_err.FpxParseError("Не удалось распарсить ни один чат, верстка полностью изменилась, или что-то сломалось.")
|
|
39
|
+
return chats
|
|
40
|
+
|
|
41
|
+
@staticmethod
|
|
42
|
+
def parse_finanses(html_content: str):
|
|
43
|
+
soup = BeautifulSoup(html_content, 'html.parser')
|
|
44
|
+
balances_container = soup.find('span', class_='balances-list')
|
|
45
|
+
if not balances_container:
|
|
46
|
+
raise fpx_err.FpxNullDataError('На странице финансов не найдено модуля баланса')
|
|
47
|
+
values = balances_container.find_all('span', class_='balances-value')
|
|
48
|
+
if not values:
|
|
49
|
+
raise fpx_err.FpxNullDataError('На странице финансов не найдено баланса')
|
|
50
|
+
clean_values = [v.text.strip() for v in values]
|
|
51
|
+
data = {}
|
|
52
|
+
for i in clean_values:
|
|
53
|
+
try:
|
|
54
|
+
value = i.replace('₽', '').replace('$', '').replace('€', '').replace(',', '.').strip()
|
|
55
|
+
num = float(value)
|
|
56
|
+
if '₽' in i: data['rub'] = num
|
|
57
|
+
elif '$' in i: data['usd'] = num
|
|
58
|
+
elif '€' in i: data['eur'] = num
|
|
59
|
+
except Exception as e:
|
|
60
|
+
logger.debug(f'Ошибка парсинга отдельной валюты: {e}. Пропускаем')
|
|
61
|
+
continue
|
|
62
|
+
if not data:
|
|
63
|
+
raise fpx_err.FpxParseError('Не удалось распарсить ни одну валюту, верстка полностью изменилась или что-то сломалось.')
|
|
64
|
+
return Balance(**data)
|
|
65
|
+
|
|
66
|
+
@staticmethod
|
|
67
|
+
def parse_chat(html_content: str):
|
|
68
|
+
soup = BeautifulSoup(html_content, 'html.parser')
|
|
69
|
+
result = {}
|
|
70
|
+
chat_div = soup.find('div', class_='chat')
|
|
71
|
+
body = soup.find('body')
|
|
72
|
+
if not chat_div or not body:
|
|
73
|
+
raise fpx_err.FpxNullDataError('На странице чата не найден блок переписки или тег body')
|
|
74
|
+
|
|
75
|
+
# парсинг чатов
|
|
76
|
+
chats = soup.find_all('div', class_='chat-msg-item chat-msg-with-head')
|
|
77
|
+
if chats:
|
|
78
|
+
try:
|
|
79
|
+
chat = chats[-1]
|
|
80
|
+
res = {}
|
|
81
|
+
res['is_system'] = False
|
|
82
|
+
res['message'] = chat.find('div', class_='chat-msg-text').get_text(separator='\n').strip()
|
|
83
|
+
author = chat.find('a', class_='chat-msg-author-link')
|
|
84
|
+
if not author:
|
|
85
|
+
res['sender'] = chat.find('span', class_='chat-msg-author-label')
|
|
86
|
+
if res['sender']:
|
|
87
|
+
res['sender'] = res['sender'].get_text(strip=True)
|
|
88
|
+
if res['sender'] == 'оповещение':
|
|
89
|
+
res['sender'] = 'FunPay'
|
|
90
|
+
res['is_system'] = True
|
|
91
|
+
else:
|
|
92
|
+
res['sender'] = author.get_text(strip=True)
|
|
93
|
+
result['last_message'] = res
|
|
94
|
+
except Exception as e:
|
|
95
|
+
logger.debug(f"Не удалось распарсить последнее сообщение в чате: {e}")
|
|
96
|
+
else:
|
|
97
|
+
result['last_message'] = None
|
|
98
|
+
|
|
99
|
+
# парсинг тех.данных
|
|
100
|
+
try:
|
|
101
|
+
result['data-name'] = chat_div.get('data-name', '')
|
|
102
|
+
app_data_str = body.get('data-app-data', '{}')
|
|
103
|
+
app_data = json.loads(app_data_str)
|
|
104
|
+
result['csrf-token'] = app_data.get('csrf-token', '')
|
|
105
|
+
result['user-id'] = app_data.get('userId', '')
|
|
106
|
+
except Exception as e:
|
|
107
|
+
logger.debug(f"Ошибка извлечения системных данных чата: {e}")
|
|
108
|
+
raise fpx_err.FpxParseError("Не удалось распарсить системные метаданные чата (CSRF/User ID)")
|
|
109
|
+
return result
|
|
110
|
+
|
|
111
|
+
@staticmethod
|
|
112
|
+
def parse_profile(html_content: str):
|
|
113
|
+
soup = BeautifulSoup(html_content, 'html.parser')
|
|
114
|
+
offer_list = soup.find_all('div', class_='offer')
|
|
115
|
+
review_list = soup.find_all('div', class_='review-compiled-review')
|
|
116
|
+
if not offer_list or not review_list:
|
|
117
|
+
logger.debug('На странице профиля не найден блок категорий или блок отзывов. Возможно ошибка или их просто не существует')
|
|
118
|
+
category_ids = set()
|
|
119
|
+
lots = []
|
|
120
|
+
for offer in offer_list:
|
|
121
|
+
links = offer.find_all('a', href=True)
|
|
122
|
+
if not links:
|
|
123
|
+
logger.debug('В блоке категории не найдено лотов. Возможно ошибка/Их просто не существует')
|
|
124
|
+
for link in links:
|
|
125
|
+
try:
|
|
126
|
+
href = link['href']
|
|
127
|
+
if '/lots/' in href and 'id=' not in href:
|
|
128
|
+
cat_id = href.strip('/').split('/')[-1]
|
|
129
|
+
if cat_id.isdigit():
|
|
130
|
+
category_ids.add(cat_id)
|
|
131
|
+
if 'id=' in href:
|
|
132
|
+
lot_id = href.split('id=')[-1].split('&')[0]
|
|
133
|
+
name_tag = link.find('div', class_='tc-desc-text')
|
|
134
|
+
name = name_tag.get_text(strip=True) if name_tag else "Unknown"
|
|
135
|
+
lots.append({'name': name, 'id': lot_id})
|
|
136
|
+
except Exception as e:
|
|
137
|
+
logger.debug(f'Ошибка парсинга отдельной категории: {e}')
|
|
138
|
+
continue
|
|
139
|
+
if not lots:
|
|
140
|
+
logger.debug('Не удалось распарсить ни один лот. Возможно изменилась вёрстка/у вас просто нет лотов.')
|
|
141
|
+
reviews = []
|
|
142
|
+
for review in review_list:
|
|
143
|
+
try:
|
|
144
|
+
rev = {}
|
|
145
|
+
rev['text'] = review.find('div', class_='review-item-text').get_text(strip=True)
|
|
146
|
+
rate_div = review.find('div', class_='rating').find('div', class_=True)
|
|
147
|
+
rating = rate_div['class'][0]
|
|
148
|
+
rev['stars'] = int(rating.replace('rating', ''))
|
|
149
|
+
rev['author'] = review.find('div', class_='media-user-name').get_text(strip=True)
|
|
150
|
+
rev['detail'] = review.find('div', class_='review-item-detail').get_text(strip=True)
|
|
151
|
+
reviews.append(rev)
|
|
152
|
+
except Exception as e:
|
|
153
|
+
logger.debug(f'При парсинге конкретного отзыва возникла ошибка: {e}')
|
|
154
|
+
continue
|
|
155
|
+
if not reviews:
|
|
156
|
+
logger.debug('Не удалось распарсить ни один отзыв, возможно их просто нет/возникла какая-то ошибка')
|
|
157
|
+
return {'category-ids': list(category_ids), 'lots': lots, 'reviews': reviews}
|
|
158
|
+
|
|
159
|
+
@staticmethod
|
|
160
|
+
def parse_lot_menu(html_content: str):
|
|
161
|
+
soup = BeautifulSoup(html_content, 'html.parser')
|
|
162
|
+
button = soup.find('button', class_='js-lot-raise')
|
|
163
|
+
if button:
|
|
164
|
+
data_game = button.get('data-game')
|
|
165
|
+
if data_game:
|
|
166
|
+
return data_game
|
|
167
|
+
raise fpx_err.FpxParseError('Атрибут data-game не найден внутри кнопки поднятия')
|
|
168
|
+
raise fpx_err.FpxNullDataError('На странице лотов не найдена кнопка для поднятия (возможно, у вас нет лотов)')
|
|
169
|
+
|
|
170
|
+
@staticmethod
|
|
171
|
+
def parse_main_menu(html_content: str):
|
|
172
|
+
soup = BeautifulSoup(html_content, 'html.parser')
|
|
173
|
+
user_link = soup.find('a', class_='user-link-dropdown')
|
|
174
|
+
result = {}
|
|
175
|
+
if user_link:
|
|
176
|
+
href = user_link.get('href', '')
|
|
177
|
+
user_id = href.strip('/').split('/')[-1]
|
|
178
|
+
if user_id.isdigit():
|
|
179
|
+
result['user-id'] = user_id
|
|
180
|
+
result['username'] = soup.find('div', class_='user-link-name').get_text(strip=True)
|
|
181
|
+
else:
|
|
182
|
+
raise fpx_err.FpxParseError('Не удалось извлечь цифровой ID юзера, возможно слетела сессия или изменилась вёрстка')
|
|
183
|
+
else:
|
|
184
|
+
raise fpx_err.FpxNullDataError('На главной странице не найдено информации о юзере. Возможно слетела сессия')
|
|
185
|
+
body = soup.find('body')
|
|
186
|
+
if not body:
|
|
187
|
+
raise fpx_err.FpxNullDataError('Тело главной страницы (body) не найдено')
|
|
188
|
+
try:
|
|
189
|
+
app_data_str = body.get('data-app-data', '{}')
|
|
190
|
+
app_data = json.loads(app_data_str)
|
|
191
|
+
result['csrf-token'] = app_data.get('csrf-token', '')
|
|
192
|
+
except Exception:
|
|
193
|
+
raise fpx_err.FpxParseError('Не удалось распарсить csrf_token из data-app-data.')
|
|
194
|
+
return result
|
|
195
|
+
|
|
196
|
+
@staticmethod
|
|
197
|
+
def parse_current_lot_menu(html_content):
|
|
198
|
+
result = {}
|
|
199
|
+
soup = BeautifulSoup(html_content, 'html.parser')
|
|
200
|
+
param_items = soup.find_all('div', class_='param-item')
|
|
201
|
+
if not param_items:
|
|
202
|
+
raise fpx_err.FpxNullDataError('Страница лота не найдена. Возможно, указана инвалидная ссылка или лот был удалён.')
|
|
203
|
+
else:
|
|
204
|
+
descriptions = {}
|
|
205
|
+
for item in param_items:
|
|
206
|
+
try:
|
|
207
|
+
header = item.find('h5').get_text(strip=True)
|
|
208
|
+
text = item.find('div').get_text(separator='\n', strip=True)
|
|
209
|
+
descriptions[header] = text
|
|
210
|
+
except Exception as e:
|
|
211
|
+
logger.debug(f'При парсинге конкретного объекта произошла ошибка: {e}')
|
|
212
|
+
result['short_desc'] = descriptions.get('Краткое описание')
|
|
213
|
+
result['description'] = descriptions.get('Подробное описание')
|
|
214
|
+
option = soup.find('option', value='21') or soup.find('option', attrs={'data-content': True})
|
|
215
|
+
if option:
|
|
216
|
+
inner_html = option.get('data-content')
|
|
217
|
+
if inner_html:
|
|
218
|
+
inner_soup = BeautifulSoup(inner_html, 'html.parser')
|
|
219
|
+
price_span = inner_soup.find('span', class_='payment-value')
|
|
220
|
+
if price_span:
|
|
221
|
+
raw_price = price_span.get_text(strip=True).replace(',', '.')
|
|
222
|
+
cleaned_price = "".join(c for c in raw_price if c.isdigit() or c == '.')
|
|
223
|
+
try:
|
|
224
|
+
result['price'] = float(cleaned_price) if cleaned_price else 0.0
|
|
225
|
+
except ValueError:
|
|
226
|
+
result['price'] = 0.0
|
|
227
|
+
return result
|
|
228
|
+
raise fpx_err.FpxNullDataError('Не удалось найти цену в скрытых атрибутах выбора оплаты.')
|
|
229
|
+
|
|
230
|
+
@staticmethod
|
|
231
|
+
def parse_my_sells(html_content):
|
|
232
|
+
result = []
|
|
233
|
+
soup = BeautifulSoup(html_content, 'html.parser')
|
|
234
|
+
tc_items = soup.find_all('a', class_='tc-item')
|
|
235
|
+
if not tc_items:
|
|
236
|
+
raise fpx_err.FpxNullDataError('На странице продаж не найдено объектов(tc-item)')
|
|
237
|
+
for item in tc_items:
|
|
238
|
+
try:
|
|
239
|
+
pre_result = {}
|
|
240
|
+
order_tag = item.find('div', class_='tc-order')
|
|
241
|
+
pre_result['order-id'] = order_tag.get_text(strip=True).replace('#', '') if order_tag else "Unknown"
|
|
242
|
+
time_tag = item.find('div', class_='tc-date-time')
|
|
243
|
+
pre_result['order-time'] = time_tag.get_text(strip=True) if time_tag else ""
|
|
244
|
+
status_tag = item.find('div', class_='tc-status')
|
|
245
|
+
pre_result['status'] = status_tag.get_text(strip=True) if status_tag else "Unknown"
|
|
246
|
+
client_tag = item.find('span', class_='pseudo-a') or item.find('div', class_='tc-user')
|
|
247
|
+
pre_result['client-name'] = client_tag.get_text(strip=True) if client_tag else "Unknown"
|
|
248
|
+
price_tag = item.find('div', class_='tc-price')
|
|
249
|
+
if price_tag:
|
|
250
|
+
raw_price = price_tag.get_text(strip=True).replace(',', '.')
|
|
251
|
+
cleaned_price = "".join(c for c in raw_price if c.isdigit() or c == '.')
|
|
252
|
+
pre_result['price'] = float(cleaned_price) if cleaned_price else 0.0
|
|
253
|
+
else:
|
|
254
|
+
pre_result['price'] = 0.0
|
|
255
|
+
order_desc = item.find('div', class_='order-desc')
|
|
256
|
+
if order_desc:
|
|
257
|
+
divs = order_desc.find_all('div')
|
|
258
|
+
pre_result['name'] = divs[0].get_text(strip=True) if len(divs) > 0 else "Unknown"
|
|
259
|
+
pre_result['category'] = divs[1].get_text(strip=True) if len(divs) > 1 else "Unknown"
|
|
260
|
+
else:
|
|
261
|
+
pre_result['name'] = "Unknown"
|
|
262
|
+
pre_result['category'] = "Unknown"
|
|
263
|
+
result.append(pre_result)
|
|
264
|
+
except Exception as e:
|
|
265
|
+
logger.debug(f'При парсинге конкретного объекта произошла ошибка: {e}')
|
|
266
|
+
continue
|
|
267
|
+
if not result:
|
|
268
|
+
raise fpx_err.FpxParseError('При парсинге не найдено ни одной продажи')
|
|
269
|
+
return result
|
|
270
|
+
|
|
271
|
+
@staticmethod
|
|
272
|
+
def parse_order_page(html_content):
|
|
273
|
+
soup = BeautifulSoup(html_content, 'html.parser')
|
|
274
|
+
result = {}
|
|
275
|
+
result['review'] = {}
|
|
276
|
+
try:
|
|
277
|
+
header = soup.find('h1', class_='page-header')
|
|
278
|
+
if not header:
|
|
279
|
+
raise fpx_err.FpxNullDataError('Страница заказа не найдена, возможно, указан неверный ID или слетела сессия.')
|
|
280
|
+
spans = header.find_all('span')
|
|
281
|
+
if not spans:
|
|
282
|
+
raise fpx_err.FpxParseError('Не удалось распарсить статус заказа из заголовка.')
|
|
283
|
+
result['status'] = " / ".join([span.get_text(strip=True) for span in spans])
|
|
284
|
+
review_container = soup.find('div', class_='review-container')
|
|
285
|
+
if review_container:
|
|
286
|
+
try:
|
|
287
|
+
text_tag = review_container.find('div', class_='review-item-text')
|
|
288
|
+
result['review']['text'] = text_tag.get_text(strip=True) if text_tag else ''
|
|
289
|
+
raw_stars = review_container.get('data-rating', '0')
|
|
290
|
+
result['review']['stars'] = int(raw_stars) if raw_stars.isdigit() else 0
|
|
291
|
+
answer_div = review_container.find('div', class_='review-item-answer')
|
|
292
|
+
if answer_div:
|
|
293
|
+
text_container = answer_div.find('div')
|
|
294
|
+
result['review']['answer'] = text_container.get_text('\n', strip=True) if text_container else ''
|
|
295
|
+
else:
|
|
296
|
+
result['review']['answer'] = ''
|
|
297
|
+
except Exception as e:
|
|
298
|
+
logger.debug(f'Ошибка парсинга деталей существующего отзыва: {e}')
|
|
299
|
+
result['review'].update({'text': '', 'stars': 0, 'answer': ''})
|
|
300
|
+
else:
|
|
301
|
+
result['review'] = {'text': '', 'stars': 0, 'answer': ''}
|
|
302
|
+
except fpx_err.FpxNullDataError:
|
|
303
|
+
raise
|
|
304
|
+
except fpx_err.FpxParseError:
|
|
305
|
+
raise
|
|
306
|
+
except Exception as e:
|
|
307
|
+
raise fpx_err.FpxParseError(f'Ошибка парсинга страницы заказа: {e}')
|
|
308
|
+
return result
|
|
309
|
+
|
|
310
|
+
@staticmethod
|
|
311
|
+
def parse_edit_lot_page(html_content):
|
|
312
|
+
soup = BeautifulSoup(html_content, 'html.parser')
|
|
313
|
+
result = {}
|
|
314
|
+
hidden_inputs = soup.find_all('input', type='hidden')
|
|
315
|
+
if not hidden_inputs:
|
|
316
|
+
raise fpx_err.FpxNullDataError('Не найдено вводных данных в редакторе лота. Возможно слетела сессия')
|
|
317
|
+
result = {tag.get('name'): tag.get('value', '') for tag in hidden_inputs if tag.get('name')}
|
|
318
|
+
selects = soup.find_all('select')
|
|
319
|
+
if not selects:
|
|
320
|
+
raise fpx_err.FpxNullDataError('Ни одна выборка в редакторе лотов не найдена. Проверьте актуальность сессии')
|
|
321
|
+
for s in selects:
|
|
322
|
+
try:
|
|
323
|
+
name = s.get('name')
|
|
324
|
+
if not name: continue
|
|
325
|
+
selected_option = s.find('option', selected=True)
|
|
326
|
+
if selected_option:
|
|
327
|
+
result[name] = selected_option.get('value', '')
|
|
328
|
+
else:
|
|
329
|
+
first_opt = s.find('option')
|
|
330
|
+
result[name] = first_opt.get('value', '') if first_opt else ''
|
|
331
|
+
except Exception as e:
|
|
332
|
+
logger.debug(f'При парсинге конкретной выборки произошла ошибка: {e}')
|
|
333
|
+
inputs = soup.find_all('input', class_='form-control')
|
|
334
|
+
if not inputs:
|
|
335
|
+
logger.debug('Ни одно поле для ввода в редакторе лотов не найдено. Возможно всё в порядке')
|
|
336
|
+
else:
|
|
337
|
+
for i in inputs:
|
|
338
|
+
try:
|
|
339
|
+
name = i.get('name')
|
|
340
|
+
if name:
|
|
341
|
+
result[name] = i.get('value', '')
|
|
342
|
+
except Exception as e:
|
|
343
|
+
logger.debug(f'При парсинге конкретного поля для ввода произошла ошибка: {e}')
|
|
344
|
+
textareas = soup.find_all('textarea')
|
|
345
|
+
if not textareas:
|
|
346
|
+
logger.debug('При парсинге редактора лотов не найдено ни одного текстового поля. Возможно всё в порядке')
|
|
347
|
+
else:
|
|
348
|
+
for t in textareas:
|
|
349
|
+
try:
|
|
350
|
+
name = t.get('name')
|
|
351
|
+
if name:
|
|
352
|
+
result[name] = t.string if t.string else ''
|
|
353
|
+
except Exception as e:
|
|
354
|
+
logger.debug(f'При парсинге конкретного текстового поля произошла ошибка: {e}')
|
|
355
|
+
return result
|
|
356
|
+
|
|
357
|
+
@staticmethod
|
|
358
|
+
def parse_category_page(html_content):
|
|
359
|
+
soup = BeautifulSoup(html_content, 'html.parser')
|
|
360
|
+
lots = soup.select('a.tc-item:not(.offer-promo)')
|
|
361
|
+
if not lots:
|
|
362
|
+
logger.debug('В категории не найдено лотов, возможно их нет или сайт недоступен')
|
|
363
|
+
result = {}
|
|
364
|
+
try:
|
|
365
|
+
lot = lots[0]
|
|
366
|
+
text_price = lot.find('div', class_='tc-price').get_text(strip=True)
|
|
367
|
+
result['price'] = float(''.join(c for c in text_price if c.isdigit() or c in '.,').replace(',', '.'))
|
|
368
|
+
result['offer_id'] = lot.get('href', '').split('=')[-1]
|
|
369
|
+
except Exception as e:
|
|
370
|
+
raise fpx_err.FpxParseError('Ошибка при парсинге категории')
|
|
371
|
+
if not result:
|
|
372
|
+
raise fpx_err.FpxParseError('У лота не найдено параметров')
|
|
373
|
+
return result
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
|
|
2
|
+
from fpx.api.client import FunPayClient
|
|
3
|
+
from fpx.api.parsers import FunPayParser
|
|
4
|
+
from fpx.classes.account.subclasses.chat import ChatManager
|
|
5
|
+
from fpx.classes.account.subclasses.addons import AddonsManager
|
|
6
|
+
from fpx.classes.account.subclasses.profile import ProfileManager
|
|
7
|
+
from fpx.classes.account.subclasses.order import OrderManager
|
|
8
|
+
from fpx.classes.account.subclasses.lot import LotManager
|
|
9
|
+
from fpx.classes.account.subclasses.editor import FunPayEditor
|
|
10
|
+
from fpx.classes.account.subclasses.review import ReviewManager
|
|
11
|
+
from fpx.classes.account.subclasses.category import CategoryManager
|
|
12
|
+
from fpx.middlewares.request_engine import RequestEngine
|
|
13
|
+
|
|
14
|
+
class Account:
|
|
15
|
+
'''
|
|
16
|
+
Взаимодействует с аккаунтом.
|
|
17
|
+
'''
|
|
18
|
+
def __init__(self, client):
|
|
19
|
+
self.http_client = client
|
|
20
|
+
self.client = FunPayClient(self, self.http_client)
|
|
21
|
+
self._request_engine = RequestEngine(self, self.http_client)
|
|
22
|
+
self.parser = FunPayParser()
|
|
23
|
+
self.username = None
|
|
24
|
+
self.user_id = None
|
|
25
|
+
self._csrf_token = None
|
|
26
|
+
self._node_names = {}
|
|
27
|
+
self.chat = ChatManager(self)
|
|
28
|
+
self.addons = AddonsManager(self)
|
|
29
|
+
self.profile = ProfileManager(self)
|
|
30
|
+
self.order = OrderManager(self)
|
|
31
|
+
self.lot = LotManager(self)
|
|
32
|
+
self.editor = FunPayEditor(self)
|
|
33
|
+
self.review = ReviewManager(self)
|
|
34
|
+
self.category = CategoryManager(self)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class AddonsManager:
|
|
5
|
+
def __init__(self, account):
|
|
6
|
+
self.account = account
|
|
7
|
+
|
|
8
|
+
async def get_game_id(self, category_id: str):
|
|
9
|
+
"""
|
|
10
|
+
Получает game_id.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
category_id (str | int): ID подкатегории.
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
str | int: ID игры.
|
|
17
|
+
"""
|
|
18
|
+
html = await self.account.client.lot_menu_by_category(category_id)
|
|
19
|
+
data = self.account.parser.parse_lot_menu(html)
|
|
20
|
+
return data
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
from fpx.models.lots import CategoryLastLot
|
|
4
|
+
|
|
5
|
+
class CategoryManager:
|
|
6
|
+
def __init__(self, account):
|
|
7
|
+
self._account = account
|
|
8
|
+
|
|
9
|
+
async def get_lot_category_last_lot(self, lot_category_id):
|
|
10
|
+
'''
|
|
11
|
+
Находит самый дешевый лот в категории.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
lot_category_id (int | str): ID категории лота
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
CategoryLastLot: Объект, содержащий в себе:
|
|
18
|
+
- price (float): Цена лота
|
|
19
|
+
- offer_id (str): ID лота
|
|
20
|
+
'''
|
|
21
|
+
html = await self._account.client.get_lot_category(lot_category_id)
|
|
22
|
+
data = self._account.parser.parse_category_page(html)
|
|
23
|
+
return CategoryLastLot(**data)
|
|
24
|
+
|
|
25
|
+
async def get_chip_category_last_lot(self, chip_category_id):
|
|
26
|
+
'''
|
|
27
|
+
Находит самый дешевый лот в категории.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
lot_category_id (int | str): ID категории лота
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
CategoryLastLot: Объект, содержащий в себе:
|
|
34
|
+
- price (float): Цена лота
|
|
35
|
+
- offer_id (str): ID лота
|
|
36
|
+
'''
|
|
37
|
+
html = await self._account.client.get_chip_category(chip_category_id)
|
|
38
|
+
data = self._account.parser.parse_category_page(html)
|
|
39
|
+
return CategoryLastLot(**data)
|