cs2tracker 2.1.8__py3-none-any.whl → 2.1.10__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/constants.py CHANGED
@@ -18,18 +18,9 @@ class OSType(enum.Enum):
18
18
 
19
19
 
20
20
  OS = OSType.WINDOWS if sys.platform.startswith("win") else OSType.LINUX
21
- TEXT_EDITOR = "notepad" if OS == OSType.WINDOWS else "nano"
22
21
  PYTHON_EXECUTABLE = sys.executable
23
22
 
24
23
 
25
- MODULE_DIR = os.path.dirname(os.path.abspath(__file__))
26
- PROJECT_DIR = os.path.dirname(MODULE_DIR)
27
- ICON_FILE = os.path.join(PROJECT_DIR, "assets", "icon.png")
28
- OUTPUT_FILE = os.path.join(MODULE_DIR, "data", "output.csv")
29
- CONFIG_FILE = os.path.join(MODULE_DIR, "data", "config.ini")
30
- BATCH_FILE = os.path.join(MODULE_DIR, "data", "cs2tracker_scraper.bat")
31
-
32
-
33
24
  RUNNING_IN_EXE = getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS")
34
25
 
35
26
  if RUNNING_IN_EXE:
@@ -45,12 +36,31 @@ if RUNNING_IN_EXE:
45
36
  os.makedirs(DATA_DIR, exist_ok=True)
46
37
 
47
38
  CONFIG_FILE = os.path.join(DATA_DIR, "config.ini")
39
+ CONFIG_FILE_BACKUP = os.path.join(DATA_DIR, "config.ini.bak")
48
40
  OUTPUT_FILE = os.path.join(DATA_DIR, "output.csv")
49
41
  BATCH_FILE = os.path.join(DATA_DIR, "cs2tracker_scraper.bat")
50
- if not os.path.exists(CONFIG_FILE):
51
- copy(CONFIG_FILE_SOURCE, CONFIG_FILE)
42
+
43
+ # Always copy the source config into the user data directory as a backup
44
+ # and overwrite the existing backup if it exists
45
+ # (This is to ensure that no outdated config backup remains in the user data directory)
46
+ copy(CONFIG_FILE_SOURCE, CONFIG_FILE_BACKUP)
47
+
52
48
  if not os.path.exists(OUTPUT_FILE):
53
49
  copy(OUTPUT_FILE_SOURCE, OUTPUT_FILE)
50
+ if not os.path.exists(CONFIG_FILE):
51
+ copy(CONFIG_FILE_SOURCE, CONFIG_FILE)
52
+
53
+ else:
54
+ MODULE_DIR = os.path.dirname(os.path.abspath(__file__))
55
+ PROJECT_DIR = os.path.dirname(MODULE_DIR)
56
+ ICON_FILE = os.path.join(PROJECT_DIR, "assets", "icon.png")
57
+ CONFIG_FILE = os.path.join(MODULE_DIR, "data", "config.ini")
58
+ CONFIG_FILE_BACKUP = os.path.join(MODULE_DIR, "data", "config.ini.bak")
59
+ OUTPUT_FILE = os.path.join(MODULE_DIR, "data", "output.csv")
60
+ BATCH_FILE = os.path.join(MODULE_DIR, "data", "cs2tracker_scraper.bat")
61
+
62
+ if not os.path.exists(CONFIG_FILE_BACKUP):
63
+ copy(CONFIG_FILE, CONFIG_FILE_BACKUP)
54
64
 
55
65
 
