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.

@@ -1,21 +1,18 @@
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
- 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.constants import AUTHOR_STRING, BANNER, CAPSULE_INFO, CASE_HREFS
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.util import PriceLogs, get_config, get_console
15
-
16
- MAX_LINE_LEN = 72
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
- self.usd_total = 0
53
- self.eur_total = 0
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 = 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 _print_error(self):
68
- """Print the last error message from the error stack, if any."""
69
- last_error = self.error_stack[-1] if self.error_stack else None
70
- if last_error:
71
- console.error(f"{last_error.message}\n")
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 EUR, 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.error_stack.append(ConfigError())
83
- self._print_error()
118
+ self._error(ConfigError())
84
119
  return
85
120
 
86
- # Reset totals from the previous run and clear the error stack
87
- self.usd_total, self.eur_total = 0, 0
88
- self.error_stack.clear()
121
+ self._prepare_new_run()
89
122
 
90
- capsule_usd_total = self._scrape_capsule_section_prices(update_sheet_callback)
91
- case_usd_total = self._scrape_case_prices(update_sheet_callback)
92
- custom_item_usd_total = self._scrape_custom_item_prices(update_sheet_callback)
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._print_total()
111
- PriceLogs.save(self.usd_total, self.eur_total)
128
+ self._convert_totals()
129
+ self._print_totals(update_sheet_callback)
112
130
  self._send_discord_notification()
113
131
 
114
- def _print_total(self):
115
- """Print the total prices in USD and EUR, formatted with titles and
116
- separators.
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
- usd_title = "USD Total".center(MAX_LINE_LEN, SEPARATOR)
119
- console.print(f"[bold green]{usd_title}")
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
- eur_title = "EUR Total".center(MAX_LINE_LEN, SEPARATOR)
123
- console.print(f"[bold green]{eur_title}")
124
- console.print(f"€{self.eur_total:.2f}")
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
- end_string = SEPARATOR * MAX_LINE_LEN
127
- console.print(f"[bold green]{end_string}\n")
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
- discord_notifications = config.getboolean(
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 webhook_url:
139
- DiscordNotifier.notify(webhook_url)
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
- use_proxy = config.getboolean("App Settings", "use_proxy", fallback=False)
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.error_stack.append(PageLoadError(page.status_code))
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 _print_item_title(self, raw_item_str, from_config=False, from_href=False):
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
- item_soup = BeautifulSoup(item_page.content, "html.parser")
205
- item_listing = item_soup.find("a", attrs={"href": f"{item_href}"})
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
- return price
217
-
218
- def _scrape_capsule_prices(self, capsule_section, capsule_info, update_sheet_callback=None):
219
- """
220
- Scrape prices for a specific capsule section, printing the details to the
221
- console.
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
- capsule_usd_total = 0
266
- for capsule_section, capsule_info in CAPSULE_INFO.items():
267
- if self.error_stack:
268
- break
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
- # Only scrape capsule sections where the user owns at least one item
271
- if any(int(owned) > 0 for _, owned in config.items(capsule_section)):
272
- capsule_usd_total += self._scrape_capsule_prices(
273
- capsule_section, capsule_info, update_sheet_callback
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
- if not config.getboolean("App Settings", "use_proxy", fallback=False):
277
- time.sleep(1)
278
-
279
- return capsule_usd_total
257
+ return prices
280
258
 
281
- def _market_page_from_href(self, item_href):
259
+ def _scrape_item_prices(self, section, update_sheet_callback=None):
282
260
  """
283
- Convert an href of a Steam Community Market item to a market page URL.
261
+ Scrape prices for all items defined in a configuration section that uses hrefs
262
+ as option keys.
284
263
 
285
- :param item_href: The href of the item listing, typically ending with the item's
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
- case_usd_total = 0
305
- for case_index, (config_case_name, owned) in enumerate(config.items("Cases")):
306
- if self.error_stack:
307
- break
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
- custom_item_name = self._print_item_title(custom_item_href, from_href=True)
278
+ item_name = config.option_to_name(item_href, href=True)
279
+ console.title(item_name, "magenta")
353
280
  try:
354
- custom_item_page_url = self._market_page_from_href(custom_item_href)
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
- update_sheet_callback([custom_item_name, owned, price_usd, price_usd_owned])
284
+ try:
285
+ update_sheet_callback([item_name, owned] + prices)
286
+ except Exception:
287
+ self._error(SheetNotFoundError())
364
288
 
365
- if not config.getboolean("App Settings", "use_proxy", fallback=False):
289
+ if not config.use_proxy and Parser.NEEDS_TIMEOUT:
366
290
  time.sleep(1)
367
- except (RetryError, ValueError):
368
- self.error_stack.append(RequestLimitExceededError())
369
- self._print_error()
291
+ except RetryError:
292
+ self._error(RequestLimitExceededError())
370
293
  except Exception as error:
371
- self.error_stack.append(UnexpectedError(error))
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__":
@@ -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)