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.
- cs2tracker/_version.py +2 -2
- cs2tracker/app/__init__.py +0 -3
- cs2tracker/app/app.py +283 -0
- cs2tracker/app/editor_frame.py +354 -158
- cs2tracker/app/history_frame.py +61 -0
- cs2tracker/app/scraper_frame.py +34 -11
- cs2tracker/config.py +263 -0
- cs2tracker/constants.py +58 -321
- cs2tracker/data/config.ini +157 -154
- cs2tracker/data/convert_inventory.js +108 -28
- cs2tracker/data/get_inventory.js +96 -32
- cs2tracker/logs.py +143 -0
- cs2tracker/main.py +4 -19
- cs2tracker/scraper/__init__.py +0 -9
- cs2tracker/scraper/background_task.py +1 -1
- cs2tracker/scraper/discord_notifier.py +34 -32
- cs2tracker/scraper/parser.py +189 -0
- cs2tracker/scraper/scraper.py +166 -246
- cs2tracker/util/__init__.py +0 -9
- cs2tracker/util/currency_conversion.py +84 -0
- cs2tracker/util/padded_console.py +24 -0
- cs2tracker/util/tkinter.py +55 -0
- {cs2tracker-2.1.13.dist-info → cs2tracker-2.1.15.dist-info}/METADATA +6 -6
- cs2tracker-2.1.15.dist-info/RECORD +31 -0
- cs2tracker/app/application.py +0 -255
- cs2tracker/util/price_logs.py +0 -100
- cs2tracker/util/validated_config.py +0 -164
- cs2tracker-2.1.13.dist-info/RECORD +0 -27
- {cs2tracker-2.1.13.dist-info → cs2tracker-2.1.15.dist-info}/WHEEL +0 -0
- {cs2tracker-2.1.13.dist-info → cs2tracker-2.1.15.dist-info}/entry_points.txt +0 -0
- {cs2tracker-2.1.13.dist-info → cs2tracker-2.1.15.dist-info}/licenses/LICENSE +0 -0
- {cs2tracker-2.1.13.dist-info → cs2tracker-2.1.15.dist-info}/top_level.txt +0 -0
cs2tracker/_version.py
CHANGED
cs2tracker/app/__init__.py
CHANGED
cs2tracker/app/app.py
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
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
|
+
|
|
7
|
+
import sv_ttk
|
|
8
|
+
|
|
9
|
+
from cs2tracker.app.editor_frame import ConfigEditorFrame
|
|
10
|
+
from cs2tracker.app.history_frame import PriceHistoryFrame
|
|
11
|
+
from cs2tracker.app.scraper_frame import ScraperFrame
|
|
12
|
+
from cs2tracker.config import get_config
|
|
13
|
+
from cs2tracker.constants import ICON_FILE, OS, OUTPUT_FILE, OSType
|
|
14
|
+
from cs2tracker.logs import PriceLogs
|
|
15
|
+
from cs2tracker.scraper.background_task import BackgroundTask
|
|
16
|
+
from cs2tracker.scraper.scraper import Scraper
|
|
17
|
+
from cs2tracker.util.currency_conversion import CURRENCY_SYMBOLS
|
|
18
|
+
from cs2tracker.util.tkinter import centered, fix_sv_ttk, size_info
|
|
19
|
+
|
|
20
|
+
APPLICATION_NAME = "CS2Tracker"
|
|
21
|
+
WINDOW_SIZE = "630x335"
|
|
22
|
+
DARK_THEME = True
|
|
23
|
+
|
|
24
|
+
SCRAPER_WINDOW_TITLE = "Price Overview"
|
|
25
|
+
SCRAPER_WINDOW_SIZE = "900x750"
|
|
26
|
+
|
|
27
|
+
CONFIG_EDITOR_TITLE = "Config Editor"
|
|
28
|
+
CONFIG_EDITOR_SIZE = "850x750"
|
|
29
|
+
|
|
30
|
+
PRICE_HISTORY_TITLE = "Price History"
|
|
31
|
+
PRICE_HISTORY_SIZE = "900x700"
|
|
32
|
+
|
|
33
|
+
config = get_config()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Application:
|
|
37
|
+
def __init__(self):
|
|
38
|
+
self.scraper = Scraper()
|
|
39
|
+
|
|
40
|
+
def run(self):
|
|
41
|
+
"""Run the main application window."""
|
|
42
|
+
window = self._configure_window()
|
|
43
|
+
|
|
44
|
+
if DARK_THEME:
|
|
45
|
+
sv_ttk.use_dark_theme()
|
|
46
|
+
else:
|
|
47
|
+
sv_ttk.use_light_theme()
|
|
48
|
+
|
|
49
|
+
fix_sv_ttk(ttk.Style())
|
|
50
|
+
|
|
51
|
+
window.mainloop()
|
|
52
|
+
|
|
53
|
+
def _configure_window(self):
|
|
54
|
+
"""Configure the main application window."""
|
|
55
|
+
window = tk.Tk()
|
|
56
|
+
window.title(APPLICATION_NAME)
|
|
57
|
+
window.geometry(centered(window, WINDOW_SIZE))
|
|
58
|
+
window.minsize(*size_info(WINDOW_SIZE))
|
|
59
|
+
|
|
60
|
+
if OS == OSType.WINDOWS:
|
|
61
|
+
app_id = "cs2tracker.unique.id"
|
|
62
|
+
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(app_id)
|
|
63
|
+
|
|
64
|
+
icon = tk.PhotoImage(file=ICON_FILE)
|
|
65
|
+
window.wm_iconphoto(True, icon)
|
|
66
|
+
|
|
67
|
+
main_frame = MainFrame(window, self.scraper)
|
|
68
|
+
main_frame.pack(expand=True, fill="both")
|
|
69
|
+
|
|
70
|
+
return window
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class MainFrame(ttk.Frame):
|
|
74
|
+
# pylint: disable=too-many-instance-attributes,attribute-defined-outside-init
|
|
75
|
+
def __init__(self, parent, scraper):
|
|
76
|
+
super().__init__(parent, padding=15)
|
|
77
|
+
self.parent = parent
|
|
78
|
+
self.scraper = scraper
|
|
79
|
+
self._add_widgets()
|
|
80
|
+
|
|
81
|
+
def _add_widgets(self):
|
|
82
|
+
"""Add widgets to the main frame."""
|
|
83
|
+
self.columnconfigure(0, weight=1)
|
|
84
|
+
self.columnconfigure(1, weight=1)
|
|
85
|
+
self.rowconfigure(0, weight=1)
|
|
86
|
+
|
|
87
|
+
self._configure_button_frame()
|
|
88
|
+
self.button_frame.grid(row=0, column=0, padx=10, pady=(0, 20), sticky="nsew")
|
|
89
|
+
self._configure_settings_frame()
|
|
90
|
+
self.settings_frame.grid(row=0, column=1, padx=10, pady=(0, 20), sticky="nsew")
|
|
91
|
+
|
|
92
|
+
def _add_button(self, text, command, row):
|
|
93
|
+
"""Create and style a button for the button frame."""
|
|
94
|
+
grid_pos = {"row": row, "column": 0, "sticky": "ew", "padx": 10, "pady": 10}
|
|
95
|
+
button = ttk.Button(self.button_frame, text=text, command=command)
|
|
96
|
+
button.grid(**grid_pos)
|
|
97
|
+
|
|
98
|
+
def _configure_button_frame(self):
|
|
99
|
+
"""Configure the button frame of the application main frame."""
|
|
100
|
+
self.button_frame = ttk.Frame(self, style="Card.TFrame", padding=15)
|
|
101
|
+
self.button_frame.columnconfigure(0, weight=1)
|
|
102
|
+
|
|
103
|
+
self._add_button("Run!", self.scrape_prices, 0)
|
|
104
|
+
self._add_button("Edit Config", self._edit_config, 1)
|
|
105
|
+
self._add_button("Show History", self._show_history, 2)
|
|
106
|
+
self._add_button("Export History", self._export_log_file, 3)
|
|
107
|
+
self._add_button("Import History", self._import_log_file, 4)
|
|
108
|
+
|
|
109
|
+
def _add_checkbox(
|
|
110
|
+
self, text, variable, command, row
|
|
111
|
+
): # pylint: disable=too-many-arguments,too-many-positional-arguments,attribute-defined-outside-init
|
|
112
|
+
"""Create and style a checkbox for the checkbox frame."""
|
|
113
|
+
grid_pos = {"row": row, "column": 0, "sticky": "w", "padx": (10, 0), "pady": 5}
|
|
114
|
+
checkbox = ttk.Checkbutton(
|
|
115
|
+
self.settings_frame,
|
|
116
|
+
text=text,
|
|
117
|
+
variable=variable,
|
|
118
|
+
command=command,
|
|
119
|
+
style="Switch.TCheckbutton",
|
|
120
|
+
)
|
|
121
|
+
checkbox.grid(**grid_pos)
|
|
122
|
+
|
|
123
|
+
def _configure_settings_frame(self):
|
|
124
|
+
"""Configure the settings frame for background tasks and other settings."""
|
|
125
|
+
self.settings_frame = ttk.LabelFrame(self, text="Settings", padding=15)
|
|
126
|
+
self.settings_frame.columnconfigure(0, weight=1)
|
|
127
|
+
|
|
128
|
+
self.background_checkbox_value = tk.BooleanVar(value=BackgroundTask.identify())
|
|
129
|
+
self._add_checkbox(
|
|
130
|
+
"Background Task",
|
|
131
|
+
self.background_checkbox_value,
|
|
132
|
+
lambda: self._toggle_background_task(self.background_checkbox_value.get()),
|
|
133
|
+
0,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
self.discord_webhook_checkbox_value = tk.BooleanVar(value=config.discord_notifications)
|
|
137
|
+
self._add_checkbox(
|
|
138
|
+
"Discord Notifications",
|
|
139
|
+
self.discord_webhook_checkbox_value,
|
|
140
|
+
lambda: self.discord_webhook_checkbox_value.set(
|
|
141
|
+
self._toggle_discord_webhook(self.discord_webhook_checkbox_value.get())
|
|
142
|
+
),
|
|
143
|
+
1,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
self.use_proxy_checkbox_value = tk.BooleanVar(value=config.use_proxy)
|
|
147
|
+
self._add_checkbox(
|
|
148
|
+
"Proxy Requests",
|
|
149
|
+
self.use_proxy_checkbox_value,
|
|
150
|
+
lambda: self.use_proxy_checkbox_value.set(
|
|
151
|
+
self._toggle_use_proxy(self.use_proxy_checkbox_value.get())
|
|
152
|
+
),
|
|
153
|
+
2,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
self.dark_theme_checkbox_value = tk.BooleanVar(value=DARK_THEME)
|
|
157
|
+
self._add_checkbox("Dark Theme", self.dark_theme_checkbox_value, self._toggle_theme, 3)
|
|
158
|
+
|
|
159
|
+
self.currency_selection_label = ttk.Label(self.settings_frame, text="Currency:")
|
|
160
|
+
self.currency_selection_label.grid(row=4, column=0, sticky="w", padx=(20, 0), pady=5)
|
|
161
|
+
self.currency_selection = ttk.Combobox(
|
|
162
|
+
self.settings_frame,
|
|
163
|
+
state="readonly",
|
|
164
|
+
values=list(CURRENCY_SYMBOLS),
|
|
165
|
+
postcommand=self.parent.focus_set,
|
|
166
|
+
)
|
|
167
|
+
self.currency_selection.set(config.conversion_currency)
|
|
168
|
+
self.currency_selection.grid(row=5, column=0, sticky="w", padx=(20, 0), pady=5)
|
|
169
|
+
|
|
170
|
+
def on_currency_change(_):
|
|
171
|
+
config.set_app_option("conversion_currency", self.currency_selection.get())
|
|
172
|
+
self.currency_selection.selection_clear()
|
|
173
|
+
self.parent.focus_set()
|
|
174
|
+
|
|
175
|
+
self.currency_selection.bind(
|
|
176
|
+
"<<ComboboxSelected>>",
|
|
177
|
+
on_currency_change,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
def scrape_prices(self):
|
|
181
|
+
"""Scrape prices from the configured sources, print the total, and save the
|
|
182
|
+
results to a file.
|
|
183
|
+
"""
|
|
184
|
+
scraper_window = tk.Toplevel(self.parent)
|
|
185
|
+
scraper_window.geometry(centered(scraper_window, SCRAPER_WINDOW_SIZE))
|
|
186
|
+
scraper_window.minsize(*size_info(SCRAPER_WINDOW_SIZE))
|
|
187
|
+
scraper_window.title(SCRAPER_WINDOW_TITLE)
|
|
188
|
+
|
|
189
|
+
run_frame = ScraperFrame(
|
|
190
|
+
scraper_window,
|
|
191
|
+
self.scraper,
|
|
192
|
+
sheet_size=SCRAPER_WINDOW_SIZE,
|
|
193
|
+
dark_theme=self.dark_theme_checkbox_value.get(),
|
|
194
|
+
)
|
|
195
|
+
run_frame.pack(expand=True, fill="both")
|
|
196
|
+
run_frame.start()
|
|
197
|
+
|
|
198
|
+
def _edit_config(self):
|
|
199
|
+
"""Open a new window with a config editor GUI."""
|
|
200
|
+
config_editor_window = tk.Toplevel(self.parent)
|
|
201
|
+
config_editor_window.geometry(centered(config_editor_window, CONFIG_EDITOR_SIZE))
|
|
202
|
+
config_editor_window.minsize(*size_info(CONFIG_EDITOR_SIZE))
|
|
203
|
+
config_editor_window.title(CONFIG_EDITOR_TITLE)
|
|
204
|
+
|
|
205
|
+
editor_frame = ConfigEditorFrame(config_editor_window)
|
|
206
|
+
editor_frame.pack(expand=True, fill="both")
|
|
207
|
+
|
|
208
|
+
def _show_history(self):
|
|
209
|
+
"""Show a chart consisting of past calculations."""
|
|
210
|
+
if PriceLogs.empty():
|
|
211
|
+
return
|
|
212
|
+
|
|
213
|
+
price_history_window = tk.Toplevel(self.parent)
|
|
214
|
+
price_history_window.geometry(centered(price_history_window, PRICE_HISTORY_SIZE))
|
|
215
|
+
price_history_window.minsize(*size_info(PRICE_HISTORY_SIZE))
|
|
216
|
+
price_history_window.title(PRICE_HISTORY_TITLE)
|
|
217
|
+
|
|
218
|
+
history_frame = PriceHistoryFrame(price_history_window)
|
|
219
|
+
history_frame.pack(expand=True, fill="both")
|
|
220
|
+
|
|
221
|
+
def _export_log_file(self):
|
|
222
|
+
"""Lets the user export the log file to a different location."""
|
|
223
|
+
if PriceLogs.empty():
|
|
224
|
+
return
|
|
225
|
+
|
|
226
|
+
export_path = asksaveasfile(
|
|
227
|
+
title="Export Log File",
|
|
228
|
+
defaultextension=".csv",
|
|
229
|
+
filetypes=[("CSV File", "*.csv")],
|
|
230
|
+
)
|
|
231
|
+
if export_path:
|
|
232
|
+
copy(OUTPUT_FILE, export_path.name)
|
|
233
|
+
|
|
234
|
+
def _import_log_file(self):
|
|
235
|
+
"""Lets the user import a log file from a different location."""
|
|
236
|
+
import_path = askopenfilename(
|
|
237
|
+
title="Import Log File",
|
|
238
|
+
defaultextension=".csv",
|
|
239
|
+
filetypes=[("CSV files", "*.csv")],
|
|
240
|
+
)
|
|
241
|
+
if not PriceLogs.validate_file(import_path):
|
|
242
|
+
return
|
|
243
|
+
copy(import_path, OUTPUT_FILE)
|
|
244
|
+
|
|
245
|
+
def _toggle_background_task(self, enabled: bool):
|
|
246
|
+
"""Toggle whether a daily price calculation should run in the background."""
|
|
247
|
+
BackgroundTask.toggle(enabled)
|
|
248
|
+
|
|
249
|
+
def _toggle_use_proxy(self, enabled: bool):
|
|
250
|
+
"""Toggle whether the scraper should use proxy servers for requests."""
|
|
251
|
+
proxy_api_key = config.proxy_api_key
|
|
252
|
+
if not proxy_api_key and enabled:
|
|
253
|
+
messagebox.showerror(
|
|
254
|
+
"Config Error",
|
|
255
|
+
"You need to enter a valid crawlbase API key into the configuration to use this feature.",
|
|
256
|
+
parent=self.parent,
|
|
257
|
+
)
|
|
258
|
+
return False
|
|
259
|
+
|
|
260
|
+
config.toggle_app_option("use_proxy", enabled)
|
|
261
|
+
return True
|
|
262
|
+
|
|
263
|
+
def _toggle_discord_webhook(self, enabled: bool):
|
|
264
|
+
"""Toggle whether the scraper should send notifications to a Discord webhook."""
|
|
265
|
+
discord_webhook_url = config.discord_webhook_url
|
|
266
|
+
if not discord_webhook_url and enabled:
|
|
267
|
+
messagebox.showerror(
|
|
268
|
+
"Config Error",
|
|
269
|
+
"You need to enter a valid Discord webhook URL into the configuration to use this feature.",
|
|
270
|
+
parent=self.parent,
|
|
271
|
+
)
|
|
272
|
+
return False
|
|
273
|
+
|
|
274
|
+
config.toggle_app_option("discord_webhook", enabled)
|
|
275
|
+
return True
|
|
276
|
+
|
|
277
|
+
def _toggle_theme(self):
|
|
278
|
+
"""Toggle the theme of the application."""
|
|
279
|
+
if self.dark_theme_checkbox_value.get():
|
|
280
|
+
sv_ttk.use_dark_theme()
|
|
281
|
+
else:
|
|
282
|
+
sv_ttk.use_light_theme()
|
|
283
|
+
fix_sv_ttk(ttk.Style())
|