cs2tracker 2.1.9__tar.gz → 2.1.10__tar.gz

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.

Files changed (36) hide show
  1. {cs2tracker-2.1.9 → cs2tracker-2.1.10}/.isort.cfg +1 -1
  2. {cs2tracker-2.1.9 → cs2tracker-2.1.10}/.pylintrc +2 -1
  3. {cs2tracker-2.1.9 → cs2tracker-2.1.10}/PKG-INFO +7 -4
  4. {cs2tracker-2.1.9 → cs2tracker-2.1.10}/README.md +5 -3
  5. {cs2tracker-2.1.9 → cs2tracker-2.1.10}/cs2tracker/_version.py +2 -2
  6. cs2tracker-2.1.10/cs2tracker/application.py +516 -0
  7. cs2tracker-2.1.10/cs2tracker/background_task.py +109 -0
  8. {cs2tracker-2.1.9 → cs2tracker-2.1.10}/cs2tracker/constants.py +2 -3
  9. cs2tracker-2.1.10/cs2tracker/data/config.ini +204 -0
  10. cs2tracker-2.1.10/cs2tracker/discord_notifier.py +87 -0
  11. cs2tracker-2.1.10/cs2tracker/price_logs.py +100 -0
  12. cs2tracker-2.1.10/cs2tracker/scraper.py +317 -0
  13. cs2tracker-2.1.10/cs2tracker/validated_config.py +117 -0
  14. {cs2tracker-2.1.9 → cs2tracker-2.1.10}/cs2tracker.egg-info/PKG-INFO +7 -4
  15. {cs2tracker-2.1.9 → cs2tracker-2.1.10}/cs2tracker.egg-info/SOURCES.txt +4 -0
  16. {cs2tracker-2.1.9 → cs2tracker-2.1.10}/cs2tracker.egg-info/requires.txt +1 -0
  17. {cs2tracker-2.1.9 → cs2tracker-2.1.10}/requirements.txt +1 -0
  18. {cs2tracker-2.1.9 → cs2tracker-2.1.10}/setup.cfg +1 -0
  19. cs2tracker-2.1.9/cs2tracker/application.py +0 -270
  20. cs2tracker-2.1.9/cs2tracker/data/config.ini +0 -205
  21. cs2tracker-2.1.9/cs2tracker/scraper.py +0 -637
  22. {cs2tracker-2.1.9 → cs2tracker-2.1.10}/.flake8 +0 -0
  23. {cs2tracker-2.1.9 → cs2tracker-2.1.10}/.gitignore +0 -0
  24. {cs2tracker-2.1.9 → cs2tracker-2.1.10}/.pre-commit-config.yaml +0 -0
  25. {cs2tracker-2.1.9 → cs2tracker-2.1.10}/LICENSE.md +0 -0
  26. {cs2tracker-2.1.9 → cs2tracker-2.1.10}/MANIFEST.in +0 -0
  27. {cs2tracker-2.1.9 → cs2tracker-2.1.10}/assets/icon.png +0 -0
  28. {cs2tracker-2.1.9 → cs2tracker-2.1.10}/cs2tracker/__init__.py +0 -0
  29. {cs2tracker-2.1.9 → cs2tracker-2.1.10}/cs2tracker/__main__.py +0 -0
  30. {cs2tracker-2.1.9 → cs2tracker-2.1.10}/cs2tracker/data/output.csv +0 -0
  31. {cs2tracker-2.1.9 → cs2tracker-2.1.10}/cs2tracker/main.py +0 -0
  32. {cs2tracker-2.1.9 → cs2tracker-2.1.10}/cs2tracker/padded_console.py +0 -0
  33. {cs2tracker-2.1.9 → cs2tracker-2.1.10}/cs2tracker.egg-info/dependency_links.txt +0 -0
  34. {cs2tracker-2.1.9 → cs2tracker-2.1.10}/cs2tracker.egg-info/entry_points.txt +0 -0
  35. {cs2tracker-2.1.9 → cs2tracker-2.1.10}/cs2tracker.egg-info/top_level.txt +0 -0
  36. {cs2tracker-2.1.9 → cs2tracker-2.1.10}/pyproject.toml +0 -0
