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
|
@@ -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.
|
|
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
|
[](https://badge.fury.io/py/cs2tracker)
|
|
41
42
|
[](https://github.com/ashiven/cs2tracker/issues)
|
|
42
43
|
[](https://github.com/ashiven/cs2tracker/pulls)
|
|
43
|
-

|
|
44
44
|
|
|
45
|
-
<img src="
|
|
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
|
|
63
|
+
- 🔍 Track prices on Steam, Buff163, Youpin898
|
|
64
64
|
- 📈 View investment price history
|
|
65
|
-
- 🧾 Export/Import
|
|
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
|
|
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,,
|
cs2tracker/app/application.py
DELETED
|
@@ -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
|
cs2tracker/util/price_logs.py
DELETED
|
@@ -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
|