cs2tracker 2.1.12__py3-none-any.whl → 2.1.13__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.

@@ -11,7 +11,7 @@ from tenacity import RetryError, retry, stop_after_attempt
11
11
 
12
12
  from cs2tracker.constants import AUTHOR_STRING, BANNER, CAPSULE_INFO, CASE_HREFS
13
13
  from cs2tracker.scraper.discord_notifier import DiscordNotifier
14
- from cs2tracker.util import PaddedConsole, PriceLogs, ValidatedConfig
14
+ from cs2tracker.util import PriceLogs, get_config, get_console
15
15
 
16
16
  MAX_LINE_LEN = 72
17
17
  SEPARATOR = "-"
@@ -20,22 +20,38 @@ PRICE_INFO = "Owned: {:<10} Steam market price: ${:<10} Total: ${:<10}\n"
20
20
  HTTP_PROXY_URL = "http://{}:@smartproxy.crawlbase.com:8012"
21
21
  HTTPS_PROXY_URL = "http://{}:@smartproxy.crawlbase.com:8012"
22
22
 
23
- console = PaddedConsole()
23
+ console = get_console()
24
+ config = get_config()
25
+
26
+
27
+ class ConfigError:
28
+ def __init__(self):
29
+ self.message = "Invalid configuration. Please fix the config file before running."
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()
31
-
51
+ self.error_stack = []
32
52
  self.usd_total = 0
33
53
  self.eur_total = 0
34
54
 
35
- def load_config(self):
36
- """Load the configuration file and validate its contents."""
37
- self.config = ValidatedConfig()
38
-
39
55
  def _start_session(self):
40
56
  """Start a requests session with custom headers and retry logic."""
41
57
  self.session = Session()
@@ -48,6 +64,12 @@ class Scraper:
48
64
  self.session.mount("http://", HTTPAdapter(max_retries=retries))
49
65
  self.session.mount("https://", HTTPAdapter(max_retries=retries))
50
66
 
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")
72
+
51
73
  def scrape_prices(self, update_sheet_callback=None):
52
74
  """
53
75
  Scrape prices for capsules and cases, calculate totals in USD and EUR, and
@@ -56,12 +78,15 @@ class Scraper:
56
78
  :param update_sheet_callback: Optional callback function to update a tksheet
57
79
  that is displayed in the GUI with the latest scraper price calculation.
58
80
  """
59
- if not self.config.valid:
60
- console.print(
61
- "[bold red][!] Invalid configuration. Please fix the config file before running."
62
- )
81
+ if not config.valid:
82
+ self.error_stack.append(ConfigError())
83
+ self._print_error()
63
84
  return
64
85
 
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()
89
+
65
90
  capsule_usd_total = self._scrape_capsule_section_prices(update_sheet_callback)
66
91
  case_usd_total = self._scrape_case_prices(update_sheet_callback)
67
92
  custom_item_usd_total = self._scrape_custom_item_prices(update_sheet_callback)
@@ -86,9 +111,6 @@ class Scraper:
86
111
  PriceLogs.save(self.usd_total, self.eur_total)
87
112
  self._send_discord_notification()
88
113
 
89
- # Reset totals for next run
90
- self.usd_total, self.eur_total = 0, 0
91
-
92
114
  def _print_total(self):
93
115
  """Print the total prices in USD and EUR, formatted with titles and
94
116
  separators.
@@ -108,10 +130,10 @@ class Scraper:
108
130
  """Send a message to a Discord webhook if notifications are enabled in the
109
131
  config file and a webhook URL is provided.
110
132
  """
111
- discord_notifications = self.config.getboolean(
133
+ discord_notifications = config.getboolean(
112
134
  "App Settings", "discord_notifications", fallback=False
113
135
  )
114
- webhook_url = self.config.get("User Settings", "discord_webhook_url", fallback=None)
136
+ webhook_url = config.get("User Settings", "discord_webhook_url", fallback=None)
115
137
 
116
138
  if discord_notifications and webhook_url:
117
139
  DiscordNotifier.notify(webhook_url)
@@ -127,8 +149,8 @@ class Scraper:
127
149
  :raises RequestException: If the request fails.
128
150
  :raises RetryError: If the retry limit is reached.
129
151
  """
