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.

@@ -0,0 +1,55 @@
1
+ import sv_ttk
2
+
3
+
4
+ def centered(window, geometry):
5
+ """Convert a regular WidthxHeight geometry into one that is centered."""
6
+ w, h = geometry.split("x")
7
+ w, h = int(w), int(h)
8
+
9
+ ws = window.winfo_screenwidth()
10
+ hs = window.winfo_screenheight()
11
+ x = (ws / 2) - (w / 2)
12
+ y = (hs / 2) - (h / 2)
13
+
14
+ x, y = int(x), int(y)
15
+
16
+ return f"{w}x{h}+{x}+{y}"
17
+
18
+
19
+ def size_info(geometry):
20
+ """Extract the window width and height from a geometry string."""
21
+ width, height = geometry.split("x")
22
+ width, height = int(width), int(height)
23
+
24
+ return width, height
25
+
26
+
27
+ def fix_sv_ttk(style):
28
+ """
29
+ Fixes the themed text entry widget in sv_ttk.
30
+
31
+ Source: https://github.com/Jesse205/TtkText?tab=readme-ov-file
32
+ """
33
+ if sv_ttk.get_theme() == "light":
34
+ style.configure("ThemedText.TEntry", fieldbackground="#fdfdfd", textpadding=5)
35
+ style.map(
36
+ "ThemedText.TEntry",
37
+ fieldbackground=[
38
+ ("hover", "!focus", "#f9f9f9"),
39
+ ],
40
+ foreground=[
41
+ ("pressed", style.lookup("TEntry", "foreground")),
42
+ ],
43
+ )
44
+ else:
45
+ style.configure("ThemedText.TEntry", fieldbackground="#292929", textpadding=5)
46
+ style.map(
47
+ "ThemedText.TEntry",
48
+ fieldbackground=[
49
+ ("hover", "!focus", "#2f2f2f"),
50
+ ("focus", "#1c1c1c"),
51
+ ],
52
+ foreground=[
53
+ ("pressed", style.lookup("TEntry", "foreground")),
54
+ ],
55
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cs2tracker
3
- Version: 2.1.13
3
+ Version: 2.1.15
4
4
  Summary: Tracking the steam market prices of CS2 items
5
5
  Home-page: https://github.com/ashiven/cs2tracker
6
6
  Author: Jannik Novak
@@ -23,6 +23,7 @@ Requires-Dist: sv_ttk==2.6.1
23
23
  Requires-Dist: tksheet==7.5.12
24
24
  Requires-Dist: nodejs-bin==18.4.0a4
25
25
  Requires-Dist: ttk-text==0.2.0
26
+ Requires-Dist: requests-cache==1.2.1
26
27
  Dynamic: license-file
27
28
 
28
29
  <p align="center">
@@ -40,9 +41,8 @@ Dynamic: license-file
40
41
  [![PyPI version](https://badge.fury.io/py/cs2tracker.svg)](https://badge.fury.io/py/cs2tracker)
41
42
  [![GitHub Issues or Pull Requests](https://img.shields.io/github/issues/ashiven/cs2tracker)](https://github.com/ashiven/cs2tracker/issues)
42
43
  [![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
44
 
45
- <img src="https://github.com/user-attachments/assets/9585afb2-bf1a-473c-be5d-cccbb3349b9a"/>
45
+ <img src="./assets/demo.gif"/>
46
46
  </div>
47
47
 
48
48
  ## Table of Contents
@@ -60,9 +60,9 @@ Dynamic: license-file
60
60
  ## Features
61
61
 
62
62
  - ⚡ Rapidly import your Storage Units
63
- - 🔍 Track CS2 Steam Market prices
63
+ - 🔍 Track prices on Steam, Buff163, Youpin898
64
64
  - 📈 View investment price history
65
- - 🧾 Export/Import price data
65
+ - 🧾 Export/Import history data
66
66
  - 📤 Discord notifications on updates
67
67
  - 📅 Daily background calculations
68
68
  - 🛡️ Proxy support to avoid rate limits
@@ -106,7 +106,7 @@ Dynamic: license-file
106
106
  ## Configuration
107
107
 
108
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:
109
+ This will open the config editor where you can change any setting by double clicking on it or navigating to it with the arrow keys and hitting enter. On top of that, the config editor allows you to:
110
110
 
111
111
  - Automatically import items from your Storage Units
112
112
  - Manually Specify the number of items you own
@@ -0,0 +1,31 @@
1
+ cs2tracker/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ cs2tracker/__main__.py,sha256=Ub--oSMv48YzfWF1CZqYlkn1-HvZ7Bhxoc7urn1oY6o,249
3
+ cs2tracker/_version.py,sha256=B5GTyfnCcU_OVejGvc44yPuiU01UHMzbmUciqEKsU_4,513
4
+ cs2tracker/config.py,sha256=iab4WqKLl8-rfV7_MAd-96lfp_edMe6QQHW_m76FvD8,11011
5
+ cs2tracker/constants.py,sha256=9E1CPVEohDmV_F2PLLrPRmtYAHHVDimbUFfohhjci1c,6742
6
+ cs2tracker/logs.py,sha256=K6uLoGjhHTlb6HLKIj6C5mz6R6bnZgltA6xwNVJ7Z8Y,6004
7
+ cs2tracker/main.py,sha256=9Jjn8-Hv9AzQLYjCjA6pwAmThE4HH1u3akF_c7atyl4,1046
8
+ cs2tracker/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ cs2tracker/app/app.py,sha256=kUiZdrkIqwbbbf4h2MTQ04hHvO6kfGihj5M1yKF0VSE,10577
10
+ cs2tracker/app/editor_frame.py,sha256=eulOzywwMXoHhu3AQ6pEypY32zK6kpoQZDswRERioMU,26089
11
+ cs2tracker/app/history_frame.py,sha256=_QJdomghK10k0q0q4hyNm_RtzrbWa5jw3Rbq72smGT0,2001
12
+ cs2tracker/app/scraper_frame.py,sha256=GDRytrNe1gUMXA_ZcbTEcOVx2N81pQJGGYI-Fv-ww6o,4233
13
+ cs2tracker/data/config.ini,sha256=qTmBG8Qo6BYEBA9qEb_D8c_TW8Lf_0MuusmGjdP6S1Q,15093
14
+ cs2tracker/data/convert_inventory.js,sha256=CbMXnw52cOxhN2Ee2-kP3qX6pgwxrv4pi6_I1Acci88,7772
15
+ cs2tracker/data/get_inventory.js,sha256=OMntZhDE_x4qsqCWqcU7SqDFuMWz_VK_umxt_4i2znQ,6304
16
+ cs2tracker/data/output.csv,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
+ cs2tracker/scraper/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
+ cs2tracker/scraper/background_task.py,sha256=pCLMt3XVVduaqYRdXtV3HIHinL3WnULP4Riwc3tL9f0,3686
19
+ cs2tracker/scraper/discord_notifier.py,sha256=1DaGQVHfeE9AsQ09uKvVyDEbIdQwhVEh7MonJK0n0So,3074
20
+ cs2tracker/scraper/parser.py,sha256=FGbR60WISxnlJ8T7srhB5lU4m73rcfo9fjYDMpLaiKs,6770
21
+ cs2tracker/scraper/scraper.py,sha256=D_NlJTZyIXDhEkHGqjyrc5LYakcfiaT7Dl7dWLSC02k,11377
22
+ cs2tracker/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
+ cs2tracker/util/currency_conversion.py,sha256=h_VdipCFPzcgybWZ2VYknOd1VjHiqsFbEVed95FOe7U,2199
24
+ cs2tracker/util/padded_console.py,sha256=ZEbU5MxDA7Xbd-_SXIyezwGvb349OgAtjNPjFT_wrZw,1774
25
+ cs2tracker/util/tkinter.py,sha256=yR6rog4P7t_rDgH6ctssfQeJYFbqFXboiRv1V9c1vk4,1518
26
+ cs2tracker-2.1.15.dist-info/licenses/LICENSE,sha256=doPNswWMPXbkhplb9cnZLwJoqqS72pJPhkSib8kIF08,19122
27
+ cs2tracker-2.1.15.dist-info/METADATA,sha256=VVNiDycjDEoGrL5dVChE5Pm0tMRI9jcJf15lh75CWvg,5657
28
+ cs2tracker-2.1.15.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
29
+ cs2tracker-2.1.15.dist-info/entry_points.txt,sha256=K8IwDIkg8QztSB9g9c89B9jR_2pG4QyJGrNs4z5RcZw,63
30
+ cs2tracker-2.1.15.dist-info/top_level.txt,sha256=2HB4xDDOxaU5BDc_yvdi9UlYLgL768n8aR-hRhFM6VQ,11
31
+ cs2tracker-2.1.15.dist-info/RECORD,,
@@ -1,255 +0,0 @@
1
- import ctypes
2
- import tkinter as tk
3
- from shutil import copy
4
- from tkinter import messagebox, ttk
5
- from tkinter.filedialog import askopenfilename, asksaveasfile
6
- from typing import cast
7
-
8
- import matplotlib.pyplot as plt
9
- import sv_ttk
10
- from matplotlib.axes import Axes
11
- from matplotlib.dates import DateFormatter
12
-
13
- from cs2tracker.app.editor_frame import ConfigEditorFrame
14
- from cs2tracker.app.scraper_frame import ScraperFrame
15
- from cs2tracker.constants import ICON_FILE, OS, OUTPUT_FILE, OSType
16
- from cs2tracker.scraper import BackgroundTask, Scraper
17
- from cs2tracker.util import PriceLogs
18
- from cs2tracker.util.validated_config import get_config
19
-
20
- APPLICATION_NAME = "CS2Tracker"
21
- WINDOW_SIZE = "630x335"
22
- DARK_THEME = True
23
-
24
- SCRAPER_WINDOW_TITLE = "CS2Tracker Scraper"
25
- SCRAPER_WINDOW_SIZE = "900x750"
26
-
27
- CONFIG_EDITOR_TITLE = "Config Editor"
28
- CONFIG_EDITOR_SIZE = "900x750"
29
-
30
-
31
- config = get_config()
32
-
33
-
34
- class Application:
35
- def __init__(self):
36
- self.scraper = Scraper()
37
- self.application_window = None
38
-
39
- def run(self):
40
- """Run the main application window with buttons for scraping prices, editing the
41
- configuration, showing history in a chart, and editing the log file.
42
- """
43
- self.application_window = self._configure_window()
44
-
45
- if DARK_THEME:
46
- sv_ttk.use_dark_theme()
47
- else:
48
- sv_ttk.use_light_theme()
49
-
50
- self.application_window.mainloop()
51
-
52
- def _add_button(self, frame, text, command, row):
53
- """Create and style a button for the button frame."""
54
- grid_pos = {"row": row, "column": 0, "sticky": "ew", "padx": 10, "pady": 10}
55
- button = ttk.Button(frame, text=text, command=command)
56
- button.grid(**grid_pos)
57
-
58
- def _configure_button_frame(self, main_frame):
59
- """Configure the button frame of the application main frame."""
60
- button_frame = ttk.Frame(main_frame, style="Card.TFrame", padding=15)
61
- button_frame.columnconfigure(0, weight=1)
62
- button_frame.grid(row=0, column=0, padx=10, pady=(7, 20), sticky="nsew")
63
-
64
- self._add_button(button_frame, "Run!", self.scrape_prices, 0)
65
- self._add_button(button_frame, "Edit Config", self._edit_config, 1)
66
- self._add_button(button_frame, "Show History", self._draw_plot, 2)
67
- self._add_button(button_frame, "Export History", self._export_log_file, 3)
68
- self._add_button(button_frame, "Import History", self._import_log_file, 4)
69
-
70
- def _add_checkbox(
71
- self, frame, text, variable, command, row
72
- ): # pylint: disable=too-many-arguments,too-many-positional-arguments
73
- """Create and style a checkbox for the checkbox frame."""
74
- grid_pos = {"row": row, "column": 0, "sticky": "w", "padx": (10, 0), "pady": 5}
75
- checkbox = ttk.Checkbutton(
76
- frame,
77
- text=text,
78
- variable=variable,
79
- command=command,
80
- style="Switch.TCheckbutton",
81
- )
82
- checkbox.grid(**grid_pos)
83
-
84
- def _configure_checkbox_frame(self, main_frame):
85
- """Configure the checkbox frame for background tasks and settings."""
86
- checkbox_frame = ttk.LabelFrame(main_frame, text="Settings", padding=15)
87
- checkbox_frame.grid(row=0, column=1, padx=10, pady=(0, 20), sticky="nsew")
88
-
89
- background_checkbox_value = tk.BooleanVar(value=BackgroundTask.identify())
90
- self._add_checkbox(
91
- checkbox_frame,
92
- "Background Task",
93
- background_checkbox_value,
94
- lambda: self._toggle_background_task(background_checkbox_value.get()),
95
- 0,
96
- )
97
-
98
- discord_webhook_checkbox_value = tk.BooleanVar(
99
- value=config.getboolean("App Settings", "discord_notifications", fallback=False)
100
- )
101
- self._add_checkbox(
102
- checkbox_frame,
103
- "Discord Notifications",
104
- discord_webhook_checkbox_value,
105
- lambda: discord_webhook_checkbox_value.set(
106
- self._toggle_discord_webhook(discord_webhook_checkbox_value.get())
107
- ),
108
- 1,
109
- )
110
-
111
- use_proxy_checkbox_value = tk.BooleanVar(
112
- value=config.getboolean("App Settings", "use_proxy", fallback=False)
113
- )
114
- self._add_checkbox(
115
- checkbox_frame,
116
- "Proxy Requests",
117
- use_proxy_checkbox_value,
118
- lambda: use_proxy_checkbox_value.set(
119
- self._toggle_use_proxy(use_proxy_checkbox_value.get())
120
- ),
121
- 2,
122
- )
123
-
124
- # pylint: disable=attribute-defined-outside-init
125
- self.dark_theme_checkbox_value = tk.BooleanVar(value=DARK_THEME)
126
- self._add_checkbox(
127
- checkbox_frame, "Dark Theme", self.dark_theme_checkbox_value, sv_ttk.toggle_theme, 3
128
- )
129
-
130
- def _configure_main_frame(self, window):
131
- """Configure the main frame of the application window with buttons and
132
- checkboxes.
133
- """
134
- main_frame = ttk.Frame(window, padding=15)
135
- main_frame.columnconfigure(0, weight=1)
136
- main_frame.columnconfigure(1, weight=1)
137
- main_frame.rowconfigure(0, weight=1)
138
-
139
- self._configure_button_frame(main_frame)
140
- self._configure_checkbox_frame(main_frame)
141
-
142
- main_frame.pack(expand=True, fill="both")
143
-
144
- def _configure_window(self):
145
- """Configure the main application window UI and add buttons for the main
146
- functionalities.
147
- """
148
- window = tk.Tk()
149
- window.title(APPLICATION_NAME)
150
- window.geometry(WINDOW_SIZE)
151
-
152
- if OS == OSType.WINDOWS:
153
- app_id = "cs2tracker.unique.id"
154
- ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(app_id)
155
-
156
- icon = tk.PhotoImage(file=ICON_FILE)
157
- window.wm_iconphoto(True, icon)
158
-
159
- self._configure_main_frame(window)
160
-
161
- return window
162
-
163
- def scrape_prices(self):
164
- """Scrape prices from the configured sources, print the total, and save the
165
- results to a file.
166
- """
167
- scraper_window = tk.Toplevel(self.application_window)
168
- scraper_window.geometry(SCRAPER_WINDOW_SIZE)
169
- scraper_window.title(SCRAPER_WINDOW_TITLE)
170
-
171
- run_frame = ScraperFrame(
172
- scraper_window,
173
- self.scraper,
174
- sheet_size=SCRAPER_WINDOW_SIZE,
175
- dark_theme=self.dark_theme_checkbox_value.get(),
176
- )
177
- run_frame.pack(expand=True, fill="both")
178
- run_frame.start()
179
-
180
- def _edit_config(self):
181
- """Open a new window with a config editor GUI."""
182
- config_editor_window = tk.Toplevel(self.application_window)
183
- config_editor_window.geometry(CONFIG_EDITOR_SIZE)
184
- config_editor_window.title(CONFIG_EDITOR_TITLE)
185
-
186
- editor_frame = ConfigEditorFrame(config_editor_window)
187
- editor_frame.pack(expand=True, fill="both")
188
-
189
- def _draw_plot(self):
190
- """Draw a plot of the scraped prices over time."""
191
- dates, usd_prices, eur_prices = PriceLogs.read()
192
-
193
- fig, ax_raw = plt.subplots(figsize=(10, 8), num="CS2Tracker Price History")
194
- fig.suptitle("CS2Tracker Price History", fontsize=16)
195
- fig.autofmt_xdate()
196
-
197
- ax = cast(Axes, ax_raw)
198
- ax.plot(dates, usd_prices, label="Dollars")
199
- ax.plot(dates, eur_prices, label="Euros")
200
- ax.legend()
201
- date_formatter = DateFormatter("%Y-%m-%d")
202
- ax.xaxis.set_major_formatter(date_formatter)
203
-
204
- plt.show()
205
-
206
- def _export_log_file(self):
207
- """Lets the user export the log file to a different location."""
208
- export_path = asksaveasfile(
209
- title="Export Log File",
210
- defaultextension=".csv",
211
- filetypes=[("CSV File", "*.csv")],
212
- )
213
- if export_path:
214
- copy(OUTPUT_FILE, export_path.name)
215
-
216
- def _import_log_file(self):
217
- """Lets the user import a log file from a different location."""
218
- import_path = askopenfilename(
219
- title="Import Log File",
220
- defaultextension=".csv",
221
- filetypes=[("CSV files", "*.csv")],
222
- )
223
- if not PriceLogs.validate_file(import_path):
224
- return
225
- copy(import_path, OUTPUT_FILE)
226
-
227
- def _toggle_background_task(self, enabled: bool):
228
- """Toggle whether a daily price calculation should run in the background."""
229
- BackgroundTask.toggle(enabled)
230
-
231
- def _toggle_use_proxy(self, enabled: bool):
232
- """Toggle whether the scraper should use proxy servers for requests."""
233
- proxy_api_key = config.get("User Settings", "proxy_api_key", fallback=None)
234
- if not proxy_api_key and enabled:
235
- messagebox.showerror(
236
- "Config Error",
237
- "You need to enter a valid crawlbase API key into the configuration to use this feature.",
238
- )
239
- return False
240
-
241
- config.toggle_use_proxy(enabled)
242
- return True
243
-
244
- def _toggle_discord_webhook(self, enabled: bool):
245
- """Toggle whether the scraper should send notifications to a Discord webhook."""
246
- discord_webhook_url = config.get("User Settings", "discord_webhook_url", fallback=None)
247
- if not discord_webhook_url and enabled:
248
- messagebox.showerror(
249
- "Config Error",
250
- "You need to enter a valid Discord webhook URL into the configuration to use this feature.",
251
- )
252
- return False
253
-
254
- config.toggle_discord_webhook(enabled)
255
- return True
@@ -1,100 +0,0 @@
1
- import csv
2
- from datetime import datetime
3
-
4
- from cs2tracker.constants import OUTPUT_FILE
5
-
6
-
7
- class PriceLogs:
8
- @classmethod
9
- def _append_latest_calculation(cls, date, usd_total, eur_total):
10
- """Append the first price calculation of the day."""
11
- with open(OUTPUT_FILE, "a", newline="", encoding="utf-8") as price_logs:
12
- price_logs_writer = csv.writer(price_logs)
13
- price_logs_writer.writerow([date, f"{usd_total:.2f}$", f"{eur_total:.2f}€"])
14
-
15
- @classmethod
16
- def _replace_latest_calculation(cls, date, usd_total, eur_total):
17
- """Replace the last calculation of today with the most recent one of today."""
18
- with open(OUTPUT_FILE, "r+", newline="", encoding="utf-8") as price_logs:
19
- price_logs_reader = csv.reader(price_logs)
20
- rows = list(price_logs_reader)
21
- rows_without_today = rows[:-1]
22
- price_logs.seek(0)
23
- price_logs.truncate()
24
-
25
- price_logs_writer = csv.writer(price_logs)
26
- price_logs_writer.writerows(rows_without_today)
27
- price_logs_writer.writerow([date, f"{usd_total:.2f}$", f"{eur_total:.2f}€"])
28
-
29
- @classmethod
30
- def save(cls, usd_total, eur_total):
31
- """
32
- Save the current date and total prices in USD and EUR to a CSV file.
33
-
34
- This will append a new entry to the output file if no entry has been made for
35
- today.
36
-
37
- :param usd_total: The total price in USD to save.
38
- :param eur_total: The total price in EUR to save.
39
- :raises FileNotFoundError: If the output file does not exist.
40
- :raises IOError: If there is an error writing to the output file.
41
- """
42
- with open(OUTPUT_FILE, "r", encoding="utf-8") as price_logs:
43
- price_logs_reader = csv.reader(price_logs)
44
- rows = list(price_logs_reader)
45
- last_log_date, _, _ = rows[-1] if rows else ("", "", "")
46
-
47
- today = datetime.now().strftime("%Y-%m-%d")
48
- if last_log_date != today:
49
- cls._append_latest_calculation(today, usd_total, eur_total)
50
- else:
51
- cls._replace_latest_calculation(today, usd_total, eur_total)
52
-
53
- @classmethod
54
- def read(cls):
55
- """
56
- Parse the output file to extract dates, dollar prices, and euro prices. This
57
- data is used for drawing the plot of past prices.
58
-
59
- :return: A tuple containing three lists: dates, dollar prices, and euro prices.
60
- :raises FileNotFoundError: If the output file does not exist.
61
- :raises IOError: If there is an error reading the output file.
62
- """
63
- dates, usd_prices, eur_prices = [], [], []
64
- with open(OUTPUT_FILE, "r", encoding="utf-8") as price_logs:
65
- price_logs_reader = csv.reader(price_logs)
66
- for row in price_logs_reader:
67
- date, price_usd, price_eur = row
68
- date = datetime.strptime(date, "%Y-%m-%d")
69
- price_usd = float(price_usd.rstrip("$"))
70
- price_eur = float(price_eur.rstrip("€"))
71
-
72
- dates.append(date)
73
- usd_prices.append(price_usd)
74
- eur_prices.append(price_eur)
75
-
76
- return dates, usd_prices, eur_prices
77
-
78
- @classmethod
79
- def validate_file(cls, log_file_path):
80
- """
81
- Ensures that the provided price log file has the right format. This should be
82
- used before importing a price log file to ensure it is valid.
83
-
84
- :param log_file_path: The path to the price log file to validate.
85
- :return: True if the price log file is valid, False otherwise.
86
- """
87
- try:
88
- with open(log_file_path, "r", encoding="utf-8") as price_logs:
89
- price_logs_reader = csv.reader(price_logs)
90
- for row in price_logs_reader:
91
- date_str, price_usd, price_eur = row
92
- datetime.strptime(date_str, "%Y-%m-%d")
93
- float(price_usd.rstrip("$"))
94
- float(price_eur.rstrip("€"))
95
- except (FileNotFoundError, IOError, ValueError, TypeError):
96
- return False
97
- except Exception:
98
- return False
99
-
100
- return True
@@ -1,164 +0,0 @@
1
- import json
2
- import re
3
- from configparser import ConfigParser
4
- from urllib.parse import quote
5
-
6
- from cs2tracker.constants import CAPSULE_INFO, CONFIG_FILE, INVENTORY_IMPORT_FILE
7
- from cs2tracker.util.padded_console import get_console
8
-
9
- STEAM_MARKET_LISTING_BASEURL_CS2 = "https://steamcommunity.com/market/listings/730/"
10
- STEAM_MARKET_LISTING_REGEX = r"^https://steamcommunity.com/market/listings/\d+/.+$"
11
-
12
- console = get_console()
13
-
14
-
15
- class ValidatedConfig(ConfigParser):
16
- def __init__(self):
17
- """Initialize the ValidatedConfig class."""
18
- super().__init__(delimiters=("~"), interpolation=None)
19
- self.optionxform = str # type: ignore
20
-
21
- self.valid = False
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)
29
- self._validate_config()
30
-
31
- def _validate_config_sections(self):
32
- """Validate that the configuration file has all required sections."""
33
- if not self.has_section("User Settings"):
34
- raise ValueError("Missing 'User Settings' section in the configuration file.")
35
- if not self.has_section("App Settings"):
36
- raise ValueError("Missing 'App Settings' section in the configuration file.")
37
- if not self.has_section("Custom Items"):
38
- raise ValueError("Missing 'Custom Items' section in the configuration file.")
39
- if not self.has_section("Cases"):
40
- raise ValueError("Missing 'Cases' section in the configuration file.")
41
- for capsule_section in CAPSULE_INFO:
42
- if not self.has_section(capsule_section):
43
- raise ValueError(f"Missing '{capsule_section}' section in the configuration file.")
44
-
45
- def _validate_config_values(self):
46
- """Validate that the configuration file has valid values for all sections."""
47
- try:
48
- for custom_item_href, custom_item_owned in self.items("Custom Items"):
49
- if not re.match(STEAM_MARKET_LISTING_REGEX, custom_item_href):
50
- raise ValueError(
51
- f"Invalid Steam market listing URL in 'Custom Items' section: {custom_item_href}"
52
- )
53
-
54
- if int(custom_item_owned) < 0:
55
- raise ValueError(
56
- f"Invalid value in 'Custom Items' section: {custom_item_href} = {custom_item_owned}"
57
- )
58
- for case_name, case_owned in self.items("Cases"):
59
- if int(case_owned) < 0:
60
- raise ValueError(
61
- f"Invalid value in 'Cases' section: {case_name} = {case_owned}"
62
- )
63
- for capsule_section in CAPSULE_INFO:
64
- for capsule_name, capsule_owned in self.items(capsule_section):
65
- if int(capsule_owned) < 0:
66
- raise ValueError(
67
- f"Invalid value in '{capsule_section}' section: {capsule_name} = {capsule_owned}"
68
- )
69
- except ValueError as error:
70
- if "Invalid " in str(error):
71
- raise
72
- raise ValueError("Invalid value type. All values must be integers.") from error
73
-
74
- def _validate_config(self):
75
- """
76
- Validate the configuration file to ensure all required sections exist with the
77
- right values.
78
-
79
- :raises ValueError: If any required section is missing or if any value is
80
- invalid.
81
- """
82
- try:
83
- self._validate_config_sections()
84
- self._validate_config_values()
85
- self.valid = True
86
- except ValueError as error:
87
- console.error(f"Config error: {error}")
88
- self.valid = False
89
- self.last_error = error
90
-
91
- def write_to_file(self):
92
- """Validate the current configuration and write it to the configuration file if
93
- it is valid.
94
- """
95
- self._validate_config()
96
-
97
- if self.valid:
98
- with open(CONFIG_FILE, "w", encoding="utf-8") as config_file:
99
- self.write(config_file)
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
-
131
- def toggle_use_proxy(self, enabled: bool):
132
- """
133
- Toggle the use of proxies for requests. This will update the configuration file.
134
-
135
- :param enabled: If True, proxies will be used; if False, they will not be used.
136
- """
137
- self.set("App Settings", "use_proxy", str(enabled))
138
- self.write_to_file()
139
-
140
- console.print(
141
- f"[bold green]{'[+] Enabled' if enabled else '[-] Disabled'} proxy usage for requests."
142
- )
143
-
144
- def toggle_discord_webhook(self, enabled: bool):
145
- """
146
- Toggle the use of a Discord webhook to notify users of price calculations.
147
-
148
- :param enabled: If True, the webhook will be used; if False, it will not be
149
- used.
150
- """
151
- self.set("App Settings", "discord_notifications", str(enabled))
152
- self.write_to_file()
153
-
154
- console.print(
155
- f"[bold green]{'[+] Enabled' if enabled else '[-] Disabled'} Discord webhook notifications."
156
- )
157
-
158
-
159
- config = ValidatedConfig()
160
-
161
-
162
- def get_config():
163
- """Accessor function to retrieve the current configuration."""
164
- return config