56
66
  BANNER = """
@@ -68,50 +78,17 @@ AUTHOR_STRING = (
68
78
  )
69
79
 
70
80
 
71
- CASE_PAGES = [
72
- "https://steamcommunity.com/market/search?q=revolution+case",
73
- "https://steamcommunity.com/market/search?q=recoil+case",
74
- "https://steamcommunity.com/market/search?q=dreams+and+nightmares+case",
75
- "https://steamcommunity.com/market/search?q=operation+riptide+case",
76
- "https://steamcommunity.com/market/search?q=snakebite+case",
77
- "https://steamcommunity.com/market/search?q=broken+fang+case",
78
- "https://steamcommunity.com/market/search?q=fracture+case",
79
- "https://steamcommunity.com/market/search?q=chroma+case",
80
- "https://steamcommunity.com/market/search?q=chroma+case",
81
- "https://steamcommunity.com/market/search?q=chroma+case",
82
- "https://steamcommunity.com/market/search?q=clutch+case",
83
- "https://steamcommunity.com/market/search?q=csgo+weapon+case",
84
- "https://steamcommunity.com/market/search?q=csgo+weapon+case",
85
- "https://steamcommunity.com/market/search?q=csgo+weapon+case",
86
- "https://steamcommunity.com/market/search?q=cs20+case",
87
- "https://steamcommunity.com/market/search?q=danger+zone+case",
88
- "https://steamcommunity.com/market/search?q=esports+case",
89
- "https://steamcommunity.com/market/search?q=esports+case",
90
- "https://steamcommunity.com/market/search?q=esports+case",
91
- "https://steamcommunity.com/market/search?q=falchion+case",
92
- "https://steamcommunity.com/market/search?q=gamma+case",
93
- "https://steamcommunity.com/market/search?q=gamma+case",
94
- "https://steamcommunity.com/market/search?q=glove+case",
95
- "https://steamcommunity.com/market/search?q=horizon+case",
96
- "https://steamcommunity.com/market/search?q=huntsman+weapon+case",
97
- "https://steamcommunity.com/market/search?q=operation+bravo+case",
98
- "https://steamcommunity.com/market/search?q=operation+breakout+case",
99
- "https://steamcommunity.com/market/search?q=operation+hydra+case",
100
- "https://steamcommunity.com/market/search?q=operation+phoenix+case",
101
- "https://steamcommunity.com/market/search?q=operation+vanguard+case",
102
- "https://steamcommunity.com/market/search?q=operation+wildfire+case",
103
- "https://steamcommunity.com/market/search?q=prisma+case",
104
- "https://steamcommunity.com/market/search?q=prisma+case",
105
- "https://steamcommunity.com/market/search?q=revolver+case",
106
- "https://steamcommunity.com/market/search?q=shadow+case",
107
- "https://steamcommunity.com/market/search?q=shattered+web+case",
108
- "https://steamcommunity.com/market/search?q=spectrum+case",
109
- "https://steamcommunity.com/market/search?q=spectrum+case",
110
- "https://steamcommunity.com/market/search?q=winter+offensive+case",
111
- "https://steamcommunity.com/market/search?q=kilowatt+case",
112
- "https://steamcommunity.com/market/search?q=gallery+case",
113
- "https://steamcommunity.com/market/search?q=fever+case",
114
- ]
81
+ POWERSHELL_COLORIZE_OUTPUT = (
82
+ "%{ "
83
+ "if($_ -match '\\[!\\]') { Write-Host $_ -ForegroundColor red } "
84
+ "elseif($_ -match 'Version|\\\\|_') { Write-Host $_ -ForegroundColor yellow } "
85
+ "elseif($_ -match 'USD|EUR|^[-|\\s]+$') { Write-Host $_ -ForegroundColor green } "
86
+ "elseif($_ -match 'Case|Capsule|-[A-Za-z0-9]') { Write-Host $_ -ForegroundColor magenta } "
87
+ "elseif($_ -match 'Legends|Challengers|Contenders|Champions|Finalists') { Write-Host $_ -ForegroundColor blue } "
88
+ "else { Write-Host $_ } "
89
+ "}"
90
+ )
91
+
115
92
 
116
93
  CASE_HREFS = [
117
94
  "https://steamcommunity.com/market/listings/730/Revolution%20Case",
@@ -1,205 +1,204 @@
1
1
  [User Settings]
2
- api_key = None
3
- discord_webhook_url = None
4
-
5
- [App Settings]
6
- use_proxy = False
7
- discord_notifications = False
2
+ proxy_api_key ~
3
+ discord_webhook_url ~
8
4
 
9
5
  [Custom Items]
10
- copenhagen_flames_gold_2022 = 0 https://steamcommunity.com/market/listings/730/Sticker%20%7C%20Copenhagen%20Flames%20%28Gold%29%20%7C%20Antwerp%202022
11
6
 
12
7
  [Cases]
13
- revolution_case = 0
14
- recoil_case = 0
15
- dreams_and_nightmares_case = 0
16
- operation_riptide_case = 0
17
- snakebite_case = 0
18
- operation_broken_fang_case = 0
19
- fracture_case = 0
20
- chroma_case = 0
21
- chroma_2_case = 0
22
- chroma_3_case = 0
23
- clutch_case = 0
24
- csgo_weapon_case = 0
25
- csgo_weapon_case_2 = 0
26
- csgo_weapon_case_3 = 0
27
- cs20_case = 0
28
- danger_zone_case = 0
29
- esports_2013_case = 0
30
- esports_2013_winter_case = 0
31
- esports_2014_summer_case = 0
32
- falchion_case = 0
33
- gamma_case = 0
34
- gamma_2_case = 0
35
- glove_case = 0
36
- horizon_case = 0
37
- huntsman_case = 0
38
- operation_bravo_case = 0
39
- operation_breakout_case = 0
40
- operation_hydra_case = 0
41
- operation_phoenix_case = 0
42
- operation_vanguard_case = 0
43
- operation_wildfire_case = 0
44
- prisma_case = 0
45
- prisma_2_case = 0
46
- revolver_case = 0
47
- shadow_case = 0
48
- shattered_web_case = 0
49
- spectrum_case = 0
50
- spectrum_2_case = 0
51
- winter_offensive_case = 0
52
- kilowatt_case = 0
53
- gallery_case = 0
54
- fever_case = 0
8
+ revolution_case ~ 0
9
+ recoil_case ~ 0
10
+ dreams_and_nightmares_case ~ 0
11
+ operation_riptide_case ~ 0
12
+ snakebite_case ~ 0
13
+ operation_broken_fang_case ~ 0
14
+ fracture_case ~ 0
15
+ chroma_case ~ 0
16
+ chroma_2_case ~ 0
17
+ chroma_3_case ~ 0
18
+ clutch_case ~ 0
19
+ csgo_weapon_case ~ 0
20
+ csgo_weapon_case_2 ~ 0
21
+ csgo_weapon_case_3 ~ 0
22
+ cs20_case ~ 0
23
+ danger_zone_case ~ 0
24
+ esports_2013_case ~ 0
25
+ esports_2013_winter_case ~ 0
26
+ esports_2014_summer_case ~ 0
27
+ falchion_case ~ 0
28
+ gamma_case ~ 0
29
+ gamma_2_case ~ 0
30
+ glove_case ~ 0
31
+ horizon_case ~ 0
32
+ huntsman_case ~ 0
33
+ operation_bravo_case ~ 0
34
+ operation_breakout_case ~ 0
35
+ operation_hydra_case ~ 0
36
+ operation_phoenix_case ~ 0
37
+ operation_vanguard_case ~ 0
38
+ operation_wildfire_case ~ 0
39
+ prisma_case ~ 0
40
+ prisma_2_case ~ 0
41
+ revolver_case ~ 0
42
+ shadow_case ~ 0
43
+ shattered_web_case ~ 0
44
+ spectrum_case ~ 0
45
+ spectrum_2_case ~ 0
46
+ winter_offensive_case ~ 0
47
+ kilowatt_case ~ 0
48
+ gallery_case ~ 0
49
+ fever_case ~ 0
55
50
 
56
51
  [Katowice 2014 Sticker Capsule]
57
- katowice_legends = 0
58
- katowice_challengers = 0
52
+ katowice_legends ~ 0
53
+ katowice_challengers ~ 0
59
54
 
60
55
  [Cologne 2014 Sticker Capsule]
61
- cologne_legends = 0
62
- cologne_challengers = 0
56
+ cologne_legends ~ 0
57
+ cologne_challengers ~ 0
63
58
 
64
59
  [DreamHack 2014 Sticker Capsule]
65
- dreamhack_legends = 0
60
+ dreamhack_legends ~ 0
66
61
 
67
62
  [Katowice 2015 Sticker Capsule]
68
- katowice_legends = 0
69
- katowice_challengers = 0
63
+ katowice_legends ~ 0
64
+ katowice_challengers ~ 0
70
65
 
71
66
  [Cologne 2015 Sticker Capsule]
72
- cologne_legends = 0
73
- cologne_challengers = 0
67
+ cologne_legends ~ 0
68
+ cologne_challengers ~ 0
74
69
 
75
70
  [Cluj-Napoca 2015 Sticker Capsule]
76
- cluj_napoca_legends = 0
77
- cluj_napoca_challengers = 0
78
- cluj_napoca_legends_autographs = 0
79
- cluj_napoca_challengers_autographs = 0
71
+ cluj_napoca_legends ~ 0
72
+ cluj_napoca_challengers ~ 0
73
+ cluj_napoca_legends_autographs ~ 0
74
+ cluj_napoca_challengers_autographs ~ 0
80
75
 
81
76
  [Columbus 2016 Sticker Capsule]
82
- columbus_legends = 0
83
- columbus_challengers = 0
84
- columbus_legends_autographs = 0
85
- columbus_challengers_autographs = 0
77
+ columbus_legends ~ 0
78
+ columbus_challengers ~ 0
79
+ columbus_legends_autographs ~ 0
80
+ columbus_challengers_autographs ~ 0
86
81
 
87
82
  [Cologne 2016 Sticker Capsule]
88
- cologne_legends = 0
89
- cologne_challengers = 0
90
- cologne_legends_autographs = 0
91
- cologne_challengers_autographs = 0
83
+ cologne_legends ~ 0
84
+ cologne_challengers ~ 0
85
+ cologne_legends_autographs ~ 0
86
+ cologne_challengers_autographs ~ 0
92
87
 
93
88
  [Atlanta 2017 Sticker Capsule]
94
- atlanta_legends = 0
95
- atlanta_challengers = 0
96
- atlanta_legends_autographs = 0
97
- atlanta_challengers_autographs = 0
89
+ atlanta_legends ~ 0
90
+ atlanta_challengers ~ 0
91
+ atlanta_legends_autographs ~ 0
92
+ atlanta_challengers_autographs ~ 0
98
93
 
99
94
  [Krakow 2017 Sticker Capsule]
100
- krakow_legends = 0
101
- krakow_challengers = 0
102
- krakow_legends_autographs = 0
103
- krakow_challengers_autographs = 0
95
+ krakow_legends ~ 0
96
+ krakow_challengers ~ 0
97
+ krakow_legends_autographs ~ 0
98
+ krakow_challengers_autographs ~ 0
104
99
 
105
100
  [Boston 2018 Sticker Capsule]
106
- boston_legends = 0
107
- boston_minor_challengers = 0
108
- boston_returning_challengers = 0
109
- boston_attending_legends = 0
110
- boston_minor_challengers_with_flash_gaming = 0
111
- boston_legends_autographs = 0
112
- boston_minor_challengers_autographs = 0
113
- boston_returning_challengers_autographs = 0
114
- boston_attending_legends_autographs = 0
115
- boston_minor_challengers_with_flash_gaming_autographs = 0
101
+ boston_legends ~ 0
102
+ boston_minor_challengers ~ 0
103
+ boston_returning_challengers ~ 0
104
+ boston_attending_legends ~ 0
105
+ boston_minor_challengers_with_flash_gaming ~ 0
106
+ boston_legends_autographs ~ 0
107
+ boston_minor_challengers_autographs ~ 0
108
+ boston_returning_challengers_autographs ~ 0
109
+ boston_attending_legends_autographs ~ 0
110
+ boston_minor_challengers_with_flash_gaming_autographs ~ 0
116
111
 
117
112
  [London 2018 Sticker Capsule]
118
- london_legends = 0
119
- london_minor_challengers = 0
120
- london_returning_challengers = 0
121
- london_legends_autographs = 0
122
- london_minor_challengers_autographs = 0
123
- london_returning_challengers_autographs = 0
113
+ london_legends ~ 0
114
+ london_minor_challengers ~ 0
115
+ london_returning_challengers ~ 0
116
+ london_legends_autographs ~ 0
117
+ london_minor_challengers_autographs ~ 0
118
+ london_returning_challengers_autographs ~ 0
124
119
 
125
120
  [Katowice 2019 Sticker Capsule]
126
- katowice_legends = 0
127
- katowice_minor_challengers = 0
128
- katowice_returning_challengers = 0
129
- katowice_legends_autographs = 0
130
- katowice_minor_challengers_autographs = 0
131
- katowice_returning_challengers_autographs = 0
121
+ katowice_legends ~ 0
122
+ katowice_minor_challengers ~ 0
123
+ katowice_returning_challengers ~ 0
124
+ katowice_legends_autographs ~ 0
125
+ katowice_minor_challengers_autographs ~ 0
126
+ katowice_returning_challengers_autographs ~ 0
132
127
 
133
128
  [Berlin 2019 Sticker Capsule]
134
- berlin_legends = 0
135
- berlin_minor_challengers = 0
136
- berlin_returning_challengers = 0
137
- berlin_legends_autographs = 0
138
- berlin_minor_challengers_autographs = 0
139
- berlin_returning_challengers_autographs = 0
129
+ berlin_legends ~ 0
130
+ berlin_minor_challengers ~ 0
131
+ berlin_returning_challengers ~ 0
132
+ berlin_legends_autographs ~ 0
133
+ berlin_minor_challengers_autographs ~ 0
134
+ berlin_returning_challengers_autographs ~ 0
140
135
 
141
136
  [2020 RMR Sticker Capsule]
142
- rmr_legends = 0
143
- rmr_challengers = 0
144
- rmr_contenders = 0
137
+ rmr_legends ~ 0
138
+ rmr_challengers ~ 0
139
+ rmr_contenders ~ 0
145
140
 
146
141
  [Stockholm 2021 Sticker Capsule]
147
- stockholm_legends = 0
148
- stockholm_challengers = 0
149
- stockholm_contenders = 0
150
- stockholm_champions_autographs = 0
151
- stockholm_finalists_autographs = 0
142
+ stockholm_legends ~ 0
143
+ stockholm_challengers ~ 0
144
+ stockholm_contenders ~ 0
145
+ stockholm_champions_autographs ~ 0
146
+ stockholm_finalists_autographs ~ 0
152
147
 
153
148
  [Antwerp 2022 Sticker Capsule]
154
- antwerp_legends = 0
155
- antwerp_challengers = 0
156
- antwerp_contenders = 0
157
- antwerp_champions_autographs = 0
158
- antwerp_challengers_autographs = 0
159
- antwerp_legends_autographs = 0
160
- antwerp_contenders_autographs = 0
149
+ antwerp_legends ~ 0
150
+ antwerp_challengers ~ 0
151
+ antwerp_contenders ~ 0
152
+ antwerp_champions_autographs ~ 0
153
+ antwerp_challengers_autographs ~ 0
154
+ antwerp_legends_autographs ~ 0
155
+ antwerp_contenders_autographs ~ 0
161
156
 
162
157
  [Rio 2022 Sticker Capsule]
163
- rio_legends = 0
164
- rio_challengers = 0
165
- rio_contenders = 0
166
- rio_champions_autographs = 0
167
- rio_challengers_autographs = 0
168
- rio_legends_autographs = 0
169
- rio_contenders_autographs = 0
158
+ rio_legends ~ 0
159
+ rio_challengers ~ 0
160
+ rio_contenders ~ 0
161
+ rio_champions_autographs ~ 0
162
+ rio_challengers_autographs ~ 0
163
+ rio_legends_autographs ~ 0
164
+ rio_contenders_autographs ~ 0
170
165
 
171
166
  [Paris 2023 Sticker Capsule]
172
- paris_legends = 0
173
- paris_challengers = 0
174
- paris_contenders = 0
175
- paris_champions_autographs = 0
176
- paris_challengers_autographs = 0
177
- paris_legends_autographs = 0
178
- paris_contenders_autographs = 0
167
+ paris_legends ~ 0
168
+ paris_challengers ~ 0
169
+ paris_contenders ~ 0
170
+ paris_champions_autographs ~ 0
171
+ paris_challengers_autographs ~ 0
172
+ paris_legends_autographs ~ 0
173
+ paris_contenders_autographs ~ 0
179
174
 
180
175
  [Copenhagen 2024 Sticker Capsule]
181
- copenhagen_legends = 0
182
- copenhagen_challengers = 0
183
- copenhagen_contenders = 0
184
- copenhagen_champions_autographs = 0
185
- copenhagen_challengers_autographs = 0
186
- copenhagen_legends_autographs = 0
187
- copenhagen_contenders_autographs = 0
176
+ copenhagen_legends ~ 0
177
+ copenhagen_challengers ~ 0
178
+ copenhagen_contenders ~ 0
179
+ copenhagen_champions_autographs ~ 0
180
+ copenhagen_challengers_autographs ~ 0
181
+ copenhagen_legends_autographs ~ 0
182
+ copenhagen_contenders_autographs ~ 0
188
183
 
189
184
  [Shanghai 2024 Sticker Capsule]
190
- shanghai_legends = 0
191
- shanghai_challengers = 0
192
- shanghai_contenders = 0
193
- shanghai_champions_autographs = 0
194
- shanghai_challengers_autographs = 0
195
- shanghai_legends_autographs = 0
196
- shanghai_contenders_autographs = 0
185
+ shanghai_legends ~ 0
186
+ shanghai_challengers ~ 0
187
+ shanghai_contenders ~ 0
188
+ shanghai_champions_autographs ~ 0
189
+ shanghai_challengers_autographs ~ 0
190
+ shanghai_legends_autographs ~ 0
191
+ shanghai_contenders_autographs ~ 0
197
192
 
198
193
  [Austin 2025 Sticker Capsule]
199
- austin_legends = 0
200
- austin_challengers = 0
201
- austin_contenders = 0
202
- austin_champions_autographs = 0
203
- austin_challengers_autographs = 0
204
- austin_legends_autographs = 0
205
- austin_contenders_autographs = 0
194
+ austin_legends ~ 0
195
+ austin_challengers ~ 0
196
+ austin_contenders ~ 0
197
+ austin_champions_autographs ~ 0
198
+ austin_challengers_autographs ~ 0
199
+ austin_legends_autographs ~ 0
200
+ austin_contenders_autographs ~ 0
201
+
202
+ [App Settings]
203
+ use_proxy ~ False
204
+ discord_notifications ~ False
@@ -0,0 +1,87 @@
1
+ import requests
2
+ from requests.exceptions import RequestException
3
+
4
+ from cs2tracker.padded_console import PaddedConsole
5
+ from cs2tracker.price_logs import PriceLogs
6
+
7
+ DC_WEBHOOK_USERNAME = "CS2Tracker"
8
+ DC_WEBHOOK_AVATAR_URL = "https://img.icons8.com/?size=100&id=uWQJp2tLXUH6&format=png&color=000000"
9
+ DC_RECENT_HISTORY_LIMIT = 5
10
+
11
+ console = PaddedConsole()
12
+
13
+
14
+ class DiscordNotifier:
15
+ @classmethod
16
+ def _construct_recent_calculations_embeds(cls):
17
+ """
18
+ Construct the embeds for the Discord message that will be sent after a price
19
+ calculation has been made.
20
+
21
+ :return: A list of embeds for the Discord message.
22
+ """
23
+ dates, usd_prices, eur_prices = PriceLogs.read()
24
+ dates, usd_prices, eur_prices = reversed(dates), reversed(usd_prices), reversed(eur_prices)
25
+
26
+ date_history, usd_history, eur_history = [], [], []
27
+ for date, usd_log, eur_log in zip(dates, usd_prices, eur_prices):
28
+ if len(date_history) >= DC_RECENT_HISTORY_LIMIT:
29
+ break
30
+ date_history.append(date.strftime("%Y-%m-%d"))
31
+ usd_history.append(f"${usd_log:.2f}")
32
+ eur_history.append(f"€{eur_log:.2f}")
33
+
34
+ date_history = "\n".join(date_history)
35
+ usd_history = "\n".join(usd_history)
36
+ eur_history = "\n".join(eur_history)
37
+
38
+ embeds = [
39
+ {
40
+ "title": "📊 Recent Price History",
41
+ "color": 5814783,
42
+ "fields": [
43
+ {
44
+ "name": "Date",
45
+ "value": date_history,
46
+ "inline": True,
47
+ },
48
+ {
49
+ "name": "USD Total",
50
+ "value": usd_history,
51
+ "inline": True,
52
+ },
53
+ {
54
+ "name": "EUR Total",
55
+ "value": eur_history,
56
+ "inline": True,
57
+ },
58
+ ],
59
+ }
60
+ ]
61
+
62
+ return embeds
63
+
64
+ @classmethod
65
+ def notify(cls, webhook_url):
66
+ """
67
+ Notify users via Discord about recent price calculations.
68
+
69
+ :param webhook_url: The Discord webhook URL to send the notification to.
70
+ """
71
+ embeds = cls._construct_recent_calculations_embeds()
72
+ try:
73
+ response = requests.post(
74
+ url=webhook_url,
75
+ json={
76
+ "embeds": embeds,
77
+ "username": DC_WEBHOOK_USERNAME,
78
+ "avatar_url": DC_WEBHOOK_AVATAR_URL,
79
+ },
80
+ timeout=10,
81
+ )
82
+ response.raise_for_status()
83
+ console.print("[bold steel_blue3][+] Discord notification sent.\n")
84
+ except RequestException as error:
85
+ console.print(f"[bold red][!] Failed to send Discord notification: {error}\n")
86
+ except Exception as error:
87
+ console.print(f"[bold red][!] An unexpected error occurred: {error}\n")
@@ -0,0 +1,100 @@
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