130
- use_proxy = self.config.getboolean("App Settings", "use_proxy", fallback=False)
131
- proxy_api_key = self.config.get("User Settings", "proxy_api_key", fallback=None)
152
+ use_proxy = config.getboolean("App Settings", "use_proxy", fallback=False)
153
+ proxy_api_key = config.get("User Settings", "proxy_api_key", fallback=None)
132
154
 
133
155
  if use_proxy and proxy_api_key:
134
156
  page = self.session.get(
@@ -143,11 +165,32 @@ class Scraper:
143
165
  page = self.session.get(url)
144
166
 
145
167
  if not page.ok or not page.content:
146
- console.print(f"[bold red][!] Failed to load page ({page.status_code}). Retrying...\n")
168
+ self.error_stack.append(PageLoadError(page.status_code))
169
+ self._print_error()
147
170
  raise RequestException(f"Failed to load page: {url}")
148
171
 
149
172
  return page
150
173
 
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
+
151
194
  def _parse_item_price(self, item_page, item_href):
152
195
  """
153
196
  Parse the price of an item from the given steamcommunity market page and item
@@ -183,15 +226,14 @@ class Scraper:
183
226
  :param update_sheet_callback: Optional callback function to update a tksheet
184
227
  that is displayed in the GUI with the latest scraper price calculation.
185
228
  """
186
- capsule_title = capsule_section.center(MAX_LINE_LEN, SEPARATOR)
187
- console.print(f"[bold magenta]{capsule_title}\n")
188
-
229
+ self._print_item_title(capsule_section)
189
230
  capsule_usd_total = 0
190
231
  try:
191
232
  capsule_page = self._get_page(capsule_info["page"])
192
- for capsule_name, capsule_href in zip(capsule_info["names"], capsule_info["items"]):
233
+ for capsule_href in capsule_info["items"]:
234
+ capsule_name = unquote(capsule_href.split("/")[-1])
193
235
  config_capsule_name = capsule_name.replace(" ", "_").lower()
194
- owned = self.config.getint(capsule_section, config_capsule_name, fallback=0)
236
+ owned = config.getint(capsule_section, config_capsule_name, fallback=0)
195
237
  if owned == 0:
196
238
  continue
197
239
 
@@ -200,15 +242,16 @@ class Scraper:
200
242
 
201
243
  console.print(f"[bold deep_sky_blue4]{capsule_name}")
202
244
  console.print(PRICE_INFO.format(owned, price_usd, price_usd_owned))
245
+ capsule_usd_total += price_usd_owned
246
+
203
247
  if update_sheet_callback:
204
248
  update_sheet_callback([capsule_name, owned, price_usd, price_usd_owned])
205
- capsule_usd_total += price_usd_owned
206
249
  except (RetryError, ValueError):
207
- console.print(
208
- "[bold red][!] Too many requests. (Consider using proxies to prevent rate limiting)\n"
209
- )
250
+ self.error_stack.append(RequestLimitExceededError())
251
+ self._print_error()
210
252
  except Exception as error:
211
- console.print(f"[bold red][!] An unexpected error occurred: {error}\n")
253
+ self.error_stack.append(UnexpectedError(error))
254
+ self._print_error()
212
255
 
213
256
  return capsule_usd_total
214
257
 
@@ -221,12 +264,18 @@ class Scraper:
221
264
  """
222
265
  capsule_usd_total = 0
223
266
  for capsule_section, capsule_info in CAPSULE_INFO.items():
267
+ if self.error_stack:
268
+ break
269
+
224
270
  # Only scrape capsule sections where the user owns at least one item
225
- if any(int(owned) > 0 for _, owned in self.config.items(capsule_section)):
271
+ if any(int(owned) > 0 for _, owned in config.items(capsule_section)):
226
272
  capsule_usd_total += self._scrape_capsule_prices(
227
273
  capsule_section, capsule_info, update_sheet_callback
228
274
  )
229
275
 
276
+ if not config.getboolean("App Settings", "use_proxy", fallback=False):
277
+ time.sleep(1)
278
+
230
279
  return capsule_usd_total
231
280
 
232
281
  def _market_page_from_href(self, item_href):
@@ -253,14 +302,13 @@ class Scraper:
253
302
  that is displayed in the GUI with the latest scraper price calculation.
254
303
  """
255
304
  case_usd_total = 0
256
- for case_index, (config_case_name, owned) in enumerate(self.config.items("Cases")):
305
+ for case_index, (config_case_name, owned) in enumerate(config.items("Cases")):
306
+ if self.error_stack:
307
+ break
257
308
  if int(owned) == 0:
258
309
  continue
259
310
 
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
-
311
+ case_name = self._print_item_title(config_case_name, from_config=True)
264
312
  try:
265
313
  case_page_url = self._market_page_from_href(CASE_HREFS[case_index])
266
314
  case_page = self._get_page(case_page_url)
@@ -268,18 +316,19 @@ class Scraper:
268
316
  price_usd_owned = round(float(int(owned) * price_usd), 2)
269
317
 
270
318
  console.print(PRICE_INFO.format(owned, price_usd, price_usd_owned))
319
+ case_usd_total += price_usd_owned
320
+
271
321
  if update_sheet_callback:
272
322
  update_sheet_callback([case_name, owned, price_usd, price_usd_owned])
273
- case_usd_total += price_usd_owned
274
323
 
275
- if not self.config.getboolean("App Settings", "use_proxy", fallback=False):
324
+ if not config.getboolean("App Settings", "use_proxy", fallback=False):
276
325
  time.sleep(1)
277
326
  except (RetryError, ValueError):
278
- console.print(
279
- "[bold red][!] Too many requests. (Consider using proxies to prevent rate limiting)\n"
280
- )
327
+ self.error_stack.append(RequestLimitExceededError())
328
+ self._print_error()
281
329
  except Exception as error:
282
- console.print(f"[bold red][!] An unexpected error occurred: {error}\n")
330
+ self.error_stack.append(UnexpectedError(error))
331
+ self._print_error()
283
332
 
284
333
  return case_usd_total
285
334
 
@@ -294,14 +343,13 @@ class Scraper:
294
343
  that is displayed in the GUI with the latest scraper price calculation.
295
344
  """
296
345
  custom_item_usd_total = 0
297
- for custom_item_href, owned in self.config.items("Custom Items"):
346
+ for custom_item_href, owned in config.items("Custom Items"):
347
+ if self.error_stack:
348
+ break
298
349
  if int(owned) == 0:
299
350
  continue
300
351
 
301
- custom_item_name = unquote(custom_item_href.split("/")[-1])
302
- custom_item_title = custom_item_name.center(MAX_LINE_LEN, SEPARATOR)
303
- console.print(f"[bold magenta]{custom_item_title}\n")
304
-
352
+ custom_item_name = self._print_item_title(custom_item_href, from_href=True)
305
353
  try:
306
354
  custom_item_page_url = self._market_page_from_href(custom_item_href)
307
355
  custom_item_page = self._get_page(custom_item_page_url)
@@ -309,38 +357,22 @@ class Scraper:
309
357
  price_usd_owned = round(float(int(owned) * price_usd), 2)
310
358
 
311
359
  console.print(PRICE_INFO.format(owned, price_usd, price_usd_owned))
360
+ custom_item_usd_total += price_usd_owned
361
+
312
362
  if update_sheet_callback:
313
363
  update_sheet_callback([custom_item_name, owned, price_usd, price_usd_owned])
314
- custom_item_usd_total += price_usd_owned
315
364
 
316
- if not self.config.getboolean("App Settings", "use_proxy", fallback=False):
365
+ if not config.getboolean("App Settings", "use_proxy", fallback=False):
317
366
  time.sleep(1)
318
367
  except (RetryError, ValueError):
319
- console.print(
320
- "[bold red][!] Too many requests. (Consider using proxies to prevent rate limiting)\n"
321
- )
368
+ self.error_stack.append(RequestLimitExceededError())
369
+ self._print_error()
322
370
  except Exception as error:
323
- console.print(f"[bold red][!] An unexpected error occurred: {error}\n")
371
+ self.error_stack.append(UnexpectedError(error))
372
+ self._print_error()
324
373
 
325
374
  return custom_item_usd_total
326
375
 
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)
343
-
344
376
 
345
377
  if __name__ == "__main__":
346
378
  scraper = Scraper()
@@ -1,9 +1,9 @@
1
1
  from cs2tracker.util.padded_console import ( # noqa: F401 # pylint:disable=unused-import
2
- PaddedConsole,
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
- ValidatedConfig,
8
+ get_config,
9
9
  )
@@ -17,6 +17,19 @@ class PaddedConsole:
17
17
  """Print text with padding to the console."""
18
18
  self.console.print(Padding(text, self.padding))
19
19
 
20
+ def error(self, text):
21
+ """Print error text with padding to the console."""
22
+ text = "[bold red][!] " + text
23
+ self.print(text)
24
+
20
25
  def __getattr__(self, attr):
21
26
  """Ensure console methods can be called directly on PaddedConsole."""
22
27
  return getattr(self.console, attr)
28
+
29
+
30
+ console = PaddedConsole()
31
+
32
+
33
+ def get_console():
34
+ """Get the PaddedConsole instance."""
35
+ return console
@@ -1,24 +1,31 @@
1
+ import json
1
2
  import re
2
3
  from configparser import ConfigParser
4
+ from urllib.parse import quote
3
5
 
4
- from cs2tracker.constants import CAPSULE_INFO, CONFIG_FILE
5
- from cs2tracker.util.padded_console import PaddedConsole
6
-
7
- console = PaddedConsole()
8
-
6
+ from cs2tracker.constants import CAPSULE_INFO, CONFIG_FILE, INVENTORY_IMPORT_FILE
7
+ from cs2tracker.util.padded_console import get_console
9
8
 
9
+ STEAM_MARKET_LISTING_BASEURL_CS2 = "https://steamcommunity.com/market/listings/730/"
10
10
  STEAM_MARKET_LISTING_REGEX = r"^https://steamcommunity.com/market/listings/\d+/.+$"
11
11
 
12
+ console = get_console()
13
+
12
14
 
13
15
  class ValidatedConfig(ConfigParser):
14
16
  def __init__(self):
15
17
  """Initialize the ValidatedConfig class."""
16
18
  super().__init__(delimiters=("~"), interpolation=None)
17
19
  self.optionxform = str # type: ignore
18
- super().read(CONFIG_FILE)
19
20
 
20
21
  self.valid = False
21
22
  self.last_error = None
23
+ self.load()
24
+
25
+ def load(self):
26
+ """Load the configuration file and validate it."""
27
+ self.clear()
28
+ self.read(CONFIG_FILE)
22
29
  self._validate_config()
23
30
 
24
31
  def _validate_config_sections(self):
@@ -77,18 +84,50 @@ class ValidatedConfig(ConfigParser):
77
84
  self._validate_config_values()
78
85
  self.valid = True
79
86
  except ValueError as error:
80
- console.print(f"[bold red][!] Config error: {error}")
87
+ console.error(f"Config error: {error}")
81
88
  self.valid = False
82
89
  self.last_error = error
83
90
 
84
91
  def write_to_file(self):
85
- """Write the current configuration to the configuration file."""
92
+ """Validate the current configuration and write it to the configuration file if
93
+ it is valid.
94
+ """
86
95
  self._validate_config()
87
96
 
88
97
  if self.valid:
89
98
  with open(CONFIG_FILE, "w", encoding="utf-8") as config_file:
90
99
  self.write(config_file)
91
100
 
101
+ def read_from_inventory_file(self):
102
+ """
103
+ Read an inventory file into the configuration.
104
+
105
+ This file is generated after a user automatically imports their inventory.
106
+ """
107
+ try:
108
+ with open(INVENTORY_IMPORT_FILE, "r", encoding="utf-8") as inventory_file:
109
+ inventory_data = json.load(inventory_file)
110
+
111
+ added_to_config = set()
112
+ for item_name, item_owned in inventory_data.items():
113
+ config_item_name = item_name.replace(" ", "_").lower()
114
+ for section in self.sections():
115
+ if config_item_name in self.options(section):
116
+ self.set(section, config_item_name, str(item_owned))
117
+ added_to_config.add(item_name)
118
+
119
+ for item_name, item_owned in inventory_data.items():
120
+ if item_name not in added_to_config:
121
+ url_encoded_item_name = quote(item_name)
122
+ listing_url = f"{STEAM_MARKET_LISTING_BASEURL_CS2}{url_encoded_item_name}"
123
+ self.set("Custom Items", listing_url, str(item_owned))
124
+
125
+ self.write_to_file()
126
+ except (FileNotFoundError, json.JSONDecodeError) as error:
127
+ console.error(f"Error reading inventory file: {error}")
128
+ self.last_error = error
129
+ self.valid = False
130
+
92
131
  def toggle_use_proxy(self, enabled: bool):
93
132
  """
94
133
  Toggle the use of proxies for requests. This will update the configuration file.
@@ -115,3 +154,11 @@ class ValidatedConfig(ConfigParser):
115
154
  console.print(
116
155
  f"[bold green]{'[+] Enabled' if enabled else '[-] Disabled'} Discord webhook notifications."
117
156
  )
157
+
158
+
159
+ config = ValidatedConfig()
160
+
161
+
162
+ def get_config():
163
+ """Accessor function to retrieve the current configuration."""
164
+ return config
@@ -0,0 +1,147 @@
1
+ Metadata-Version: 2.4
2
+ Name: cs2tracker
3
+ Version: 2.1.13
4
+ Summary: Tracking the steam market prices of CS2 items
5
+ Home-page: https://github.com/ashiven/cs2tracker
6
+ Author: Jannik Novak
7
+ Author-email: nevisha@pm.me
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Requires-Python: >=3.11
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: numpy==1.26.4
15
+ Requires-Dist: beautifulsoup4==4.11.1
16
+ Requires-Dist: CurrencyConverter==0.17.9
17
+ Requires-Dist: matplotlib==3.7.0
18
+ Requires-Dist: Requests==2.31.0
19
+ Requires-Dist: rich==13.6.0
20
+ Requires-Dist: tenacity==8.2.2
21
+ Requires-Dist: urllib3==2.1.0
22
+ Requires-Dist: sv_ttk==2.6.1
23
+ Requires-Dist: tksheet==7.5.12
24
+ Requires-Dist: nodejs-bin==18.4.0a4
25
+ Requires-Dist: ttk-text==0.2.0
26
+ Dynamic: license-file
27
+
28
+ <p align="center">
29
+ <h2 align="center">CS2Tracker</h2>
30
+ </p>
31
+
32
+ <p align="center">
33
+ A simple, elegant GUI tool to track CS2 item investments
34
+ </p>
35
+
36
+ <div align="center">
37
+
38
+ [![CC BY-NC-ND 4.0][cc-by-nc-nd-shield]][cc-by-nc-nd]
39
+ [![GitHub Release](https://img.shields.io/github/v/release/ashiven/cs2tracker)](https://github.com/ashiven/cs2tracker/releases)
40
+ [![PyPI version](https://badge.fury.io/py/cs2tracker.svg)](https://badge.fury.io/py/cs2tracker)
41
+ [![GitHub Issues or Pull Requests](https://img.shields.io/github/issues/ashiven/cs2tracker)](https://github.com/ashiven/cs2tracker/issues)
42
+ [![GitHub Issues or Pull Requests](https://img.shields.io/github/issues-pr/ashiven/cs2tracker)](https://github.com/ashiven/cs2tracker/pulls)
43
+ ![GitHub Repo stars](https://img.shields.io/github/stars/ashiven/cs2tracker)
44
+
45
+ <img src="https://github.com/user-attachments/assets/9585afb2-bf1a-473c-be5d-cccbb3349b9a"/>
46
+ </div>
47
+
48
+ ## Table of Contents
49
+
50
+ - [Features](#features)
51
+ - [Getting Started](#getting-started)
52
+ - [Prerequisites](#prerequisites)
53
+ - [Installation](#installation)
54
+ - [Usage](#usage)
55
+ - [Configuration](#configuration)
56
+ - [Advanced Features](#advanced-features)
57
+ - [Contributing](#contributing)
58
+ - [License](#license)
59
+
60
+ ## Features
61
+
62
+ - ⚡ Rapidly import your Storage Units
63
+ - 🔍 Track CS2 Steam Market prices
64
+ - 📈 View investment price history
65
+ - 🧾 Export/Import price data
66
+ - 📤 Discord notifications on updates
67
+ - 📅 Daily background calculations
68
+ - 🛡️ Proxy support to avoid rate limits
69
+
70
+ ## Getting Started
71
+
72
+ ### Prerequisites
73
+
74
+ - Download and install the latest versions of [Python](https://www.python.org/downloads/) and [Pip](https://pypi.org/project/pip/). (Required on Linux)
75
+ - Register for the [Crawlbase Smart Proxy API](https://crawlbase.com/) and retrieve your API key. (Optional)
76
+ - Create a [Discord Webhook](https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks) to be notified about recent price updates. (Optional)
77
+
78
+ ### Installation
79
+
80
+ #### Option 1: Windows Executable
81
+
82
+ - Simply [download the latest executable](https://github.com/ashiven/cs2tracker/releases/latest/download/cs2tracker-windows.zip) and run it.
83
+
84
+ #### Option 2: Install via Pip
85
+
86
+ 1. Install the program:
87
+
88
+ ```bash
89
+ pip install cs2tracker
90
+ ```
91
+
92
+ 2. Run it:
93
+
94
+ ```bash
95
+ cs2tracker
96
+ ```
97
+
98
+ ## Usage
99
+
100
+ - Click **Run!** to gather the current market prices of your items and calculate the total amount in USD and EUR.
101
+ - The generated Excel sheet can be saved by right-clicking and then selecting **Save Sheet**.
102
+ - Use **Edit Config** to specify the numbers of items owned in the configuration.
103
+ - Click **Show History** to see a price chart consisting of past calculations.
104
+ - Use **Export / Import History** to export or import the price history to or from a CSV file.
105
+
106
+ ## Configuration
107
+
108
+ You can configure the app settings via the **Edit Config** option.
109
+ This will open the config editor where you can change any setting by simply double clicking on it. On top of that, the config editor allows you to:
110
+
111
+ - Automatically import items from your Storage Units
112
+ - Manually Specify the number of items you own
113
+ - Add custom items that are not listed in the config
114
+ - Enter Discord webhook and Crawlbase proxy API keys
115
+
116
+ ## Advanced Features
117
+
118
+ - Enable **Daily Background Calculations** to automatically run a daily calculation of your investment in the background.
119
+ - Use **Receive Discord Notifications** to receive a notification on your Discord server whenever the program has finished calculating your investment.
120
+ - You need to set up a [webhook](https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks) in your Discord server and enter the webhook url into the `discord_webhook_url` field in the config `User Settings`.
121
+ - Enable **Proxy Requests** to prevent your requests from being rate limited by the steamcommunity server.
122
+ - You need to register for a free API key on [Crawlbase](crawlbase.com) and enter it into the `proxy_api_key` field in the config `User Settings`.
123
+
124
+ ## Contributing
125
+
126
+ Please feel free to submit a pull request or open an issue. See [issues](https://github.com/ashiven/cs2tracker/issues) and [pull requests](https://github.com/ashiven/cs2tracker/pulls) for current work.
127
+
128
+ 1. Fork the repository
129
+ 2. Create a new branch
130
+ 3. Make your changes
131
+ 4. Submit a PR
132
+
133
+ ## License
134
+
135
+ This project is licensed under the
136
+ [Creative Commons Attribution-NonCommercial-NoDerivs 4.0 International License][cc-by-nc-nd].
137
+
138
+ [![CC BY-NC-ND 4.0][cc-by-nc-nd-image]][cc-by-nc-nd]
139
+
140
+ [cc-by-nc-nd]: http://creativecommons.org/licenses/by-nc-nd/4.0/
141
+ [cc-by-nc-nd-image]: https://licensebuttons.net/l/by-nc-nd/4.0/88x31.png
142
+ [cc-by-nc-nd-shield]: https://img.shields.io/badge/License-CC%20BY--NC--ND%204.0-lightgrey.svg
143
+
144
+ ---
145
+
146
+ > GitHub [@ashiven](https://github.com/Ashiven) &nbsp;&middot;&nbsp;
147
+ > Twitter [ashiven\_](https://twitter.com/ashiven_)
@@ -0,0 +1,27 @@
1
+ cs2tracker/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ cs2tracker/__main__.py,sha256=Ub--oSMv48YzfWF1CZqYlkn1-HvZ7Bhxoc7urn1oY6o,249
3
+ cs2tracker/_version.py,sha256=MzsJ263HZYR6SmK-vVyZsM4dSlCzgRW6jJZBsjDPWV8,513
4
+ cs2tracker/constants.py,sha256=65tLl30rCbR78xoNJSV0ubWc0ZWbytztj0S_uwgNrvU,22879
5
+ cs2tracker/main.py,sha256=Ahsu1gk0RuMS9FdVsv7exnnWENd8-vj_dcCEWjZP1DM,1552
6
+ cs2tracker/app/__init__.py,sha256=uqAxdDzoR2-2IrDc1riIU3Pi9vLEDwr68eg93-0RFmM,105
7
+ cs2tracker/app/application.py,sha256=TEoGvqxyCTJKdfC1Su7bh9fzTr71jiVqlTC_K4Wdu2Q,9395
8
+ cs2tracker/app/editor_frame.py,sha256=V_8SRWgterkEYiADrZww9prpKfX-3ccjd2OWVrx7-2g,18074
9
+ cs2tracker/app/scraper_frame.py,sha256=yrtnWtaNQqd8Ho6hVUxLP3A5oCBmtvEHJa3p8STEJtA,3399
10
+ cs2tracker/data/config.ini,sha256=anKg7t3JB7kQcvcMKHls4oxTEGwWnGkM9obn-_l0gEE,6806
11
+ cs2tracker/data/convert_inventory.js,sha256=5bEDfe9Rf2wYC4dIlmvjhbslfnuKuBBNjuTk67jEV9Q,5370
12
+ cs2tracker/data/get_inventory.js,sha256=O9w7w-diI7tD5NMgGMDyvrNfrv5ObE46m5rMK8PlpBE,4108
13
+ cs2tracker/data/output.csv,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
+ cs2tracker/scraper/__init__.py,sha256=kfUB9yfshSche92ZvTqQQYBkVqujRez46djPoa9u0YU,335
15
+ cs2tracker/scraper/background_task.py,sha256=1_lKnFWn9wuABpru58JdQbfDHjL0Up-7h6Nlcc4TsZ4,3671
16
+ cs2tracker/scraper/discord_notifier.py,sha256=-PdTRAtzMHTjDH17zguI0LKB7INITEohx0da0fWRdKI,2962
17
+ cs2tracker/scraper/scraper.py,sha256=Zj-8EZZNAjifiOywfvJrOQ8KtJK-SeaZerwx8plvLI8,15358
18
+ cs2tracker/util/__init__.py,sha256=Pgltyrkotd5ijU-srmY5vaFP7gzz9itSAl2LfS-D7-E,322
19
+ cs2tracker/util/padded_console.py,sha256=LXgPDqccgrIowwjV6CLsxn5zxuoiPA-9pomURPSLllU,936
20
+ cs2tracker/util/price_logs.py,sha256=JuZ2ptDtxA-NzKjpG_4q0eeOr1IaUvVah-Qth6GrDDU,4165
21
+ cs2tracker/util/validated_config.py,sha256=bQDIuu42hK0ZbFY1fYGTeYTip7tUNmxJLou88F3r4q8,6638
22
+ cs2tracker-2.1.13.dist-info/licenses/LICENSE,sha256=doPNswWMPXbkhplb9cnZLwJoqqS72pJPhkSib8kIF08,19122
23
+ cs2tracker-2.1.13.dist-info/METADATA,sha256=Os14jGDiniACP9PCaaaGL9SK2otVtIBkYt3mD-MtSIk,5694
24
+ cs2tracker-2.1.13.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
25
+ cs2tracker-2.1.13.dist-info/entry_points.txt,sha256=K8IwDIkg8QztSB9g9c89B9jR_2pG4QyJGrNs4z5RcZw,63
26
+ cs2tracker-2.1.13.dist-info/top_level.txt,sha256=2HB4xDDOxaU5BDc_yvdi9UlYLgL768n8aR-hRhFM6VQ,11
27
+ cs2tracker-2.1.13.dist-info/RECORD,,