@@ -1,2 +1,2 @@
1
1
  [settings]
2
- known_third_party = bs4,currency_converter,matplotlib,requests,rich,tenacity,urllib3
2
+ known_third_party = bs4,currency_converter,matplotlib,requests,rich,sv_ttk,tenacity,urllib3
@@ -7,4 +7,5 @@ disable=
7
7
  consider-using-with,
8
8
  import-error,
9
9
  no-else-return,
10
- broad-exception-caught
10
+ broad-exception-caught,
11
+ too-few-public-methods
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cs2tracker
3
- Version: 2.1.9
3
+ Version: 2.1.10
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
@@ -19,6 +19,7 @@ Requires-Dist: Requests==2.31.0
19
19
  Requires-Dist: rich==13.6.0
20
20
  Requires-Dist: tenacity==8.2.2
21
21
  Requires-Dist: urllib3==2.1.0
22
+ Requires-Dist: sv_ttk==2.6.1
22
23
  Dynamic: license-file
23
24
 
24
25
  <div align="center">
@@ -46,7 +47,7 @@ Dynamic: license-file
46
47
 
47
48
  ### Setup
48
49
 
49
- #### Windows Executable _(no color support)_
50
+ #### Windows Executable
50
51
 
51
52
  - Simply [download the latest executable](https://github.com/ashiven/cs2tracker/releases/latest/download/cs2tracker-windows.zip) and run it.
52
53
 
@@ -67,11 +68,13 @@ Dynamic: license-file
67
68
  ### Options
68
69
 
69
70
  - `Run!` to gather the current market prices of your items and calculate the total amount in USD and EUR.
70
- - `Edit Config` to specify the numbers of items owned in the config file. You can also add items other than cases and sticker capsules following the format in the `Custom Items` section. (item_name = item_owned item_page)
71
+ - `Edit Config` to specify the numbers of items owned in the configuration. You can also add items other than cases and sticker capsules via `Add Custom Item`
72
+ - `Reset Config` to reset the configuration to its original state. This will remove any custom items you have added and reset the number of items owned for all items.
71
73
  - `Show History` to see a price chart consisting of past calculations. A new data point is generated once a day upon running the program.
74
+ - `Export / Import History` to export the price history to a CSV file or import it from a CSV file. This may be used to back up your history data or perform further analysis on it.
72
75
  - `Daily Background Calculations` to automatically run a daily calculation of your investment in the background and save the results such that they can later be viewed via `Show History`.
73
76
  - `Receive Discord Notifications` to receive a notification on your Discord server when the program has finished calculating your investment. 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 file.
74
- - `Proxy Requests` to prevent your requests from being rate limited by the steamcommunity server. You need to register for a free API key on [Crawlbase](crawlbase.com) and enter it into the `api_key` field in the config file.
77
+ - `Proxy Requests` to prevent your requests from being rate limited by the steamcommunity server. You need to register for a free API key on [Crawlbase](crawlbase.com) and enter it into the `proxy_api_key` field in the `User Settings` configuration section.
75
78
 
76
79
  ---
77
80
 
@@ -23,7 +23,7 @@
23
23
 
24
24
  ### Setup
25
25
 
26
- #### Windows Executable _(no color support)_
26
+ #### Windows Executable
27
27
 
28
28
  - Simply [download the latest executable](https://github.com/ashiven/cs2tracker/releases/latest/download/cs2tracker-windows.zip) and run it.
29
29
 
@@ -44,11 +44,13 @@
44
44
  ### Options
45
45
 
46
46
  - `Run!` to gather the current market prices of your items and calculate the total amount in USD and EUR.
47
- - `Edit Config` to specify the numbers of items owned in the config file. You can also add items other than cases and sticker capsules following the format in the `Custom Items` section. (item_name = item_owned item_page)
47
+ - `Edit Config` to specify the numbers of items owned in the configuration. You can also add items other than cases and sticker capsules via `Add Custom Item`
48
+ - `Reset Config` to reset the configuration to its original state. This will remove any custom items you have added and reset the number of items owned for all items.
48
49
  - `Show History` to see a price chart consisting of past calculations. A new data point is generated once a day upon running the program.
50
+ - `Export / Import History` to export the price history to a CSV file or import it from a CSV file. This may be used to back up your history data or perform further analysis on it.
49
51
  - `Daily Background Calculations` to automatically run a daily calculation of your investment in the background and save the results such that they can later be viewed via `Show History`.
50
52
  - `Receive Discord Notifications` to receive a notification on your Discord server when the program has finished calculating your investment. 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 file.
51
- - `Proxy Requests` to prevent your requests from being rate limited by the steamcommunity server. You need to register for a free API key on [Crawlbase](crawlbase.com) and enter it into the `api_key` field in the config file.
53
+ - `Proxy Requests` to prevent your requests from being rate limited by the steamcommunity server. You need to register for a free API key on [Crawlbase](crawlbase.com) and enter it into the `proxy_api_key` field in the `User Settings` configuration section.
52
54
 
53
55
  ---
54
56
 
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '2.1.9'
21
- __version_tuple__ = version_tuple = (2, 1, 9)
20
+ __version__ = version = '2.1.10'
21
+ __version_tuple__ = version_tuple = (2, 1, 10)
@@ -0,0 +1,516 @@
1
+ import ctypes
2
+ import tkinter as tk
3
+ from shutil import copy
4
+ from subprocess import Popen
5
+ from threading import Thread
6
+ from tkinter import messagebox, ttk
7
+ from tkinter.filedialog import askopenfilename, asksaveasfile
8
+ from typing import cast
9
+
10
+ import matplotlib.pyplot as plt
11
+ import sv_ttk
12
+ from matplotlib.axes import Axes
13
+ from matplotlib.dates import DateFormatter
14
+
15
+ from cs2tracker.background_task import BackgroundTask
16
+ from cs2tracker.constants import (
17
+ CONFIG_FILE,
18
+ CONFIG_FILE_BACKUP,
19
+ ICON_FILE,
20
+ OS,
21
+ OUTPUT_FILE,
22
+ POWERSHELL_COLORIZE_OUTPUT,
23
+ PYTHON_EXECUTABLE,
24
+ RUNNING_IN_EXE,
25
+ OSType,
26
+ )
27
+ from cs2tracker.price_logs import PriceLogs
28
+ from cs2tracker.scraper import Scraper
29
+
30
+ APPLICATION_NAME = "CS2Tracker"
31
+ WINDOW_SIZE = "630x380"
32
+
33
+ CONFIG_EDITOR_TITLE = "Config Editor"
34
+ CONFIG_EDITOR_SIZE = "800x750"
35
+
36
+ NEW_CUSTOM_ITEM_TITLE = "Add Custom Item"
37
+ NEW_CUSTOM_ITEM_SIZE = "500x200"
38
+
39
+ SCRAPER_WINDOW_HEIGHT = 40
40
+ SCRAPER_WINDOW_WIDTH = 120
41
+ SCRAPER_WINDOW_BACKGROUND_COLOR = "Black"
42
+
43
+
44
+ class Application:
45
+ def __init__(self):
46
+ self.scraper = Scraper()
47
+ self.application_window = None
48
+ self.config_editor_window = None
49
+
50
+ def run(self):
51
+ """Run the main application window with buttons for scraping prices, editing the
52
+ configuration, showing history in a chart, and editing the log file.
53
+ """
54
+ self.application_window = self._configure_window()
55
+
56
+ sv_ttk.use_dark_theme()
57
+
58
+ self.application_window.mainloop()
59
+
60
+ def _add_button(self, frame, text, command, row):
61
+ """Create and style a button for the button frame."""
62
+ grid_pos = {"row": row, "column": 0, "sticky": "ew", "padx": 10, "pady": 10}
63
+ button = ttk.Button(frame, text=text, command=command)
64
+ button.grid(**grid_pos)
65
+
66
+ def _configure_button_frame(self, main_frame):
67
+ """Configure the button frame of the application main frame."""
68
+ button_frame = ttk.Frame(main_frame, style="Card.TFrame", padding=15)
69
+ button_frame.columnconfigure(0, weight=1)
70
+ button_frame.grid(row=0, column=0, padx=10, pady=(7, 20), sticky="nsew")
71
+
72
+ self._add_button(button_frame, "Run!", self.scrape_prices, 0)
73
+ self._add_button(button_frame, "Edit Config", self._edit_config, 1)
74
+ self._add_button(button_frame, "Reset Config", self._reset_config, 2)
75
+ self._add_button(button_frame, "Show History", self._draw_plot, 3)
76
+ self._add_button(button_frame, "Export History", self._export_log_file, 4)
77
+ self._add_button(button_frame, "Import History", self._import_log_file, 5)
78
+
79
+ def _add_checkbox(
80
+ self, frame, text, variable, command, row
81
+ ): # pylint: disable=too-many-arguments,too-many-positional-arguments
82
+ """Create and style a checkbox for the checkbox frame."""
83
+ grid_pos = {"row": row, "column": 0, "sticky": "w", "padx": (10, 0), "pady": 5}
84
+ checkbox = ttk.Checkbutton(
85
+ frame,
86
+ text=text,
87
+ variable=variable,
88
+ command=command,
89
+ style="Switch.TCheckbutton",
90
+ )
91
+ checkbox.grid(**grid_pos)
92
+
93
+ def _configure_checkbox_frame(self, main_frame):
94
+ """Configure the checkbox frame for background tasks and settings."""
95
+ checkbox_frame = ttk.LabelFrame(main_frame, text="Settings", padding=15)
96
+ checkbox_frame.grid(row=0, column=1, padx=10, pady=(0, 20), sticky="nsew")
97
+
98
+ background_checkbox_value = tk.BooleanVar(value=BackgroundTask.identify())
99
+ self._add_checkbox(
100
+ checkbox_frame,
101
+ "Background Task",
102
+ background_checkbox_value,
103
+ lambda: self._toggle_background_task(background_checkbox_value.get()),
104
+ 0,
105
+ )
106
+
107
+ discord_webhook_checkbox_value = tk.BooleanVar(
108
+ value=self.scraper.config.getboolean(
109
+ "App Settings", "discord_notifications", fallback=False
110
+ )
111
+ )
112
+ self._add_checkbox(
113
+ checkbox_frame,
114
+ "Discord Notifications",
115
+ discord_webhook_checkbox_value,
116
+ lambda: discord_webhook_checkbox_value.set(
117
+ self._toggle_discord_webhook(discord_webhook_checkbox_value.get())
118
+ ),
119
+ 1,
120
+ )
121
+
122
+ use_proxy_checkbox_value = tk.BooleanVar(
123
+ value=self.scraper.config.getboolean("App Settings", "use_proxy", fallback=False)
124
+ )
125
+ self._add_checkbox(
126
+ checkbox_frame,
127
+ "Proxy Requests",
128
+ use_proxy_checkbox_value,
129
+ lambda: use_proxy_checkbox_value.set(
130
+ self._toggle_use_proxy(use_proxy_checkbox_value.get())
131
+ ),
132
+ 2,
133
+ )
134
+
135
+ dark_theme_checkbox_value = tk.BooleanVar(value=True)
136
+ self._add_checkbox(
137
+ checkbox_frame, "Dark Theme", dark_theme_checkbox_value, sv_ttk.toggle_theme, 3
138
+ )
139
+
140
+ def _configure_main_frame(self, window):
141
+ """Configure the main frame of the application window with buttons and
142
+ checkboxes.
143
+ """
144
+ main_frame = ttk.Frame(window, padding=15)
145
+ main_frame.columnconfigure(0, weight=1)
146
+ main_frame.columnconfigure(1, weight=1)
147
+ main_frame.rowconfigure(0, weight=1)
148
+
149
+ self._configure_button_frame(main_frame)
150
+ self._configure_checkbox_frame(main_frame)
151
+
152
+ main_frame.pack(expand=True, fill="both")
153
+
154
+ def _configure_window(self):
155
+ """Configure the main application window UI and add buttons for the main
156
+ functionalities.
157
+ """
158
+ window = tk.Tk()
159
+ window.title(APPLICATION_NAME)
160
+ window.geometry(WINDOW_SIZE)
161
+
162
+ if OS == OSType.WINDOWS:
163
+ app_id = "cs2tracker.unique.id"
164
+ ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(app_id)
165
+
166
+ icon = tk.PhotoImage(file=ICON_FILE)
167
+ window.wm_iconphoto(True, icon)
168
+
169
+ self._configure_main_frame(window)
170
+
171
+ return window
172
+
173
+ def _construct_scraper_command_windows(self):
174
+ """Construct the command to run the scraper in a new window for Windows."""
175
+ get_size = "$size = $Host.UI.RawUI.WindowSize;"
176
+ set_size = "$Host.UI.RawUI.WindowSize = $size;"
177
+ set_window_title = f"$Host.UI.RawUI.WindowTitle = '{APPLICATION_NAME}';"
178
+ set_window_width = (
179
+ f"$size.Width = [Math]::Min({SCRAPER_WINDOW_WIDTH}, $Host.UI.RawUI.BufferSize.Width);"
180
+ )
181
+ set_window_height = f"$size.Height = {SCRAPER_WINDOW_HEIGHT};"
182
+ set_background_color = (
183
+ f"$Host.UI.RawUI.BackgroundColor = '{SCRAPER_WINDOW_BACKGROUND_COLOR}';"
184
+ )
185
+ clear = "Clear-Host;"
186
+
187
+ if RUNNING_IN_EXE:
188
+ # The python executable is set as the executable itself in PyInstaller
189
+ scraper_cmd = f"{PYTHON_EXECUTABLE} --only-scrape | {POWERSHELL_COLORIZE_OUTPUT}"
190
+ else:
191
+ scraper_cmd = f"{PYTHON_EXECUTABLE} -m cs2tracker --only-scrape"
192
+
193
+ cmd = (
194
+ 'start powershell -NoExit -Command "& {'
195
+ + set_window_title
196
+ + get_size
197
+ + set_window_width
198
+ + set_window_height
199
+ + set_size
200
+ + set_background_color
201
+ + clear
202
+ + scraper_cmd
203
+ + '}"'
204
+ )
205
+ return cmd
206
+
207
+ def _construct_scraper_command(self):
208
+ """Construct the command to run the scraper in a new window."""
209
+ if OS == OSType.WINDOWS:
210
+ return self._construct_scraper_command_windows()
211
+ else:
212
+ # TODO: Implement for Linux
213
+ return ""
214
+
215
+ def scrape_prices(self):
216
+ """Scrape prices from the configured sources, print the total, and save the
217
+ results to a file.
218
+ """
219
+ if OS == OSType.WINDOWS:
220
+ scraper_cmd = self._construct_scraper_command()
221
+ Popen(scraper_cmd, shell=True)
222
+ else:
223
+ # TODO: implement external window for Linux
224
+ self.scraper.scrape_prices()
225
+
226
+ def _make_tree_editable(self, editor_frame, tree):
227
+ """
228
+ Add a binding to the treeview that allows double-clicking on a cell to edit its
229
+ value.
230
+
231
+ Source: https://stackoverflow.com/questions/75787251/create-an-editable-tkinter-treeview-with-keyword-connection
232
+ """
233
+
234
+ def set_cell_value(event):
235
+ def save_edit(event):
236
+ tree.set(row, column=column, value=event.widget.get())
237
+ event.widget.destroy()
238
+
239
+ try:
240
+ row = tree.identify_row(event.y)
241
+ column = tree.identify_column(event.x)
242
+ item_text = tree.set(row, column)
243
+ if item_text.strip() == "":
244
+ left_item_text = tree.item(row, "text")
245
+ # Don't allow editing of section headers
246
+ if any(left_item_text == section for section in self.scraper.config.sections()):
247
+ return
248
+ x, y, w, h = tree.bbox(row, column)
249
+ entryedit = ttk.Entry(editor_frame)
250
+ entryedit.place(x=x, y=y, width=w, height=h + 3) # type: ignore
251
+ entryedit.insert("end", item_text)
252
+ entryedit.bind("<Return>", save_edit)
253
+ entryedit.focus_set()
254
+ entryedit.grab_set()
255
+ except Exception:
256
+ pass
257
+
258
+ def destroy_entries(_):
259
+ """Destroy any entry widgets in the treeview when the mouse wheel is
260
+ used.
261
+ """
262
+ for widget in editor_frame.winfo_children():
263
+ if isinstance(widget, ttk.Entry):
264
+ widget.destroy()
265
+
266
+ def destroy_entry(event):
267
+ """Destroy the entry widget if the user clicks outside of it."""
268
+ if isinstance(event.widget, ttk.Entry):
269
+ event.widget.destroy()
270
+
271
+ tree.bind("<Double-1>", set_cell_value)
272
+ self.config_editor_window.bind("<MouseWheel>", destroy_entries) # type: ignore
273
+ self.config_editor_window.bind("<Button-1>", destroy_entry) # type: ignore
274
+
275
+ def _configure_treeview(self, editor_frame):
276
+ """Add a treeview to the editor frame to display and edit configuration
277
+ options.
278
+ """
279
+ scrollbar = ttk.Scrollbar(editor_frame)
280
+ scrollbar.pack(side="right", fill="y", padx=(5, 0))
281
+
282
+ tree = ttk.Treeview(
283
+ editor_frame,
284
+ columns=(1,),
285
+ height=10,
286
+ selectmode="browse",
287
+ yscrollcommand=scrollbar.set,
288
+ )
289
+ scrollbar.config(command=tree.yview)
290
+
291
+ tree.column("#0", anchor="w", width=200)
292
+ tree.column(1, anchor="w", width=25)
293
+ tree.heading("#0", text="Option")
294
+ tree.heading(1, text="Value")
295
+
296
+ for section in self.scraper.config.sections():
297
+ if section == "App Settings":
298
+ continue
299
+ section_level = tree.insert("", "end", iid=section, text=section)
300
+ for config_option, value in self.scraper.config.items(section):
301
+ title_option = config_option.replace("_", " ").title()
302
+ tree.insert(section_level, "end", text=title_option, values=(value,))
303
+
304
+ self._make_tree_editable(editor_frame, tree)
305
+
306
+ return tree
307
+
308
+ def _configure_save_button(self, button_frame, tree):
309
+ """Save updated options and values from the treeview back to the config file."""
310
+
311
+ def save_config():
312
+ for child in tree.get_children():
313
+ for item in tree.get_children(child):
314
+ title_option = tree.item(item, "text")
315
+ config_option = title_option.lower().replace(" ", "_")
316
+ value = tree.item(item, "values")[0]
317
+ section = tree.parent(item)
318
+ section_name = tree.item(section, "text")
319
+ if section_name == "Custom Items":
320
+ # custom items are already saved upon creation (Saving them again would result in duplicates)
321
+ continue
322
+ self.scraper.config.set(section_name, config_option, value)
323
+
324
+ self.scraper.config.write_to_file()
325
+ if self.scraper.config.valid:
326
+ messagebox.showinfo(
327
+ "Config Saved", "The configuration has been saved successfully."
328
+ )
329
+ else:
330
+ messagebox.showerror(
331
+ "Config Error",
332
+ f"The configuration is invalid. ({self.scraper.config.last_error})",
333
+ )
334
+
335
+ save_button = ttk.Button(button_frame, text="Save", command=save_config)
336
+ save_button.pack(side="left", expand=True, padx=5)
337
+
338
+ def _configure_custom_item_button(self, button_frame, tree):
339
+ """Add a button that opens an entry dialog to add a custom item to the
340
+ configuration.
341
+ """
342
+
343
+ def add_custom_item(item_url, item_owned):
344
+ """Add a custom item to the configuration."""
345
+ if not item_url or not item_owned:
346
+ messagebox.showerror("Input Error", "All fields must be filled out.")
347
+ return
348
+
349
+ try:
350
+ if int(item_owned) < 0:
351
+ raise ValueError("Owned count must be a non-negative integer.")
352
+ except ValueError as error:
353
+ messagebox.showerror("Input Error", f"Invalid owned count: {error}")
354
+ return
355
+
356
+ self.scraper.config.set("Custom Items", item_url, item_owned)
357
+ self.scraper.config.write_to_file()
358
+ if self.scraper.config.valid:
359
+ tree.insert("Custom Items", "end", text=item_url, values=(item_owned,))
360
+ messagebox.showinfo("Custom Item Added", "Custom item has been added successfully.")
361
+ else:
362
+ self.scraper.config.remove_option("Custom Items", item_url)
363
+ messagebox.showerror(
364
+ "Config Error",
365
+ f"The configuration is invalid. ({self.scraper.config.last_error})",
366
+ )
367
+
368
+ def open_custom_item_dialog():
369
+ """Open a dialog to enter custom item details."""
370
+ dialog = tk.Toplevel(self.config_editor_window)
371
+ dialog.title(NEW_CUSTOM_ITEM_TITLE)
372
+ dialog.geometry(NEW_CUSTOM_ITEM_SIZE)
373
+
374
+ dialog_frame = ttk.Frame(dialog, padding=10)
375
+ dialog_frame.pack(expand=True, fill="both")
376
+
377
+ ttk.Label(dialog_frame, text="Item URL:").pack(pady=5)
378
+ item_url_entry = ttk.Entry(dialog_frame)
379
+ item_url_entry.pack(fill="x", padx=10)
380
+
381
+ ttk.Label(dialog_frame, text="Owned Count:").pack(pady=5)
382
+ item_owned_entry = ttk.Entry(dialog_frame)
383
+ item_owned_entry.pack(fill="x", padx=10)
384
+
385
+ add_button = ttk.Button(
386
+ dialog_frame,
387
+ text="Add",
388
+ command=lambda: add_custom_item(item_url_entry.get(), item_owned_entry.get()),
389
+ )
390
+ add_button.pack(pady=10)
391
+
392
+ custom_item_button = ttk.Button(
393
+ button_frame, text="Add Custom Item", command=open_custom_item_dialog
394
+ )
395
+ custom_item_button.pack(side="left", expand=True, padx=5)
396
+
397
+ def _configure_editor_frame(self):
398
+ """Configure the main editor frame which displays the configuration options in a
399
+ structured way.
400
+ """
401
+ editor_frame = ttk.Frame(self.config_editor_window, padding=30)
402
+ editor_frame.pack(expand=True, fill="both")
403
+
404
+ tree = self._configure_treeview(editor_frame)
405
+ tree.pack(expand=True, fill="both")
406
+
407
+ button_frame = ttk.Frame(editor_frame, padding=10)
408
+
409
+ self._configure_save_button(button_frame, tree)
410
+ self._configure_custom_item_button(button_frame, tree)
411
+
412
+ button_frame.pack(side="bottom", padx=10, pady=(0, 10))
413
+
414
+ def _edit_config(self):
415
+ """Open a new window with a config editor GUI."""
416
+ self.config_editor_window = tk.Toplevel(self.application_window)
417
+ self.config_editor_window.geometry(CONFIG_EDITOR_SIZE)
418
+ self.config_editor_window.title(CONFIG_EDITOR_TITLE)
419
+
420
+ self._configure_editor_frame()
421
+
422
+ def _reset_config(self):
423
+ """Reset the configuration file to its default state."""
424
+ confirm = messagebox.askokcancel(
425
+ "Reset Config", "Are you sure you want to reset the configuration?"
426
+ )
427
+ if confirm:
428
+ copy(CONFIG_FILE_BACKUP, CONFIG_FILE)
429
+ self.scraper.load_config()
430
+
431
+ def _draw_plot(self):
432
+ """Draw a plot of the scraped prices over time."""
433
+ dates, usd_prices, eur_prices = PriceLogs.read()
434
+
435
+ fig, ax_raw = plt.subplots(figsize=(10, 8), num="CS2Tracker Price History")
436
+ fig.suptitle("CS2Tracker Price History", fontsize=16)
437
+ fig.autofmt_xdate()
438
+
439
+ ax = cast(Axes, ax_raw)
440
+ ax.plot(dates, usd_prices, label="Dollars")
441
+ ax.plot(dates, eur_prices, label="Euros")
442
+ ax.legend()
443
+ date_formatter = DateFormatter("%Y-%m-%d")
444
+ ax.xaxis.set_major_formatter(date_formatter)
445
+
446
+ plt.show()
447
+
448
+ def _export_log_file(self):
449
+ """Lets the user export the log file to a different location."""
450
+ export_path = asksaveasfile(
451
+ title="Export Log File",
452
+ defaultextension=".csv",
453
+ filetypes=[("CSV files", "*.csv")],
454
+ )
455
+ if export_path:
456
+ copy(OUTPUT_FILE, export_path.name)
457
+
458
+ def _import_log_file(self):
459
+ """Lets the user import a log file from a different location."""
460
+ import_path = askopenfilename(
461
+ title="Import Log File",
462
+ defaultextension=".csv",
463
+ filetypes=[("CSV files", "*.csv")],
464
+ )
465
+ if not PriceLogs.validate_file(import_path):
466
+ return
467
+ copy(import_path, OUTPUT_FILE)
468
+
469
+ def _toggle_background_task(self, enabled: bool):
470
+ """Toggle whether a daily price calculation should run in the background."""
471
+ BackgroundTask.toggle(enabled)
472
+
473
+ def _toggle_use_proxy(self, enabled: bool):
474
+ """Toggle whether the scraper should use proxy servers for requests."""
475
+ proxy_api_key = self.scraper.config.get("User Settings", "proxy_api_key", fallback=None)
476
+ if not proxy_api_key and enabled:
477
+ messagebox.showerror(
478
+ "Config Error",
479
+ "You need to enter a valid crawlbase API key into the config file to use this feature.",
480
+ )
481
+ return False
482
+
483
+ self.scraper.toggle_use_proxy(enabled)
484
+ return True
485
+
486
+ def _toggle_discord_webhook(self, enabled: bool):
487
+ """Toggle whether the scraper should send notifications to a Discord webhook."""
488
+ discord_webhook_url = self.scraper.config.get(
489
+ "User Settings", "discord_webhook_url", fallback=None
490
+ )
491
+ if not discord_webhook_url and enabled:
492
+ messagebox.showerror(
493
+ "Config Error",
494
+ "You need to enter a valid Discord webhook URL into the config file to use this feature.",
495
+ )
496
+ return False
497
+
498
+ self.scraper.toggle_discord_webhook(enabled)
499
+ return True
500
+
501
+
502
+ def _popen_and_call(popen_args, callback):
503
+ """
504
+ Runs the given args in a subprocess.Popen, and then calls the function callback when
505
+ the subprocess completes.
506
+
507
+ Source: https://stackoverflow.com/questions/2581817/python-subprocess-callback-when-cmd-exits
508
+ """
509
+
510
+ def process_and_callback(popen_args, callback):
511
+ process = Popen(**popen_args)
512
+ process.wait()
513
+ callback()
514
+
515
+ thread = Thread(target=process_and_callback, args=(popen_args, callback), daemon=True)
516
+ thread.start()