cs2tracker 2.1.13__py3-none-any.whl → 2.1.15__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.
Potentially problematic release.
This version of cs2tracker might be problematic. Click here for more details.
- cs2tracker/_version.py +2 -2
- cs2tracker/app/__init__.py +0 -3
- cs2tracker/app/app.py +283 -0
- cs2tracker/app/editor_frame.py +354 -158
- cs2tracker/app/history_frame.py +61 -0
- cs2tracker/app/scraper_frame.py +34 -11
- cs2tracker/config.py +263 -0
- cs2tracker/constants.py +58 -321
- cs2tracker/data/config.ini +157 -154
- cs2tracker/data/convert_inventory.js +108 -28
- cs2tracker/data/get_inventory.js +96 -32
- cs2tracker/logs.py +143 -0
- cs2tracker/main.py +4 -19
- cs2tracker/scraper/__init__.py +0 -9
- cs2tracker/scraper/background_task.py +1 -1
- cs2tracker/scraper/discord_notifier.py +34 -32
- cs2tracker/scraper/parser.py +189 -0
- cs2tracker/scraper/scraper.py +166 -246
- cs2tracker/util/__init__.py +0 -9
- cs2tracker/util/currency_conversion.py +84 -0
- cs2tracker/util/padded_console.py +24 -0
- cs2tracker/util/tkinter.py +55 -0
- {cs2tracker-2.1.13.dist-info → cs2tracker-2.1.15.dist-info}/METADATA +6 -6
- cs2tracker-2.1.15.dist-info/RECORD +31 -0
- cs2tracker/app/application.py +0 -255
- cs2tracker/util/price_logs.py +0 -100
- cs2tracker/util/validated_config.py +0 -164
- cs2tracker-2.1.13.dist-info/RECORD +0 -27
- {cs2tracker-2.1.13.dist-info → cs2tracker-2.1.15.dist-info}/WHEEL +0 -0
- {cs2tracker-2.1.13.dist-info → cs2tracker-2.1.15.dist-info}/entry_points.txt +0 -0
- {cs2tracker-2.1.13.dist-info → cs2tracker-2.1.15.dist-info}/licenses/LICENSE +0 -0
- {cs2tracker-2.1.13.dist-info → cs2tracker-2.1.15.dist-info}/top_level.txt +0 -0
cs2tracker/scraper/scraper.py
CHANGED
|
@@ -1,21 +1,18 @@
|
|
|
1
1
|
import time
|
|
2
2
|
from datetime import datetime
|
|
3
|
-
from urllib.parse import unquote
|
|
4
3
|
|
|
5
|
-
from
|
|
6
|
-
from bs4.element import Tag
|
|
7
|
-
from currency_converter import CurrencyConverter
|
|
8
|
-
from requests import RequestException, Session
|
|
4
|
+
from requests import RequestException
|
|
9
5
|
from requests.adapters import HTTPAdapter, Retry
|
|
6
|
+
from requests_cache import CachedSession
|
|
10
7
|
from tenacity import RetryError, retry, stop_after_attempt
|
|
11
8
|
|
|
12
|
-
from cs2tracker.
|
|
9
|
+
from cs2tracker.config import get_config
|
|
10
|
+
from cs2tracker.constants import AUTHOR_STRING, BANNER
|
|
11
|
+
from cs2tracker.logs import PriceLogs
|
|
13
12
|
from cs2tracker.scraper.discord_notifier import DiscordNotifier
|
|
14
|
-
from cs2tracker.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
SEPARATOR = "-"
|
|
18
|
-
PRICE_INFO = "Owned: {:<10} Steam market price: ${:<10} Total: ${:<10}\n"
|
|
13
|
+
from cs2tracker.scraper.parser import Parser
|
|
14
|
+
from cs2tracker.util.currency_conversion import convert, to_symbol
|
|
15
|
+
from cs2tracker.util.padded_console import get_console
|
|
19
16
|
|
|
20
17
|
HTTP_PROXY_URL = "http://{}:@smartproxy.crawlbase.com:8012"
|
|
21
18
|
HTTPS_PROXY_URL = "http://{}:@smartproxy.crawlbase.com:8012"
|
|
@@ -29,6 +26,11 @@ class ConfigError:
|
|
|
29
26
|
self.message = "Invalid configuration. Please fix the config file before running."
|
|
30
27
|
|
|
31
28
|
|
|
29
|
+
class ParsingError:
|
|
30
|
+
def __init__(self, message):
|
|
31
|
+
self.message = message
|
|
32
|
+
|
|
33
|
+
|
|
32
34
|
class RequestLimitExceededError:
|
|
33
35
|
def __init__(self):
|
|
34
36
|
self.message = "Too many requests. Consider using proxies to prevent rate limiting."
|
|
@@ -44,17 +46,33 @@ class UnexpectedError:
|
|
|
44
46
|
self.message = f"An unexpected error occurred: {error}"
|
|
45
47
|
|
|
46
48
|
|
|
49
|
+
class SheetNotFoundError:
|
|
50
|
+
def __init__(self):
|
|
51
|
+
self.message = "Could not find sheet to update."
|
|
52
|
+
|
|
53
|
+
|
|
47
54
|
class Scraper:
|
|
48
55
|
def __init__(self):
|
|
49
56
|
"""Initialize the Scraper class."""
|
|
50
57
|
self._start_session()
|
|
51
58
|
self.error_stack = []
|
|
52
|
-
|
|
53
|
-
|
|
59
|
+
|
|
60
|
+
# We set the conversion currency as an attribute of the Scraper instance
|
|
61
|
+
# and only update it from the config at the start of the scraping process.
|
|
62
|
+
# This allows us to use the same conversion currency throughout the scraping
|
|
63
|
+
# process and prevents issues with changing the conversion currency while scraping.
|
|
64
|
+
self.conversion_currency = config.conversion_currency
|
|
65
|
+
self.totals = {
|
|
66
|
+
price_source: {
|
|
67
|
+
"USD": 0.0,
|
|
68
|
+
self.conversion_currency: 0.0,
|
|
69
|
+
}
|
|
70
|
+
for price_source in Parser.SOURCES
|
|
71
|
+
}
|
|
54
72
|
|
|
55
73
|
def _start_session(self):
|
|
56
74
|
"""Start a requests session with custom headers and retry logic."""
|
|
57
|
-
self.session =
|
|
75
|
+
self.session = CachedSession("scraper_cache", backend="memory")
|
|
58
76
|
self.session.headers.update(
|
|
59
77
|
{
|
|
60
78
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36"
|
|
@@ -64,79 +82,112 @@ class Scraper:
|
|
|
64
82
|
self.session.mount("http://", HTTPAdapter(max_retries=retries))
|
|
65
83
|
self.session.mount("https://", HTTPAdapter(max_retries=retries))
|
|
66
84
|
|
|
67
|
-
def
|
|
68
|
-
"""
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
85
|
+
def _error(self, error):
|
|
86
|
+
"""Add an error to the error stack and print the last error message from the
|
|
87
|
+
error stack.
|
|
88
|
+
"""
|
|
89
|
+
self.error_stack.append(error)
|
|
90
|
+
console.error(f"{error.message}")
|
|
91
|
+
|
|
92
|
+
def _prepare_new_run(self):
|
|
93
|
+
"""
|
|
94
|
+
Reset totals for the next run and get the most recent conversion currency from
|
|
95
|
+
the config.
|
|
96
|
+
|
|
97
|
+
This way, we don't have to create a new Scraper instance for each run.
|
|
98
|
+
"""
|
|
99
|
+
self.error_stack.clear()
|
|
100
|
+
self.conversion_currency = config.conversion_currency
|
|
101
|
+
self.totals = {
|
|
102
|
+
price_source: {
|
|
103
|
+
"USD": 0.0,
|
|
104
|
+
self.conversion_currency: 0.0,
|
|
105
|
+
}
|
|
106
|
+
for price_source in Parser.SOURCES
|
|
107
|
+
}
|
|
72
108
|
|
|
73
109
|
def scrape_prices(self, update_sheet_callback=None):
|
|
74
110
|
"""
|
|
75
|
-
Scrape prices for capsules and cases, calculate totals in USD and
|
|
76
|
-
print/save the results.
|
|
111
|
+
Scrape prices for capsules and cases, calculate totals in USD and conversion
|
|
112
|
+
currency, and print/save the results.
|
|
77
113
|
|
|
78
114
|
:param update_sheet_callback: Optional callback function to update a tksheet
|
|
79
115
|
that is displayed in the GUI with the latest scraper price calculation.
|
|
80
116
|
"""
|
|
81
117
|
if not config.valid:
|
|
82
|
-
self.
|
|
83
|
-
self._print_error()
|
|
118
|
+
self._error(ConfigError())
|
|
84
119
|
return
|
|
85
120
|
|
|
86
|
-
|
|
87
|
-
self.usd_total, self.eur_total = 0, 0
|
|
88
|
-
self.error_stack.clear()
|
|
121
|
+
self._prepare_new_run()
|
|
89
122
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
self.usd_total += capsule_usd_total
|
|
95
|
-
self.usd_total += case_usd_total
|
|
96
|
-
self.usd_total += custom_item_usd_total
|
|
97
|
-
self.eur_total = CurrencyConverter().convert(self.usd_total, "USD", "EUR")
|
|
98
|
-
|
|
99
|
-
if update_sheet_callback:
|
|
100
|
-
update_sheet_callback(["", "", "", ""])
|
|
101
|
-
update_sheet_callback(
|
|
102
|
-
[
|
|
103
|
-
f"[{datetime.now().strftime('%Y-%m-%d')}] Total:",
|
|
104
|
-
f"${self.usd_total:.2f}",
|
|
105
|
-
f"€{self.eur_total:.2f}",
|
|
106
|
-
"",
|
|
107
|
-
]
|
|
108
|
-
)
|
|
123
|
+
for section in config.sections():
|
|
124
|
+
if section in ("User Settings", "App Settings"):
|
|
125
|
+
continue
|
|
126
|
+
self._scrape_item_prices(section, update_sheet_callback)
|
|
109
127
|
|
|
110
|
-
self.
|
|
111
|
-
|
|
128
|
+
self._convert_totals()
|
|
129
|
+
self._print_totals(update_sheet_callback)
|
|
112
130
|
self._send_discord_notification()
|
|
113
131
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
132
|
+
usd_totals = [self.totals[price_source]["USD"] for price_source in Parser.SOURCES]
|
|
133
|
+
PriceLogs.save(usd_totals)
|
|
134
|
+
|
|
135
|
+
def _convert_totals(self):
|
|
117
136
|
"""
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
console.print(f"${self.usd_total:.2f}")
|
|
137
|
+
Convert the total prices from USD to the configured conversion currency and
|
|
138
|
+
update the totals dictionary.
|
|
121
139
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
140
|
+
with the converted totals.
|
|
141
|
+
"""
|
|
142
|
+
for price_source, totals in self.totals.items():
|
|
143
|
+
usd_total = totals["USD"]
|
|
144
|
+
converted_total = convert(usd_total, "USD", self.conversion_currency)
|
|
145
|
+
self.totals.update({price_source: {"USD": usd_total, self.conversion_currency: converted_total}}) # type: ignore
|
|
125
146
|
|
|
126
|
-
|
|
127
|
-
|
|
147
|
+
def _print_totals(self, update_sheet_callback=None):
|
|
148
|
+
"""
|
|
149
|
+
Print the total prices in USD and converted currency, formatted with titles and
|
|
150
|
+
separators.
|
|
151
|
+
|
|
152
|
+
:param update_sheet_callback: Optional callback function to update a tksheet
|
|
153
|
+
with the final totals.
|
|
154
|
+
"""
|
|
155
|
+
console.title("USD Total", "green")
|
|
156
|
+
for price_source, totals in self.totals.items():
|
|
157
|
+
usd_total = totals["USD"]
|
|
158
|
+
console.print(f"{price_source.name.title():<10}: ${usd_total:.2f}")
|
|
159
|
+
|
|
160
|
+
console.title(f"{self.conversion_currency} Total", "green")
|
|
161
|
+
for price_source, totals in self.totals.items():
|
|
162
|
+
converted_total = totals[self.conversion_currency]
|
|
163
|
+
console.print(
|
|
164
|
+
f"{price_source.name.title():<10}: {to_symbol(self.conversion_currency)}{converted_total:.2f}"
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
if update_sheet_callback and not (
|
|
168
|
+
self.error_stack and isinstance(self.error_stack[-1], SheetNotFoundError)
|
|
169
|
+
):
|
|
170
|
+
update_sheet_callback(["", ""] + ["", ""] * len(Parser.SOURCES))
|
|
171
|
+
for price_source, totals in self.totals.items():
|
|
172
|
+
usd_total = totals["USD"]
|
|
173
|
+
converted_total = totals[self.conversion_currency]
|
|
174
|
+
update_sheet_callback(
|
|
175
|
+
[
|
|
176
|
+
f"[{datetime.now().strftime('%Y-%m-%d')}] {price_source.name.title()} Total:",
|
|
177
|
+
f"${usd_total:.2f}",
|
|
178
|
+
f"{to_symbol(self.conversion_currency)}{converted_total:.2f}",
|
|
179
|
+
"",
|
|
180
|
+
]
|
|
181
|
+
)
|
|
128
182
|
|
|
129
183
|
def _send_discord_notification(self):
|
|
130
184
|
"""Send a message to a Discord webhook if notifications are enabled in the
|
|
131
185
|
config file and a webhook URL is provided.
|
|
132
186
|
"""
|
|
133
|
-
|
|
134
|
-
"App Settings", "discord_notifications", fallback=False
|
|
135
|
-
)
|
|
136
|
-
webhook_url = config.get("User Settings", "discord_webhook_url", fallback=None)
|
|
187
|
+
discord_webhook_url = config.discord_webhook_url
|
|
137
188
|
|
|
138
|
-
if discord_notifications and
|
|
139
|
-
DiscordNotifier.notify(
|
|
189
|
+
if config.discord_notifications and discord_webhook_url:
|
|
190
|
+
DiscordNotifier.notify(discord_webhook_url)
|
|
140
191
|
|
|
141
192
|
@retry(stop=stop_after_attempt(10))
|
|
142
193
|
def _get_page(self, url):
|
|
@@ -149,10 +200,9 @@ class Scraper:
|
|
|
149
200
|
:raises RequestException: If the request fails.
|
|
150
201
|
:raises RetryError: If the retry limit is reached.
|
|
151
202
|
"""
|
|
152
|
-
|
|
153
|
-
proxy_api_key = config.get("User Settings", "proxy_api_key", fallback=None)
|
|
203
|
+
proxy_api_key = config.proxy_api_key
|
|
154
204
|
|
|
155
|
-
if use_proxy and proxy_api_key:
|
|
205
|
+
if config.use_proxy and proxy_api_key:
|
|
156
206
|
page = self.session.get(
|
|
157
207
|
url=url,
|
|
158
208
|
proxies={
|
|
@@ -165,213 +215,83 @@ class Scraper:
|
|
|
165
215
|
page = self.session.get(url)
|
|
166
216
|
|
|
167
217
|
if not page.ok or not page.content:
|
|
168
|
-
self.
|
|
169
|
-
self._print_error()
|
|
218
|
+
self._error(PageLoadError(page.status_code))
|
|
170
219
|
raise RequestException(f"Failed to load page: {url}")
|
|
171
220
|
|
|
172
221
|
return page
|
|
173
222
|
|
|
174
|
-
def
|
|
175
|
-
"""
|
|
176
|
-
Print the title for a case, capsule, or custom item.
|
|
177
|
-
|
|
178
|
-
:param raw_item_str: The raw string to convert into an item name and title.
|
|
179
|
-
:param from_config: Whether the raw item string is from the config file.
|
|
180
|
-
:param from_href: Whether the raw item string is an href.
|
|
181
|
-
:return: The formatted item name.
|
|
182
|
-
"""
|
|
183
|
-
if from_config:
|
|
184
|
-
item_name = raw_item_str.replace("_", " ").title()
|
|
185
|
-
elif from_href:
|
|
186
|
-
item_name = unquote(raw_item_str.split("/")[-1])
|
|
187
|
-
else:
|
|
188
|
-
item_name = raw_item_str
|
|
189
|
-
|
|
190
|
-
item_title = item_name.center(MAX_LINE_LEN, SEPARATOR)
|
|
191
|
-
console.print(f"[bold magenta]{item_title}\n")
|
|
192
|
-
return item_name
|
|
193
|
-
|
|
194
|
-
def _parse_item_price(self, item_page, item_href):
|
|
195
|
-
"""
|
|
196
|
-
Parse the price of an item from the given steamcommunity market page and item
|
|
197
|
-
href.
|
|
198
|
-
|
|
199
|
-
:param item_page: The HTTP response object containing the item page content.
|
|
200
|
-
:param item_href: The href of the item listing to find the price for.
|
|
201
|
-
:return: The price of the item as a float.
|
|
202
|
-
:raises ValueError: If the item listing or price span cannot be found.
|
|
223
|
+
def _scrape_prices_from_all_sources(self, item_href, owned):
|
|
203
224
|
"""
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
if not isinstance(item_listing, Tag):
|
|
207
|
-
raise ValueError(f"Failed to find item listing: {item_href}")
|
|
208
|
-
|
|
209
|
-
item_price_span = item_listing.find("span", attrs={"class": "normal_price"})
|
|
210
|
-
if not isinstance(item_price_span, Tag):
|
|
211
|
-
raise ValueError(f"Failed to find price span in item listing: {item_href}")
|
|
212
|
-
|
|
213
|
-
price_str = item_price_span.text.split()[2]
|
|
214
|
-
price = float(price_str.replace("$", ""))
|
|
225
|
+
For a given item href and owned count, scrape the item's price from all sources
|
|
226
|
+
available to the currently registered parser.
|
|
215
227
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
:param capsule_section: The section name in the config for the capsule.
|
|
224
|
-
:param capsule_info: A dictionary containing information about the capsule page,
|
|
225
|
-
hrefs, and names.
|
|
226
|
-
:param update_sheet_callback: Optional callback function to update a tksheet
|
|
227
|
-
that is displayed in the GUI with the latest scraper price calculation.
|
|
228
|
-
"""
|
|
229
|
-
self._print_item_title(capsule_section)
|
|
230
|
-
capsule_usd_total = 0
|
|
231
|
-
try:
|
|
232
|
-
capsule_page = self._get_page(capsule_info["page"])
|
|
233
|
-
for capsule_href in capsule_info["items"]:
|
|
234
|
-
capsule_name = unquote(capsule_href.split("/")[-1])
|
|
235
|
-
config_capsule_name = capsule_name.replace(" ", "_").lower()
|
|
236
|
-
owned = config.getint(capsule_section, config_capsule_name, fallback=0)
|
|
237
|
-
if owned == 0:
|
|
238
|
-
continue
|
|
239
|
-
|
|
240
|
-
price_usd = self._parse_item_price(capsule_page, capsule_href)
|
|
241
|
-
price_usd_owned = round(float(owned * price_usd), 2)
|
|
242
|
-
|
|
243
|
-
console.print(f"[bold deep_sky_blue4]{capsule_name}")
|
|
244
|
-
console.print(PRICE_INFO.format(owned, price_usd, price_usd_owned))
|
|
245
|
-
capsule_usd_total += price_usd_owned
|
|
246
|
-
|
|
247
|
-
if update_sheet_callback:
|
|
248
|
-
update_sheet_callback([capsule_name, owned, price_usd, price_usd_owned])
|
|
249
|
-
except (RetryError, ValueError):
|
|
250
|
-
self.error_stack.append(RequestLimitExceededError())
|
|
251
|
-
self._print_error()
|
|
252
|
-
except Exception as error:
|
|
253
|
-
self.error_stack.append(UnexpectedError(error))
|
|
254
|
-
self._print_error()
|
|
255
|
-
|
|
256
|
-
return capsule_usd_total
|
|
257
|
-
|
|
258
|
-
def _scrape_capsule_section_prices(self, update_sheet_callback=None):
|
|
259
|
-
"""
|
|
260
|
-
Scrape prices for all capsule sections defined in the configuration.
|
|
261
|
-
|
|
262
|
-
:param update_sheet_callback: Optional callback function to update a tksheet
|
|
263
|
-
that is displayed in the GUI with the latest scraper price calculation.
|
|
228
|
+
:param item_href: The url of the steamcommunity market listing of the item
|
|
229
|
+
:param owned: How many of this item the user owns
|
|
230
|
+
:return: A list of item prices for the different sources
|
|
231
|
+
:raises RequestException: If the request fails.
|
|
232
|
+
:raises RetryError: If the retry limit is reached.
|
|
233
|
+
:raises ValueError: If the parser could not find the item
|
|
264
234
|
"""
|
|
265
|
-
|
|
266
|
-
for
|
|
267
|
-
|
|
268
|
-
|
|
235
|
+
prices = []
|
|
236
|
+
for price_source in Parser.SOURCES:
|
|
237
|
+
try:
|
|
238
|
+
item_page_url = Parser.get_item_page_url(item_href, price_source)
|
|
239
|
+
item_page = self._get_page(item_page_url)
|
|
240
|
+
price_usd = Parser.parse_item_price(item_page, item_href, price_source)
|
|
269
241
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
242
|
+
price_usd_owned = round(float(int(owned) * price_usd), 2)
|
|
243
|
+
self.totals[price_source]["USD"] += price_usd_owned
|
|
244
|
+
|
|
245
|
+
prices += [price_usd, price_usd_owned]
|
|
246
|
+
console.price(
|
|
247
|
+
Parser.PRICE_INFO,
|
|
248
|
+
owned,
|
|
249
|
+
price_source.name.title(),
|
|
250
|
+
price_usd,
|
|
251
|
+
price_usd_owned,
|
|
274
252
|
)
|
|
253
|
+
except ValueError as error:
|
|
254
|
+
prices += [0.0, 0.0]
|
|
255
|
+
self._error(ParsingError(error))
|
|
275
256
|
|
|
276
|
-
|
|
277
|
-
time.sleep(1)
|
|
278
|
-
|
|
279
|
-
return capsule_usd_total
|
|
257
|
+
return prices
|
|
280
258
|
|
|
281
|
-
def
|
|
259
|
+
def _scrape_item_prices(self, section, update_sheet_callback=None):
|
|
282
260
|
"""
|
|
283
|
-
|
|
261
|
+
Scrape prices for all items defined in a configuration section that uses hrefs
|
|
262
|
+
as option keys.
|
|
284
263
|
|
|
285
|
-
|
|
286
|
-
name.
|
|
287
|
-
:return: A URL string for the Steam Community Market page of the item.
|
|
288
|
-
"""
|
|
289
|
-
url_encoded_name = item_href.split("/")[-1]
|
|
290
|
-
page_url = f"https://steamcommunity.com/market/search?q={url_encoded_name}"
|
|
291
|
-
|
|
292
|
-
return page_url
|
|
293
|
-
|
|
294
|
-
def _scrape_case_prices(self, update_sheet_callback=None):
|
|
295
|
-
"""
|
|
296
|
-
Scrape prices for all cases defined in the configuration.
|
|
297
|
-
|
|
298
|
-
For each case, it prints the case name, owned count, price per item, and total
|
|
264
|
+
For each item, it prints the item name, owned count, price per item, and total
|
|
299
265
|
price for owned items.
|
|
300
266
|
|
|
301
267
|
:param update_sheet_callback: Optional callback function to update a tksheet
|
|
302
268
|
that is displayed in the GUI with the latest scraper price calculation.
|
|
303
269
|
"""
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
if int(owned) == 0:
|
|
309
|
-
continue
|
|
310
|
-
|
|
311
|
-
case_name = self._print_item_title(config_case_name, from_config=True)
|
|
312
|
-
try:
|
|
313
|
-
case_page_url = self._market_page_from_href(CASE_HREFS[case_index])
|
|
314
|
-
case_page = self._get_page(case_page_url)
|
|
315
|
-
price_usd = self._parse_item_price(case_page, CASE_HREFS[case_index])
|
|
316
|
-
price_usd_owned = round(float(int(owned) * price_usd), 2)
|
|
317
|
-
|
|
318
|
-
console.print(PRICE_INFO.format(owned, price_usd, price_usd_owned))
|
|
319
|
-
case_usd_total += price_usd_owned
|
|
320
|
-
|
|
321
|
-
if update_sheet_callback:
|
|
322
|
-
update_sheet_callback([case_name, owned, price_usd, price_usd_owned])
|
|
323
|
-
|
|
324
|
-
if not config.getboolean("App Settings", "use_proxy", fallback=False):
|
|
325
|
-
time.sleep(1)
|
|
326
|
-
except (RetryError, ValueError):
|
|
327
|
-
self.error_stack.append(RequestLimitExceededError())
|
|
328
|
-
self._print_error()
|
|
329
|
-
except Exception as error:
|
|
330
|
-
self.error_stack.append(UnexpectedError(error))
|
|
331
|
-
self._print_error()
|
|
332
|
-
|
|
333
|
-
return case_usd_total
|
|
334
|
-
|
|
335
|
-
def _scrape_custom_item_prices(self, update_sheet_callback=None):
|
|
336
|
-
"""
|
|
337
|
-
Scrape prices for custom items defined in the configuration.
|
|
338
|
-
|
|
339
|
-
For each custom item, it prints the item name, owned count, price per item, and
|
|
340
|
-
total price for owned items.
|
|
341
|
-
|
|
342
|
-
:param update_sheet_callback: Optional callback function to update a tksheet
|
|
343
|
-
that is displayed in the GUI with the latest scraper price calculation.
|
|
344
|
-
"""
|
|
345
|
-
custom_item_usd_total = 0
|
|
346
|
-
for custom_item_href, owned in config.items("Custom Items"):
|
|
347
|
-
if self.error_stack:
|
|
270
|
+
for item_href, owned in config.items(section):
|
|
271
|
+
if self.error_stack and isinstance(
|
|
272
|
+
self.error_stack[-1], (RequestLimitExceededError, SheetNotFoundError)
|
|
273
|
+
):
|
|
348
274
|
break
|
|
349
275
|
if int(owned) == 0:
|
|
350
276
|
continue
|
|
351
277
|
|
|
352
|
-
|
|
278
|
+
item_name = config.option_to_name(item_href, href=True)
|
|
279
|
+
console.title(item_name, "magenta")
|
|
353
280
|
try:
|
|
354
|
-
|
|
355
|
-
custom_item_page = self._get_page(custom_item_page_url)
|
|
356
|
-
price_usd = self._parse_item_price(custom_item_page, custom_item_href)
|
|
357
|
-
price_usd_owned = round(float(int(owned) * price_usd), 2)
|
|
358
|
-
|
|
359
|
-
console.print(PRICE_INFO.format(owned, price_usd, price_usd_owned))
|
|
360
|
-
custom_item_usd_total += price_usd_owned
|
|
281
|
+
prices = self._scrape_prices_from_all_sources(item_href, owned)
|
|
361
282
|
|
|
362
283
|
if update_sheet_callback:
|
|
363
|
-
|
|
284
|
+
try:
|
|
285
|
+
update_sheet_callback([item_name, owned] + prices)
|
|
286
|
+
except Exception:
|
|
287
|
+
self._error(SheetNotFoundError())
|
|
364
288
|
|
|
365
|
-
if not config.
|
|
289
|
+
if not config.use_proxy and Parser.NEEDS_TIMEOUT:
|
|
366
290
|
time.sleep(1)
|
|
367
|
-
except
|
|
368
|
-
self.
|
|
369
|
-
self._print_error()
|
|
291
|
+
except RetryError:
|
|
292
|
+
self._error(RequestLimitExceededError())
|
|
370
293
|
except Exception as error:
|
|
371
|
-
self.
|
|
372
|
-
self._print_error()
|
|
373
|
-
|
|
374
|
-
return custom_item_usd_total
|
|
294
|
+
self._error(UnexpectedError(error))
|
|
375
295
|
|
|
376
296
|
|
|
377
297
|
if __name__ == "__main__":
|
cs2tracker/util/__init__.py
CHANGED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
from cs2tracker.util.padded_console import ( # noqa: F401 # pylint:disable=unused-import
|
|
2
|
-
get_console,
|
|
3
|
-
)
|
|
4
|
-
from cs2tracker.util.price_logs import ( # noqa: F401 # pylint:disable=unused-import
|
|
5
|
-
PriceLogs,
|
|
6
|
-
)
|
|
7
|
-
from cs2tracker.util.validated_config import ( # noqa: F401 # pylint:disable=unused-import
|
|
8
|
-
get_config,
|
|
9
|
-
)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from currency_converter import CurrencyConverter
|
|
2
|
+
|
|
3
|
+
from cs2tracker.config import get_config
|
|
4
|
+
|
|
5
|
+
config = get_config()
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
converter = CurrencyConverter()
|
|
9
|
+
CURRENCY_SYMBOLS = {
|
|
10
|
+
"EUR": "€",
|
|
11
|
+
"KRW": "₩",
|
|
12
|
+
"ISK": "kr",
|
|
13
|
+
"HKD": "HK$",
|
|
14
|
+
"SKK": "Sk",
|
|
15
|
+
"SEK": "kr",
|
|
16
|
+
"NOK": "kr",
|
|
17
|
+
"HUF": "Ft",
|
|
18
|
+
"LTL": "Lt",
|
|
19
|
+
"ZAR": "R",
|
|
20
|
+
"PHP": "₱",
|
|
21
|
+
"GBP": "£",
|
|
22
|
+
"MXN": "$",
|
|
23
|
+
"CYP": "£",
|
|
24
|
+
"LVL": "Ls",
|
|
25
|
+
"DKK": "kr",
|
|
26
|
+
"NZD": "NZ$",
|
|
27
|
+
"TRY": "₺",
|
|
28
|
+
"USD": "$",
|
|
29
|
+
"RON": "lei",
|
|
30
|
+
"RUB": "₽",
|
|
31
|
+
"EEK": "kr",
|
|
32
|
+
"CHF": "CHF",
|
|
33
|
+
"MYR": "RM",
|
|
34
|
+
"ILS": "₪",
|
|
35
|
+
"PLN": "zł",
|
|
36
|
+
"BRL": "R$",
|
|
37
|
+
"BGN": "лв",
|
|
38
|
+
"THB": "฿",
|
|
39
|
+
"INR": "₹",
|
|
40
|
+
"ROL": "lei",
|
|
41
|
+
"AUD": "A$",
|
|
42
|
+
"CNY": "¥",
|
|
43
|
+
"HRK": "kn",
|
|
44
|
+
"MTL": "Lm",
|
|
45
|
+
"IDR": "Rp",
|
|
46
|
+
"JPY": "¥",
|
|
47
|
+
"CAD": "C$",
|
|
48
|
+
}
|
|
49
|
+
CURRENCY_SYMBOLS = {currency: symbol for currency, symbol in CURRENCY_SYMBOLS.items() if currency in converter.currencies} # type: ignore
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def convert(amount, source_currency, target_currency):
|
|
53
|
+
"""
|
|
54
|
+
Convert an amount from source currency to target currency.
|
|
55
|
+
|
|
56
|
+
:param amount: The amount to convert.
|
|
57
|
+
:param source_currency: The currency to convert from.
|
|
58
|
+
:param target_currency: The currency to convert to.
|
|
59
|
+
:return: The converted amount in the target currency.
|
|
60
|
+
"""
|
|
61
|
+
try:
|
|
62
|
+
if target_currency == "EUR":
|
|
63
|
+
converted_amount = converter.convert(amount, source_currency, target_currency)
|
|
64
|
+
else:
|
|
65
|
+
# The currency converter always needs the target or origin currency to be EUR
|
|
66
|
+
# Therefore we need an intermediate conversion step if the target currency is not EUR
|
|
67
|
+
intermediate_amount = converter.convert(amount, source_currency, "EUR")
|
|
68
|
+
converted_amount = converter.convert(intermediate_amount, "EUR", target_currency)
|
|
69
|
+
|
|
70
|
+
return round(converted_amount, 2)
|
|
71
|
+
except Exception:
|
|
72
|
+
return 0.0
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def to_symbol(currency):
|
|
76
|
+
"""
|
|
77
|
+
Convert a currency code to its symbol.
|
|
78
|
+
|
|
79
|
+
:param currency: The currency code to convert.
|
|
80
|
+
:return: The symbol of the currency.
|
|
81
|
+
"""
|
|
82
|
+
if currency in CURRENCY_SYMBOLS:
|
|
83
|
+
return CURRENCY_SYMBOLS[currency]
|
|
84
|
+
return currency
|
|
@@ -7,6 +7,10 @@ PADDING_LEFT = 4
|
|
|
7
7
|
PADDING_RIGHT = 0
|
|
8
8
|
|
|
9
9
|
|
|
10
|
+
MAX_LINE_LEN = 72
|
|
11
|
+
SEPARATOR = "-"
|
|
12
|
+
|
|
13
|
+
|
|
10
14
|
class PaddedConsole:
|
|
11
15
|
def __init__(self, padding=(PADDING_TOP, PADDING_RIGHT, PADDING_BOTTOM, PADDING_LEFT)):
|
|
12
16
|
"""Initialize a PaddedConsole with specified padding."""
|
|
@@ -17,11 +21,31 @@ class PaddedConsole:
|
|
|
17
21
|
"""Print text with padding to the console."""
|
|
18
22
|
self.console.print(Padding(text, self.padding))
|
|
19
23
|
|
|
24
|
+
def info(self, text):
|
|
25
|
+
"""Print info text with padding to the console."""
|
|
26
|
+
text = "[bold green][+] " + text
|
|
27
|
+
self.print(text)
|
|
28
|
+
|
|
20
29
|
def error(self, text):
|
|
21
30
|
"""Print error text with padding to the console."""
|
|
22
31
|
text = "[bold red][!] " + text
|
|
23
32
|
self.print(text)
|
|
24
33
|
|
|
34
|
+
def title(self, text, color):
|
|
35
|
+
"""Print the given text as a title."""
|
|
36
|
+
title = text.center(MAX_LINE_LEN, SEPARATOR)
|
|
37
|
+
console.print(f"\n[bold {color}]{title}\n")
|
|
38
|
+
|
|
39
|
+
def separator(self, color):
|
|
40
|
+
"""Print a separator line."""
|
|
41
|
+
separator = SEPARATOR * MAX_LINE_LEN
|
|
42
|
+
console.print(f"[bold {color}]{separator}")
|
|
43
|
+
|
|
44
|
+
def price(self, price_str, price_source, owned, steam_market_price, total_owned):
|
|
45
|
+
# pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
46
|
+
"""Print price information."""
|
|
47
|
+
console.print(price_str.format(price_source, owned, steam_market_price, total_owned))
|
|
48
|
+
|
|
25
49
|
def __getattr__(self, attr):
|
|
26
50
|
"""Ensure console methods can be called directly on PaddedConsole."""
|
|
27
51
|
return getattr(self.console, attr)
|