Skip to content

Commit 5908356

Browse files
authored
Orders parser (funpayhub#21)
1 parent 9a633ec commit 5908356

5 files changed

Lines changed: 164 additions & 9 deletions

File tree

funpayparsers/parsers/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
from .chat_preview_parser import *
33
from .messages_parser import *
44
from .badge_parser import *
5+
from .order_previews_parser import *
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
__all__ = ('OrderPreviewsParserOptions', 'OrderPreviewsParser', )
2+
3+
from dataclasses import dataclass
4+
5+
from funpayparsers.parsers.base import FunPayObjectParserOptions, FunPayObjectParser
6+
from funpayparsers.parsers.utils import extract_css_url
7+
from funpayparsers.types.orders import OrderPreview, OrderCounterpartyInfo
8+
from funpayparsers.types.enums import OrderStatus
9+
from funpayparsers.types.common import MoneyValue
10+
11+
from lxml import html
12+
13+
14+
@dataclass(frozen=True)
15+
class OrderPreviewsParserOptions(FunPayObjectParserOptions):
16+
...
17+
18+
19+
class OrderPreviewsParser(FunPayObjectParser[
20+
list[OrderPreview],
21+
OrderPreviewsParserOptions
22+
]):
23+
options_cls: OrderPreviewsParserOptions = OrderPreviewsParserOptions
24+
25+
def _parse(self):
26+
result = []
27+
28+
for o in self.tree.xpath('//a[contains(@class, "tc-item")]'):
29+
order_id: str = o.get('href').split('/')[-2]
30+
date: str = o.xpath('string(.//div[@class="tc-date-time"][1])')
31+
32+
desc = o.xpath('string(.//div[@class="order-desc"][1]/div[1])')
33+
category_desc = o.xpath('string(.//div[@class="text-muted"][1])')
34+
35+
status_class = o.xpath('string(.//div[contains(@class, "tc-status")][1]/@class)')
36+
status = OrderStatus.get_by_css_class(status_class)
37+
38+
val_str = o.xpath('string(.//div[contains(@class, "tc-price")])').strip()
39+
val, char = val_str.split()
40+
value = MoneyValue(raw_source=val_str, value=float(val), character=char)
41+
42+
user_tag = o.xpath('.//div[contains(@class, "media-user")][1]')[0]
43+
photo_style = o.xpath(
44+
'string(.//div[contains(@class, "avatar-photo")]/@style)')
45+
nickname_tag = user_tag.xpath('.//div[@class="media-user-name"]/span[1]')[0]
46+
user_nickname = nickname_tag.text
47+
user_id = int(nickname_tag.get('data-href').split('/')[-2])
48+
online = 'online' in user_tag.get('class')
49+
banned = 'banned' in user_tag.get('class')
50+
user_status_text: str = user_tag.xpath(
51+
'string(.//div[contains(@class, "media-user-status")][1])'
52+
)
53+
photo_url = extract_css_url(photo_style)
54+
55+
counterparty = OrderCounterpartyInfo(
56+
raw_source=html.tostring(user_tag, encoding='unicode'),
57+
id=user_id,
58+
username=user_nickname,
59+
online=online,
60+
avatar_url=photo_url,
61+
banned=banned,
62+
status_text=user_status_text
63+
)
64+
65+
order_obj = OrderPreview(
66+
raw_source=html.tostring(o, encoding='unicode'),
67+
id=order_id,
68+
date_text=date,
69+
desc=desc,
70+
category_text=category_desc,
71+
status=status,
72+
amount=value,
73+
counterparty=counterparty
74+
)
75+
result.append(order_obj)
76+
77+
return result

funpayparsers/types/common.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,16 @@ class MoneyValue(FunPayObject):
2121
value: int | float
2222
"""The numeric amount of the monetary value."""
2323

24-
currency: Currency
25-
"""The currency of the value."""
26-
2724
character: str
2825
"""The currency character, e.g., $, €, ₽, ¤, etc."""
2926

27+
@property
28+
def currency(self) -> Currency:
29+
return Currency.get_by_character(self.character)
30+
31+
def __str__(self):
32+
return f'<MoneyValue: {self.value} {self.currency.name}>'
33+
3034

3135
@dataclass
3236
class UserBadge(FunPayObject):

funpayparsers/types/orders.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,18 @@ class OrderCounterpartyInfo(FunPayObject):
2626
online: bool
2727
"""True, if counterparty is online."""
2828

29-
blocked: bool
30-
"""True, if counterparty is blocked."""
29+
banned: bool
30+
"""True, if counterparty is banned."""
3131

32-
last_online_text: str | None
33-
"""Last online status text (if available)."""
32+
status_text: str
33+
"""Status text (online / banned / last seen online)."""
3434

3535
avatar_url: str
3636
"""Counterpart avatar URL."""
3737

38+
def __str__(self):
39+
return f'{self.username} (id: {self.id}), status: {self.status_text}.'
40+
3841

3942
@dataclass
4043
class OrderPreview(FunPayObject):
@@ -49,8 +52,8 @@ class OrderPreview(FunPayObject):
4952
date_text: str
5053
"""Order date (as human-readable text)."""
5154

52-
desc: str | None
53-
"""Order description (if available)."""
55+
desc: str
56+
"""Order description."""
5457

5558
category_text: str
5659
"""Order category and subcategory text."""
@@ -63,3 +66,7 @@ class OrderPreview(FunPayObject):
6366

6467
counterparty: OrderCounterpartyInfo
6568
"""Associated counterparty info."""
69+
70+
def __str__(self):
71+
return (f'<{self.amount} {self.status.name} order {self.id} '
72+
f'dated {self.date_text}: {self.desc}. ({self.category_text})>')
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
from funpayparsers.parsers.order_previews_parser import (OrderPreviewsParser,
2+
OrderPreviewsParserOptions)
3+
from funpayparsers.types.orders import OrderPreview, OrderCounterpartyInfo
4+
from funpayparsers.types.enums import OrderStatus
5+
from funpayparsers.types.common import MoneyValue
6+
7+
8+
OPTIONS = OrderPreviewsParserOptions(empty_raw_source=True)
9+
10+
11+
refunded_order_html = """
12+
<a href="https://funpay.com/orders/ABCDEFGH/" class="tc-item warning">
13+
<div class="tc-date">
14+
<div class="tc-date-time">вчера, 13:33</div>
15+
<div class="tc-date-left">22 часа назад</div>
16+
</div>
17+
<div class="tc-order">#ABCDEFGH</div>
18+
<div class="order-desc">
19+
<div>Order Description</div>
20+
<div class="text-muted">Category, Subcategory</div>
21+
</div>
22+
<div class="tc-user">
23+
<div class="media media-user offline">
24+
<div class="media-left">
25+
<div class="avatar-photo pseudo-a" tabindex="0" data-href="https://funpay.com/users/123456/" style="background-image: url(path/to/avatar);"></div>
26+
</div>
27+
<div class="media-body">
28+
<div class="media-user-name">
29+
<span class="pseudo-a" tabindex="0" data-href="https://funpay.com/users/123456/">Counterparty username</span>
30+
</div>
31+
<div class="media-user-status">Counterparty Status</div>
32+
</div>
33+
</div>
34+
</div>
35+
<div class="tc-status text-warning">Возврат</div>
36+
<div class="tc-price text-nowrap tc-buyer-sum">25.12 <span class="unit">₽</span></div>
37+
</a>
38+
"""
39+
40+
refunded_order_obj = OrderPreview(
41+
raw_source='',
42+
id='ABCDEFGH',
43+
date_text='вчера, 13:33',
44+
desc='Order Description',
45+
category_text='Category, Subcategory',
46+
status=OrderStatus.REFUNDED,
47+
amount=MoneyValue(
48+
raw_source='',
49+
value=25.12,
50+
character='₽'
51+
),
52+
counterparty=OrderCounterpartyInfo(
53+
raw_source='',
54+
id=123456,
55+
username='Counterparty username',
56+
online=False,
57+
banned=False,
58+
status_text='Counterparty Status',
59+
avatar_url='path/to/avatar'
60+
)
61+
)
62+
63+
64+
def test_refunded_order_parsing():
65+
parser = OrderPreviewsParser(refunded_order_html, options=OPTIONS)
66+
assert parser.parse() == [refunded_order_obj]

0 commit comments

Comments
 (0)