cs2tracker 2.1.12__py3-none-any.whl → 2.1.14__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/application.py +84 -88
- cs2tracker/app/editor_frame.py +451 -143
- cs2tracker/app/scraper_frame.py +43 -9
- cs2tracker/constants.py +66 -451
- cs2tracker/data/config.ini +153 -153
- cs2tracker/data/convert_inventory.js +187 -0
- cs2tracker/data/get_inventory.js +191 -0
- cs2tracker/main.py +2 -2
- cs2tracker/scraper/background_task.py +4 -4
- cs2tracker/scraper/discord_notifier.py +4 -4
- cs2tracker/scraper/parsers.py +192 -0
- cs2tracker/scraper/scraper.py +146 -224
- cs2tracker/util/__init__.py +2 -2
- cs2tracker/util/padded_console.py +32 -0
- cs2tracker/util/validated_config.py +117 -16
- cs2tracker-2.1.14.dist-info/METADATA +148 -0
- cs2tracker-2.1.14.dist-info/RECORD +28 -0
- cs2tracker-2.1.14.dist-info/licenses/LICENSE +402 -0
- cs2tracker-2.1.12.dist-info/METADATA +0 -82
- cs2tracker-2.1.12.dist-info/RECORD +0 -25
- cs2tracker-2.1.12.dist-info/licenses/LICENSE.md +0 -21
- {cs2tracker-2.1.12.dist-info → cs2tracker-2.1.14.dist-info}/WHEEL +0 -0
- {cs2tracker-2.1.12.dist-info → cs2tracker-2.1.14.dist-info}/entry_points.txt +0 -0
- {cs2tracker-2.1.12.dist-info → cs2tracker-2.1.14.dist-info}/top_level.txt +0 -0
cs2tracker/scraper/scraper.py
CHANGED
|
@@ -1,44 +1,63 @@
|
|
|
1
1
|
import time
|
|
2
2
|
from datetime import datetime
|
|
3
|
-
from urllib.parse import unquote
|
|
4
3
|
|
|
5
|
-
from bs4 import BeautifulSoup
|
|
6
|
-
from bs4.element import Tag
|
|
7
4
|
from currency_converter import CurrencyConverter
|
|
8
|
-
from requests import RequestException
|
|
5
|
+
from requests import RequestException
|
|
9
6
|
from requests.adapters import HTTPAdapter, Retry
|
|
7
|
+
from requests_cache import CachedSession
|
|
10
8
|
from tenacity import RetryError, retry, stop_after_attempt
|
|
11
9
|
|
|
12
|
-
from cs2tracker.constants import AUTHOR_STRING, BANNER
|
|
10
|
+
from cs2tracker.constants import AUTHOR_STRING, BANNER
|
|
13
11
|
from cs2tracker.scraper.discord_notifier import DiscordNotifier
|
|
14
|
-
from cs2tracker.
|
|
15
|
-
|
|
16
|
-
MAX_LINE_LEN = 72
|
|
17
|
-
SEPARATOR = "-"
|
|
18
|
-
PRICE_INFO = "Owned: {:<10} Steam market price: ${:<10} Total: ${:<10}\n"
|
|
12
|
+
from cs2tracker.scraper.parsers import CSGOTrader, PriceSource
|
|
13
|
+
from cs2tracker.util import PriceLogs, get_config, get_console
|
|
19
14
|
|
|
20
15
|
HTTP_PROXY_URL = "http://{}:@smartproxy.crawlbase.com:8012"
|
|
21
16
|
HTTPS_PROXY_URL = "http://{}:@smartproxy.crawlbase.com:8012"
|
|
22
17
|
|
|
23
|
-
console =
|
|
18
|
+
console = get_console()
|
|
19
|
+
config = get_config()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ConfigError:
|
|
23
|
+
def __init__(self):
|
|
24
|
+
self.message = "Invalid configuration. Please fix the config file before running."
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ParsingError:
|
|
28
|
+
def __init__(self, message):
|
|
29
|
+
self.message = message
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class RequestLimitExceededError:
|
|
33
|
+
def __init__(self):
|
|
34
|
+
self.message = "Too many requests. Consider using proxies to prevent rate limiting."
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class PageLoadError:
|
|
38
|
+
def __init__(self, status_code):
|
|
39
|
+
self.message = f"Failed to load page: {status_code}. Retrying..."
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class UnexpectedError:
|
|
43
|
+
def __init__(self, error):
|
|
44
|
+
self.message = f"An unexpected error occurred: {error}"
|
|
24
45
|
|
|
25
46
|
|
|
26
47
|
class Scraper:
|
|
27
48
|
def __init__(self):
|
|
28
49
|
"""Initialize the Scraper class."""
|
|
29
|
-
self.load_config()
|
|
30
50
|
self._start_session()
|
|
51
|
+
self._add_parser(CSGOTrader)
|
|
31
52
|
|
|
32
|
-
self.
|
|
33
|
-
self.
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
"""Load the configuration file and validate its contents."""
|
|
37
|
-
self.config = ValidatedConfig()
|
|
53
|
+
self.error_stack = []
|
|
54
|
+
self.totals = {
|
|
55
|
+
price_source: {"usd": 0.0, "eur": 0.0} for price_source in self.parser.SOURCES
|
|
56
|
+
}
|
|
38
57
|
|
|
39
58
|
def _start_session(self):
|
|
40
59
|
"""Start a requests session with custom headers and retry logic."""
|
|
41
|
-
self.session =
|
|
60
|
+
self.session = CachedSession("scraper_cache", backend="memory")
|
|
42
61
|
self.session.headers.update(
|
|
43
62
|
{
|
|
44
63
|
"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"
|
|
@@ -48,6 +67,16 @@ class Scraper:
|
|
|
48
67
|
self.session.mount("http://", HTTPAdapter(max_retries=retries))
|
|
49
68
|
self.session.mount("https://", HTTPAdapter(max_retries=retries))
|
|
50
69
|
|
|
70
|
+
def _add_parser(self, parser):
|
|
71
|
+
"""Add a parser for a specific page where item prices should be scraped."""
|
|
72
|
+
self.parser = parser
|
|
73
|
+
|
|
74
|
+
def _print_error(self):
|
|
75
|
+
"""Print the last error message from the error stack, if any."""
|
|
76
|
+
last_error = self.error_stack[-1] if self.error_stack else None
|
|
77
|
+
if last_error:
|
|
78
|
+
console.error(f"{last_error.message}")
|
|
79
|
+
|
|
51
80
|
def scrape_prices(self, update_sheet_callback=None):
|
|
52
81
|
"""
|
|
53
82
|
Scrape prices for capsules and cases, calculate totals in USD and EUR, and
|
|
@@ -56,62 +85,73 @@ class Scraper:
|
|
|
56
85
|
:param update_sheet_callback: Optional callback function to update a tksheet
|
|
57
86
|
that is displayed in the GUI with the latest scraper price calculation.
|
|
58
87
|
"""
|
|
59
|
-
if not
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
)
|
|
88
|
+
if not config.valid:
|
|
89
|
+
self.error_stack.append(ConfigError())
|
|
90
|
+
self._print_error()
|
|
63
91
|
return
|
|
64
92
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
93
|
+
# Reset totals from the previous run and clear the error stack
|
|
94
|
+
self.error_stack.clear()
|
|
95
|
+
self.totals = {
|
|
96
|
+
price_source: {"usd": 0.0, "eur": 0.0} for price_source in self.parser.SOURCES
|
|
97
|
+
}
|
|
68
98
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
99
|
+
for section in config.sections():
|
|
100
|
+
if section in ("User Settings", "App Settings"):
|
|
101
|
+
continue
|
|
102
|
+
self._scrape_item_prices(section, update_sheet_callback)
|
|
103
|
+
|
|
104
|
+
for price_source, totals in self.totals.items():
|
|
105
|
+
usd_total = totals["usd"]
|
|
106
|
+
eur_total = CurrencyConverter().convert(usd_total, "USD", "EUR")
|
|
107
|
+
self.totals.update({price_source: {"usd": usd_total, "eur": eur_total}}) # type: ignore
|
|
73
108
|
|
|
74
109
|
if update_sheet_callback:
|
|
75
|
-
update_sheet_callback(["", ""
|
|
76
|
-
|
|
77
|
-
[
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
110
|
+
update_sheet_callback(["", ""] + ["", ""] * len(self.parser.SOURCES))
|
|
111
|
+
for price_source, totals in self.totals.items():
|
|
112
|
+
usd_total = totals["usd"]
|
|
113
|
+
eur_total = totals["eur"]
|
|
114
|
+
update_sheet_callback(
|
|
115
|
+
[
|
|
116
|
+
f"[{datetime.now().strftime('%Y-%m-%d')}] {price_source.value.title()} Total:",
|
|
117
|
+
f"${usd_total:.2f}",
|
|
118
|
+
f"€{eur_total:.2f}",
|
|
119
|
+
"",
|
|
120
|
+
]
|
|
121
|
+
)
|
|
84
122
|
|
|
85
123
|
self._print_total()
|
|
86
|
-
PriceLogs.save(self.usd_total, self.eur_total)
|
|
87
124
|
self._send_discord_notification()
|
|
88
125
|
|
|
89
|
-
#
|
|
90
|
-
|
|
126
|
+
# TODO: modify price logs, charts etc for multiple sources (only use steam as source for now)
|
|
127
|
+
steam_usd_total = self.totals[PriceSource.STEAM]["usd"]
|
|
128
|
+
steam_eur_total = self.totals[PriceSource.STEAM]["eur"]
|
|
129
|
+
PriceLogs.save(steam_usd_total, steam_eur_total)
|
|
91
130
|
|
|
92
131
|
def _print_total(self):
|
|
93
132
|
"""Print the total prices in USD and EUR, formatted with titles and
|
|
94
133
|
separators.
|
|
95
134
|
"""
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
135
|
+
console.title("USD Total", "green")
|
|
136
|
+
for price_source, totals in self.totals.items():
|
|
137
|
+
usd_total = totals.get("usd")
|
|
138
|
+
console.print(f"{price_source.value.title():<10}: ${usd_total:.2f}")
|
|
99
139
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
140
|
+
console.title("EUR Total", "green")
|
|
141
|
+
for price_source, totals in self.totals.items():
|
|
142
|
+
eur_total = totals.get("eur")
|
|
143
|
+
console.print(f"{price_source.value.title():<10}: €{eur_total:.2f}")
|
|
103
144
|
|
|
104
|
-
|
|
105
|
-
console.print(f"[bold green]{end_string}\n")
|
|
145
|
+
console.separator("green")
|
|
106
146
|
|
|
107
147
|
def _send_discord_notification(self):
|
|
108
148
|
"""Send a message to a Discord webhook if notifications are enabled in the
|
|
109
149
|
config file and a webhook URL is provided.
|
|
110
150
|
"""
|
|
111
|
-
discord_notifications =
|
|
151
|
+
discord_notifications = config.getboolean(
|
|
112
152
|
"App Settings", "discord_notifications", fallback=False
|
|
113
153
|
)
|
|
114
|
-
webhook_url =
|
|
154
|
+
webhook_url = config.get("User Settings", "discord_webhook_url", fallback=None)
|
|
115
155
|
|
|
116
156
|
if discord_notifications and webhook_url:
|
|
117
157
|
DiscordNotifier.notify(webhook_url)
|
|
@@ -127,8 +167,8 @@ class Scraper:
|
|
|
127
167
|
:raises RequestException: If the request fails.
|
|
128
168
|
:raises RetryError: If the retry limit is reached.
|
|
129
169
|
"""
|
|
130
|
-
use_proxy =
|
|
131
|
-
proxy_api_key =
|
|
170
|
+
use_proxy = config.getboolean("App Settings", "use_proxy", fallback=False)
|
|
171
|
+
proxy_api_key = config.get("User Settings", "proxy_api_key", fallback=None)
|
|
132
172
|
|
|
133
173
|
if use_proxy and proxy_api_key:
|
|
134
174
|
page = self.session.get(
|
|
@@ -143,203 +183,85 @@ class Scraper:
|
|
|
143
183
|
page = self.session.get(url)
|
|
144
184
|
|
|
145
185
|
if not page.ok or not page.content:
|
|
146
|
-
|
|
186
|
+
self.error_stack.append(PageLoadError(page.status_code))
|
|
187
|
+
self._print_error()
|
|
147
188
|
raise RequestException(f"Failed to load page: {url}")
|
|
148
189
|
|
|
149
190
|
return page
|
|
150
191
|
|
|
151
|
-
def
|
|
152
|
-
"""
|
|
153
|
-
Parse the price of an item from the given steamcommunity market page and item
|
|
154
|
-
href.
|
|
155
|
-
|
|
156
|
-
:param item_page: The HTTP response object containing the item page content.
|
|
157
|
-
:param item_href: The href of the item listing to find the price for.
|
|
158
|
-
:return: The price of the item as a float.
|
|
159
|
-
:raises ValueError: If the item listing or price span cannot be found.
|
|
160
|
-
"""
|
|
161
|
-
item_soup = BeautifulSoup(item_page.content, "html.parser")
|
|
162
|
-
item_listing = item_soup.find("a", attrs={"href": f"{item_href}"})
|
|
163
|
-
if not isinstance(item_listing, Tag):
|
|
164
|
-
raise ValueError(f"Failed to find item listing: {item_href}")
|
|
165
|
-
|
|
166
|
-
item_price_span = item_listing.find("span", attrs={"class": "normal_price"})
|
|
167
|
-
if not isinstance(item_price_span, Tag):
|
|
168
|
-
raise ValueError(f"Failed to find price span in item listing: {item_href}")
|
|
169
|
-
|
|
170
|
-
price_str = item_price_span.text.split()[2]
|
|
171
|
-
price = float(price_str.replace("$", ""))
|
|
172
|
-
|
|
173
|
-
return price
|
|
174
|
-
|
|
175
|
-
def _scrape_capsule_prices(self, capsule_section, capsule_info, update_sheet_callback=None):
|
|
176
|
-
"""
|
|
177
|
-
Scrape prices for a specific capsule section, printing the details to the
|
|
178
|
-
console.
|
|
179
|
-
|
|
180
|
-
:param capsule_section: The section name in the config for the capsule.
|
|
181
|
-
:param capsule_info: A dictionary containing information about the capsule page,
|
|
182
|
-
hrefs, and names.
|
|
183
|
-
:param update_sheet_callback: Optional callback function to update a tksheet
|
|
184
|
-
that is displayed in the GUI with the latest scraper price calculation.
|
|
185
|
-
"""
|
|
186
|
-
capsule_title = capsule_section.center(MAX_LINE_LEN, SEPARATOR)
|
|
187
|
-
console.print(f"[bold magenta]{capsule_title}\n")
|
|
188
|
-
|
|
189
|
-
capsule_usd_total = 0
|
|
190
|
-
try:
|
|
191
|
-
capsule_page = self._get_page(capsule_info["page"])
|
|
192
|
-
for capsule_name, capsule_href in zip(capsule_info["names"], capsule_info["items"]):
|
|
193
|
-
config_capsule_name = capsule_name.replace(" ", "_").lower()
|
|
194
|
-
owned = self.config.getint(capsule_section, config_capsule_name, fallback=0)
|
|
195
|
-
if owned == 0:
|
|
196
|
-
continue
|
|
197
|
-
|
|
198
|
-
price_usd = self._parse_item_price(capsule_page, capsule_href)
|
|
199
|
-
price_usd_owned = round(float(owned * price_usd), 2)
|
|
200
|
-
|
|
201
|
-
console.print(f"[bold deep_sky_blue4]{capsule_name}")
|
|
202
|
-
console.print(PRICE_INFO.format(owned, price_usd, price_usd_owned))
|
|
203
|
-
if update_sheet_callback:
|
|
204
|
-
update_sheet_callback([capsule_name, owned, price_usd, price_usd_owned])
|
|
205
|
-
capsule_usd_total += price_usd_owned
|
|
206
|
-
except (RetryError, ValueError):
|
|
207
|
-
console.print(
|
|
208
|
-
"[bold red][!] Too many requests. (Consider using proxies to prevent rate limiting)\n"
|
|
209
|
-
)
|
|
210
|
-
except Exception as error:
|
|
211
|
-
console.print(f"[bold red][!] An unexpected error occurred: {error}\n")
|
|
212
|
-
|
|
213
|
-
return capsule_usd_total
|
|
214
|
-
|
|
215
|
-
def _scrape_capsule_section_prices(self, update_sheet_callback=None):
|
|
192
|
+
def _scrape_prices_from_all_sources(self, item_href, owned):
|
|
216
193
|
"""
|
|
217
|
-
|
|
194
|
+
For a given item href and owned count, scrape the item's price from all sources
|
|
195
|
+
available to the currently registered parser.
|
|
218
196
|
|
|
219
|
-
:param
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
if any(int(owned) > 0 for _, owned in self.config.items(capsule_section)):
|
|
226
|
-
capsule_usd_total += self._scrape_capsule_prices(
|
|
227
|
-
capsule_section, capsule_info, update_sheet_callback
|
|
228
|
-
)
|
|
229
|
-
|
|
230
|
-
return capsule_usd_total
|
|
231
|
-
|
|
232
|
-
def _market_page_from_href(self, item_href):
|
|
233
|
-
"""
|
|
234
|
-
Convert an href of a Steam Community Market item to a market page URL.
|
|
235
|
-
|
|
236
|
-
:param item_href: The href of the item listing, typically ending with the item's
|
|
237
|
-
name.
|
|
238
|
-
:return: A URL string for the Steam Community Market page of the item.
|
|
239
|
-
"""
|
|
240
|
-
url_encoded_name = item_href.split("/")[-1]
|
|
241
|
-
page_url = f"https://steamcommunity.com/market/search?q={url_encoded_name}"
|
|
242
|
-
|
|
243
|
-
return page_url
|
|
244
|
-
|
|
245
|
-
def _scrape_case_prices(self, update_sheet_callback=None):
|
|
246
|
-
"""
|
|
247
|
-
Scrape prices for all cases defined in the configuration.
|
|
248
|
-
|
|
249
|
-
For each case, it prints the case name, owned count, price per item, and total
|
|
250
|
-
price for owned items.
|
|
251
|
-
|
|
252
|
-
:param update_sheet_callback: Optional callback function to update a tksheet
|
|
253
|
-
that is displayed in the GUI with the latest scraper price calculation.
|
|
197
|
+
:param item_href: The url of the steamcommunity market listing of the item
|
|
198
|
+
:param owned: How many of this item the user owns
|
|
199
|
+
:return: A list of item prices for the different sources
|
|
200
|
+
:raises RequestException: If the request fails.
|
|
201
|
+
:raises RetryError: If the retry limit is reached.
|
|
202
|
+
:raises ValueError: If the parser could not find the item
|
|
254
203
|
"""
|
|
255
|
-
|
|
256
|
-
for
|
|
257
|
-
if int(owned) == 0:
|
|
258
|
-
continue
|
|
259
|
-
|
|
260
|
-
case_name = config_case_name.replace("_", " ").title()
|
|
261
|
-
case_title = case_name.center(MAX_LINE_LEN, SEPARATOR)
|
|
262
|
-
console.print(f"[bold magenta]{case_title}\n")
|
|
263
|
-
|
|
204
|
+
prices = []
|
|
205
|
+
for price_source in self.parser.SOURCES:
|
|
264
206
|
try:
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
price_usd = self.
|
|
268
|
-
price_usd_owned = round(float(int(owned) * price_usd), 2)
|
|
269
|
-
|
|
270
|
-
console.print(PRICE_INFO.format(owned, price_usd, price_usd_owned))
|
|
271
|
-
if update_sheet_callback:
|
|
272
|
-
update_sheet_callback([case_name, owned, price_usd, price_usd_owned])
|
|
273
|
-
case_usd_total += price_usd_owned
|
|
207
|
+
item_page_url = self.parser.get_item_page_url(item_href, price_source)
|
|
208
|
+
item_page = self._get_page(item_page_url)
|
|
209
|
+
price_usd = self.parser.parse_item_price(item_page, item_href, price_source)
|
|
274
210
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
211
|
+
price_usd_owned = round(float(int(owned) * price_usd), 2)
|
|
212
|
+
self.totals[price_source]["usd"] += price_usd_owned
|
|
213
|
+
|
|
214
|
+
prices += [price_usd, price_usd_owned]
|
|
215
|
+
console.price(
|
|
216
|
+
self.parser.PRICE_INFO,
|
|
217
|
+
owned,
|
|
218
|
+
price_source.value.title(),
|
|
219
|
+
price_usd,
|
|
220
|
+
price_usd_owned,
|
|
280
221
|
)
|
|
281
|
-
except
|
|
282
|
-
|
|
222
|
+
except ValueError as error:
|
|
223
|
+
prices += [0.0, 0.0]
|
|
224
|
+
self.error_stack.append(ParsingError(error))
|
|
225
|
+
self._print_error()
|
|
283
226
|
|
|
284
|
-
return
|
|
227
|
+
return prices
|
|
285
228
|
|
|
286
|
-
def
|
|
229
|
+
def _scrape_item_prices(self, section, update_sheet_callback=None):
|
|
287
230
|
"""
|
|
288
|
-
Scrape prices for
|
|
231
|
+
Scrape prices for all items defined in a configuration section that uses hrefs
|
|
232
|
+
as option keys.
|
|
289
233
|
|
|
290
|
-
For each
|
|
291
|
-
|
|
234
|
+
For each item, it prints the item name, owned count, price per item, and total
|
|
235
|
+
price for owned items.
|
|
292
236
|
|
|
293
237
|
:param update_sheet_callback: Optional callback function to update a tksheet
|
|
294
238
|
that is displayed in the GUI with the latest scraper price calculation.
|
|
295
239
|
"""
|
|
296
|
-
|
|
297
|
-
|
|
240
|
+
for item_href, owned in config.items(section):
|
|
241
|
+
if self.error_stack and isinstance(self.error_stack[-1], RequestLimitExceededError):
|
|
242
|
+
break
|
|
298
243
|
if int(owned) == 0:
|
|
299
244
|
continue
|
|
300
245
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
console.print(f"[bold magenta]{custom_item_title}\n")
|
|
304
|
-
|
|
246
|
+
item_name = config.option_to_name(item_href, href=True)
|
|
247
|
+
console.title(item_name, "magenta")
|
|
305
248
|
try:
|
|
306
|
-
|
|
307
|
-
custom_item_page = self._get_page(custom_item_page_url)
|
|
308
|
-
price_usd = self._parse_item_price(custom_item_page, custom_item_href)
|
|
309
|
-
price_usd_owned = round(float(int(owned) * price_usd), 2)
|
|
249
|
+
prices = self._scrape_prices_from_all_sources(item_href, owned)
|
|
310
250
|
|
|
311
|
-
console.print(PRICE_INFO.format(owned, price_usd, price_usd_owned))
|
|
312
251
|
if update_sheet_callback:
|
|
313
|
-
update_sheet_callback([
|
|
314
|
-
custom_item_usd_total += price_usd_owned
|
|
252
|
+
update_sheet_callback([item_name, owned] + prices)
|
|
315
253
|
|
|
316
|
-
if
|
|
254
|
+
if (
|
|
255
|
+
not config.getboolean("App Settings", "use_proxy", fallback=False)
|
|
256
|
+
and self.parser.NEEDS_TIMEOUT
|
|
257
|
+
):
|
|
317
258
|
time.sleep(1)
|
|
318
|
-
except
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
)
|
|
259
|
+
except RetryError:
|
|
260
|
+
self.error_stack.append(RequestLimitExceededError())
|
|
261
|
+
self._print_error()
|
|
322
262
|
except Exception as error:
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
return custom_item_usd_total
|
|
326
|
-
|
|
327
|
-
def toggle_use_proxy(self, enabled: bool):
|
|
328
|
-
"""
|
|
329
|
-
Toggle the use of proxies for requests. This will update the configuration file.
|
|
330
|
-
|
|
331
|
-
:param enabled: If True, proxies will be used; if False, they will not be used.
|
|
332
|
-
"""
|
|
333
|
-
self.config.toggle_use_proxy(enabled)
|
|
334
|
-
|
|
335
|
-
def toggle_discord_webhook(self, enabled: bool):
|
|
336
|
-
"""
|
|
337
|
-
Toggle the use of a Discord webhook to notify users of price calculations.
|
|
338
|
-
|
|
339
|
-
:param enabled: If True, the webhook will be used; if False, it will not be
|
|
340
|
-
used.
|
|
341
|
-
"""
|
|
342
|
-
self.config.toggle_discord_webhook(enabled)
|
|
263
|
+
self.error_stack.append(UnexpectedError(error))
|
|
264
|
+
self._print_error()
|
|
343
265
|
|
|
344
266
|
|
|
345
267
|
if __name__ == "__main__":
|
cs2tracker/util/__init__.py
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
from cs2tracker.util.padded_console import ( # noqa: F401 # pylint:disable=unused-import
|
|
2
|
-
|
|
2
|
+
get_console,
|
|
3
3
|
)
|
|
4
4
|
from cs2tracker.util.price_logs import ( # noqa: F401 # pylint:disable=unused-import
|
|
5
5
|
PriceLogs,
|
|
6
6
|
)
|
|
7
7
|
from cs2tracker.util.validated_config import ( # noqa: F401 # pylint:disable=unused-import
|
|
8
|
-
|
|
8
|
+
get_config,
|
|
9
9
|
)
|
|
@@ -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,6 +21,34 @@ class PaddedConsole:
|
|
|
17
21
|
"""Print text with padding to the console."""
|
|
18
22
|
self.console.print(Padding(text, self.padding))
|
|
19
23
|
|
|
24
|
+
def error(self, text):
|
|
25
|
+
"""Print error text with padding to the console."""
|
|
26
|
+
text = "[bold red][!] " + text
|
|
27
|
+
self.print(text)
|
|
28
|
+
|
|
29
|
+
def title(self, text, color):
|
|
30
|
+
"""Print the given text as a title."""
|
|
31
|
+
title = text.center(MAX_LINE_LEN, SEPARATOR)
|
|
32
|
+
console.print(f"\n[bold {color}]{title}\n")
|
|
33
|
+
|
|
34
|
+
def separator(self, color):
|
|
35
|
+
"""Print a separator line."""
|
|
36
|
+
separator = SEPARATOR * MAX_LINE_LEN
|
|
37
|
+
console.print(f"[bold {color}]{separator}")
|
|
38
|
+
|
|
39
|
+
def price(self, price_str, price_source, owned, steam_market_price, total_owned):
|
|
40
|
+
# pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
41
|
+
"""Print price information."""
|
|
42
|
+
console.print(price_str.format(price_source, owned, steam_market_price, total_owned))
|
|
43
|
+
|
|
20
44
|
def __getattr__(self, attr):
|
|
21
45
|
"""Ensure console methods can be called directly on PaddedConsole."""
|
|
22
46
|
return getattr(self.console, attr)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
console = PaddedConsole()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def get_console():
|
|
53
|
+
"""Get the PaddedConsole instance."""
|
|
54
|
+
return console
|