cs2tracker 2.1.1__py3-none-any.whl → 2.1.2__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.
cs2tracker/__main__.py CHANGED
@@ -1,7 +1,12 @@
1
- from .main import main
1
+ from cs2tracker.main import main
2
2
 
3
3
 
4
4
  def entry_point():
5
+ """
6
+ The entry point for the CS2Tracker application.
7
+
8
+ Calls the main function to start the application.
9
+ """
5
10
  main()
6
11
 
7
12
 
cs2tracker/_version.py CHANGED
@@ -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.1'
21
- __version_tuple__ = version_tuple = (2, 1, 1)
20
+ __version__ = version = '2.1.2'
21
+ __version_tuple__ = version_tuple = (2, 1, 2)
cs2tracker/application.py CHANGED
@@ -1,14 +1,14 @@
1
- import csv
2
- import datetime
3
1
  import os
4
2
  import subprocess
5
3
  import tkinter as tk
4
+ from typing import cast
6
5
 
7
6
  import matplotlib.pyplot as plt
7
+ from matplotlib.axes import Axes
8
8
  from matplotlib.dates import DateFormatter
9
9
 
10
- from .constants import CONFIG_FILE, OUTPUT_FILE, TEXT_EDITOR
11
- from .scraper import Scraper
10
+ from cs2tracker.constants import CONFIG_FILE, OUTPUT_FILE, TEXT_EDITOR
11
+ from cs2tracker.scraper import Scraper
12
12
 
13
13
 
14
14
  class Application:
@@ -16,6 +16,16 @@ class Application:
16
16
  self.scraper = Scraper()
17
17
 
18
18
  def run(self):
19
+ """Run the main application window with buttons for scraping prices, editing the
20
+ configuration, showing history in a chart, and editing the log file.
21
+ """
22
+ application_window = self._configure_window()
23
+ application_window.mainloop()
24
+
25
+ def _configure_window(self):
26
+ """Configure the main application window layout with buttons for the various
27
+ actions.
28
+ """
19
29
  window = tk.Tk()
20
30
  window.title("CS2Tracker")
21
31
  window.geometry("400x400")
@@ -25,12 +35,8 @@ class Application:
25
35
 
26
36
  run_button = tk.Button(window, text="Run!", command=self._scrape_prices)
27
37
  edit_button = tk.Button(window, text="Edit Config", command=self._edit_config)
28
- plot_button = tk.Button(
29
- window, text="Show History (Chart)", command=self._draw_plot
30
- )
31
- plotfile_button = tk.Button(
32
- window, text="Show History (File)", command=self._plot_file
33
- )
38
+ plot_button = tk.Button(window, text="Show History (Chart)", command=self._draw_plot)
39
+ plotfile_button = tk.Button(window, text="Show History (File)", command=self._edit_log_file)
34
40
 
35
41
  run_button.grid(row=1, column=0, pady=10, sticky="NSEW")
36
42
  edit_button.grid(row=2, column=0, pady=10, sticky="NSEW")
@@ -48,24 +54,28 @@ class Application:
48
54
  plot_button.grid_configure(sticky="NSEW")
49
55
  plotfile_button.grid_configure(sticky="NSEW")
50
56
 
51
- window.mainloop()
57
+ return window
52
58
 
53
59
  def _scrape_prices(self):
60
+ """Scrape prices from the configured sources, print the total, and save the
61
+ results to a file.
62
+ """
54
63
  self.scraper.scrape_prices()
55
- self.scraper.print_total()
56
- self.scraper.save_to_file()
57
64
 
58
65
  def _edit_config(self):
66
+ """Edit the configuration file using the specified text editor."""
59
67
  subprocess.call([TEXT_EDITOR, CONFIG_FILE])
60
- config = self.scraper.parse_config()
61
- self.scraper.set_config(config)
68
+ self.scraper.parse_config()
62
69
 
63
70
  def _draw_plot(self):
64
- datesp, dollars, euros = self._parse_output()
71
+ """Draw a plot of the scraped prices over time."""
72
+ dates, dollars, euros = self.scraper.read_price_log()
65
73
 
66
- fig, ax = plt.subplots()
67
- ax.plot(datesp, dollars, label="Dollars")
68
- ax.plot(datesp, euros, label="Euros")
74
+ fig, ax_raw = plt.subplots()
75
+ ax = cast(Axes, ax_raw)
76
+
77
+ ax.plot(dates, dollars, label="Dollars")
78
+ ax.plot(dates, euros, label="Euros")
69
79
  ax.set_xlabel("Date")
70
80
  ax.set_ylabel("Price")
71
81
  ax.legend()
@@ -76,39 +86,8 @@ class Application:
76
86
 
77
87
  plt.show()
78
88
 
79
- def _parse_output(self):
80
- def parse_row(row):
81
- date_str, price_str = row
82
- price = float(price_str[:-1])
83
- return date_str, price
84
-
85
- dates = []
86
- dollars = []
87
- euros = []
88
- row_num = 0
89
-
90
- if not os.path.isfile(OUTPUT_FILE):
91
- open(OUTPUT_FILE, "w", encoding="utf-8").close()
92
-
93
- with open(OUTPUT_FILE, "r", newline="", encoding="utf-8") as csvfile:
94
- reader = csv.reader(csvfile)
95
- for row in reader:
96
- row_num += 1
97
- date, price = parse_row(row)
98
- if row_num % 2 == 0:
99
- euros.append(price)
100
- else:
101
- dollars.append(price)
102
- dates.append(date)
103
-
104
- datesp = []
105
- for date_str in dates:
106
- date = datetime.datetime.strptime(date_str[:-9], "%Y-%m-%d")
107
- datesp.append(date)
108
-
109
- return datesp, dollars, euros
110
-
111
- def _plot_file(self):
89
+ def _edit_log_file(self):
90
+ """Opens the file containing past price calculations."""
112
91
  if not os.path.isfile(OUTPUT_FILE):
113
92
  open(OUTPUT_FILE, "w", encoding="utf-8").close()
114
93
  subprocess.call([TEXT_EDITOR, OUTPUT_FILE])
cs2tracker/constants.py CHANGED
@@ -7,178 +7,177 @@ OUTPUT_FILE = f"{BASE_DIR}/data/output.csv"
7
7
  CONFIG_FILE = f"{BASE_DIR}/data/config.ini"
8
8
 
9
9
 
10
- CAPSULE_NAMES = [
11
- "RMR Legends",
12
- "RMR Challengers",
13
- "RMR Contenders",
14
- "Stockholm Legends",
15
- "Stockholm Challengers",
16
- "Stockholm Contenders",
17
- "Stockholm Champions Autographs",
18
- "Stockholm Finalists Autographs",
19
- "Antwerp Legends",
20
- "Antwerp Challengers",
21
- "Antwerp Contenders",
22
- "Antwerp Champions Autographs",
23
- "Antwerp Challengers Autographs",
24
- "Antwerp Legends Autographs",
25
- "Antwerp Contenders Autographs",
26
- "Rio Legends",
27
- "Rio Challengers",
28
- "Rio Contenders",
29
- "Rio Champions Autographs",
30
- "Rio Challengers Autographs",
31
- "Rio Legends Autographs",
32
- "Rio Contenders Autographs",
33
- "Paris Legends",
34
- "Paris Challengers",
35
- "Paris Contenders",
36
- "Paris Champions Autographs",
37
- "Paris Challengers Autographs",
38
- "Paris Legends Autographs",
39
- "Paris Contenders Autographs",
40
- "Copenhagen Legends",
41
- "Copenhagen Challengers",
42
- "Copenhagen Contenders",
43
- "Copenhagen Champions Autographs",
44
- "Copenhagen Challengers Autographs",
45
- "Copenhagen Legends Autographs",
46
- "Copenhagen Contenders Autographs",
47
- "Shanghai Legends",
48
- "Shanghai Challengers",
49
- "Shanghai Contenders",
50
- "Shanghai Champions Autographs",
51
- "Shanghai Challengers Autographs",
52
- "Shanghai Legends Autographs",
53
- "Shanghai Contenders Autographs",
54
- "Austin Legends",
55
- "Austin Challengers",
56
- "Austin Contenders",
57
- "Austin Champions Autographs",
58
- "Austin Challengers Autographs",
59
- "Austin Legends Autographs",
60
- "Austin Contenders Autographs",
61
- ]
10
+ RMR_CAPSULES = {
11
+ "page": "https://steamcommunity.com/market/search?q=2020+rmr",
12
+ "items": [
13
+ "https://steamcommunity.com/market/listings/730/2020%20RMR%20Legends",
14
+ "https://steamcommunity.com/market/listings/730/2020%20RMR%20Challengers",
15
+ "https://steamcommunity.com/market/listings/730/2020%20RMR%20Contenders",
16
+ ],
17
+ "names": ["RMR Legends", "RMR Challengers", "RMR Contenders"],
18
+ }
62
19
 
63
- CAPSULE_NAMES_GENERIC = [
64
- "Legends",
65
- "Challengers",
66
- "Contenders",
67
- "Champions Autographs",
68
- "Challengers Autographs",
69
- "Legends Autographs",
70
- "Contenders Autographs",
71
- "Finalists Autographs",
72
- ]
20
+ STOCKHOLM_CAPSULES = {
21
+ "page": "https://steamcommunity.com/market/search?q=stockholm+capsule",
22
+ "items": [
23
+ "https://steamcommunity.com/market/listings/730/Stockholm%202021%20Legends%20Sticker%20Capsule",
24
+ "https://steamcommunity.com/market/listings/730/Stockholm%202021%20Challengers%20Sticker%20Capsule",
25
+ "https://steamcommunity.com/market/listings/730/Stockholm%202021%20Contenders%20Sticker%20Capsule",
26
+ "https://steamcommunity.com/market/listings/730/Stockholm%202021%20Champions%20Autograph%20Capsule",
27
+ "https://steamcommunity.com/market/listings/730/Stockholm%202021%20Finalists%20Autograph%20Capsule",
28
+ ],
29
+ "names": [
30
+ "Stockholm Legends",
31
+ "Stockholm Challengers",
32
+ "Stockholm Contenders",
33
+ "Stockholm Champions Autographs",
34
+ "Stockholm Finalists Autographs",
35
+ ],
36
+ }
73
37
 
74
- CAPSULE_PAGES = [
75
- "https://steamcommunity.com/market/search?q=2020+rmr",
76
- "https://steamcommunity.com/market/search?q=stockholm+capsule",
77
- "https://steamcommunity.com/market/search?q=antwerp+capsule",
78
- "https://steamcommunity.com/market/search?q=rio+capsule",
79
- "https://steamcommunity.com/market/search?q=paris+capsule",
80
- "https://steamcommunity.com/market/search?q=copenhagen+capsule",
81
- "https://steamcommunity.com/market/search?q=shanghai+capsule",
82
- "https://steamcommunity.com/market/search?q=austin+capsule",
83
- ]
38
+ ANTWERP_CAPSULES = {
39
+ "page": "https://steamcommunity.com/market/search?q=antwerp+capsule",
40
+ "items": [
41
+ "https://steamcommunity.com/market/listings/730/Antwerp%202022%20Legends%20Sticker%20Capsule",
42
+ "https://steamcommunity.com/market/listings/730/Antwerp%202022%20Challengers%20Sticker%20Capsule",
43
+ "https://steamcommunity.com/market/listings/730/Antwerp%202022%20Contenders%20Sticker%20Capsule",
44
+ "https://steamcommunity.com/market/listings/730/Antwerp%202022%20Champions%20Autograph%20Capsule",
45
+ "https://steamcommunity.com/market/listings/730/Antwerp%202022%20Challengers%20Autograph%20Capsule",
46
+ "https://steamcommunity.com/market/listings/730/Antwerp%202022%20Legends%20Autograph%20Capsule",
47
+ "https://steamcommunity.com/market/listings/730/Antwerp%202022%20Contenders%20Autograph%20Capsule",
48
+ ],
49
+ "names": [
50
+ "Antwerp Legends",
51
+ "Antwerp Challengers",
52
+ "Antwerp Contenders",
53
+ "Antwerp Champions Autographs",
54
+ "Antwerp Challengers Autographs",
55
+ "Antwerp Legends Autographs",
56
+ "Antwerp Contenders Autographs",
57
+ ],
58
+ }
84
59
 
85
- CAPSULE_HREFS = [
86
- "https://steamcommunity.com/market/listings/730/2020%20RMR%20Legends",
87
- "https://steamcommunity.com/market/listings/730/2020%20RMR%20Challengers",
88
- "https://steamcommunity.com/market/listings/730/2020%20RMR%20Contenders",
89
- "https://steamcommunity.com/market/listings/730/Stockholm%202021%20Legends%20Sticker%20Capsule",
90
- "https://steamcommunity.com/market/listings/730/Stockholm%202021%20Challengers%20Sticker%20Capsule",
91
- "https://steamcommunity.com/market/listings/730/Stockholm%202021%20Contenders%20Sticker%20Capsule",
92
- "https://steamcommunity.com/market/listings/730/Stockholm%202021%20Champions%20Autograph%20Capsule",
93
- "https://steamcommunity.com/market/listings/730/Stockholm%202021%20Finalists%20Autograph%20Capsule",
94
- "https://steamcommunity.com/market/listings/730/Antwerp%202022%20Legends%20Sticker%20Capsule",
95
- "https://steamcommunity.com/market/listings/730/Antwerp%202022%20Challengers%20Sticker%20Capsule",
96
- "https://steamcommunity.com/market/listings/730/Antwerp%202022%20Contenders%20Sticker%20Capsule",
97
- "https://steamcommunity.com/market/listings/730/Antwerp%202022%20Champions%20Autograph%20Capsule",
98
- "https://steamcommunity.com/market/listings/730/Antwerp%202022%20Challengers%20Autograph%20Capsule",
99
- "https://steamcommunity.com/market/listings/730/Antwerp%202022%20Legends%20Autograph%20Capsule",
100
- "https://steamcommunity.com/market/listings/730/Antwerp%202022%20Contenders%20Autograph%20Capsule",
101
- "https://steamcommunity.com/market/listings/730/Rio%202022%20Legends%20Sticker%20Capsule",
102
- "https://steamcommunity.com/market/listings/730/Rio%202022%20Challengers%20Sticker%20Capsule",
103
- "https://steamcommunity.com/market/listings/730/Rio%202022%20Contenders%20Sticker%20Capsule",
104
- "https://steamcommunity.com/market/listings/730/Rio%202022%20Champions%20Autograph%20Capsule",
105
- "https://steamcommunity.com/market/listings/730/Rio%202022%20Challengers%20Autograph%20Capsule",
106
- "https://steamcommunity.com/market/listings/730/Rio%202022%20Legends%20Autograph%20Capsule",
107
- "https://steamcommunity.com/market/listings/730/Rio%202022%20Contenders%20Autograph%20Capsule",
108
- "https://steamcommunity.com/market/listings/730/Paris%202023%20Legends%20Sticker%20Capsule",
109
- "https://steamcommunity.com/market/listings/730/Paris%202023%20Challengers%20Sticker%20Capsule",
110
- "https://steamcommunity.com/market/listings/730/Paris%202023%20Contenders%20Sticker%20Capsule",
111
- "https://steamcommunity.com/market/listings/730/Paris%202023%20Champions%20Autograph%20Capsule",
112
- "https://steamcommunity.com/market/listings/730/Paris%202023%20Challengers%20Autograph%20Capsule",
113
- "https://steamcommunity.com/market/listings/730/Paris%202023%20Legends%20Autograph%20Capsule",
114
- "https://steamcommunity.com/market/listings/730/Paris%202023%20Contenders%20Autograph%20Capsule",
115
- "https://steamcommunity.com/market/listings/730/Copenhagen%202024%20Legends%20Sticker%20Capsule",
116
- "https://steamcommunity.com/market/listings/730/Copenhagen%202024%20Challengers%20Sticker%20Capsule",
117
- "https://steamcommunity.com/market/listings/730/Copenhagen%202024%20Contenders%20Sticker%20Capsule",
118
- "https://steamcommunity.com/market/listings/730/Copenhagen%202024%20Champions%20Autograph%20Capsule",
119
- "https://steamcommunity.com/market/listings/730/Copenhagen%202024%20Challengers%20Autograph%20Capsule",
120
- "https://steamcommunity.com/market/listings/730/Copenhagen%202024%20Legends%20Autograph%20Capsule",
121
- "https://steamcommunity.com/market/listings/730/Copenhagen%202024%20Contenders%20Autograph%20Capsule",
122
- "https://steamcommunity.com/market/listings/730/Shanghai%202024%20Legends%20Sticker%20Capsule",
123
- "https://steamcommunity.com/market/listings/730/Shanghai%202024%20Challengers%20Sticker%20Capsule",
124
- "https://steamcommunity.com/market/listings/730/Shanghai%202024%20Contenders%20Sticker%20Capsule",
125
- "https://steamcommunity.com/market/listings/730/Shanghai%202024%20Champions%20Autograph%20Capsule",
126
- "https://steamcommunity.com/market/listings/730/Shanghai%202024%20Challengers%20Autograph%20Capsule",
127
- "https://steamcommunity.com/market/listings/730/Shanghai%202024%20Legends%20Autograph%20Capsule",
128
- "https://steamcommunity.com/market/listings/730/Shanghai%202024%20Contenders%20Autograph%20Capsule",
129
- "https://steamcommunity.com/market/listings/730/Austin%202025%20Legends%20Sticker%20Capsule",
130
- "https://steamcommunity.com/market/listings/730/Austin%202025%20Challengers%20Sticker%20Capsule",
131
- "https://steamcommunity.com/market/listings/730/Austin%202025%20Contenders%20Sticker%20Capsule",
132
- "https://steamcommunity.com/market/listings/730/Austin%202025%20Champions%20Autograph%20Capsule",
133
- "https://steamcommunity.com/market/listings/730/Austin%202025%20Challengers%20Autograph%20Capsule",
134
- "https://steamcommunity.com/market/listings/730/Austin%202025%20Legends%20Autograph%20Capsule",
135
- "https://steamcommunity.com/market/listings/730/Austin%202025%20Contenders%20Autograph%20Capsule",
136
- ]
60
+ RIO_CAPSULES = {
61
+ "page": "https://steamcommunity.com/market/search?q=rio+capsule",
62
+ "items": [
63
+ "https://steamcommunity.com/market/listings/730/Rio%202022%20Legends%20Sticker%20Capsule",
64
+ "https://steamcommunity.com/market/listings/730/Rio%202022%20Challengers%20Sticker%20Capsule",
65
+ "https://steamcommunity.com/market/listings/730/Rio%202022%20Contenders%20Sticker%20Capsule",
66
+ "https://steamcommunity.com/market/listings/730/Rio%202022%20Champions%20Autograph%20Capsule",
67
+ "https://steamcommunity.com/market/listings/730/Rio%202022%20Challengers%20Autograph%20Capsule",
68
+ "https://steamcommunity.com/market/listings/730/Rio%202022%20Legends%20Autograph%20Capsule",
69
+ "https://steamcommunity.com/market/listings/730/Rio%202022%20Contenders%20Autograph%20Capsule",
70
+ ],
71
+ "names": [
72
+ "Rio Legends",
73
+ "Rio Challengers",
74
+ "Rio Contenders",
75
+ "Rio Champions Autographs",
76
+ "Rio Challengers Autographs",
77
+ "Rio Legends Autographs",
78
+ "Rio Contenders Autographs",
79
+ ],
80
+ }
137
81
 
138
- CASE_NAMES = [
139
- "Revolution Case",
140
- "Recoil Case",
141
- "Dreams And Nightmares Case",
142
- "Operation Riptide Case",
143
- "Snakebite Case",
144
- "Operation Broken Fang Case",
145
- "Fracture Case",
146
- "Chroma Case",
147
- "Chroma 2 Case",
148
- "Chroma 3 Case",
149
- "Clutch Case",
150
- "CSGO Weapon Case",
151
- "CSGO Weapon Case 2",
152
- "CSGO Weapon Case 3",
153
- "CS20 Case",
154
- "Danger Zone Case",
155
- "eSports 2013 Case",
156
- "eSports 2013 Winter Case",
157
- "eSports 2014 Summer Case",
158
- "Falchion Case",
159
- "Gamma Case",
160
- "Gamma 2 Case",
161
- "Glove Case",
162
- "Horizon Case",
163
- "Huntsman Case",
164
- "Operation Bravo Case",
165
- "Operation Breakout Case",
166
- "Operation Hydra Case",
167
- "Operation Phoenix Case",
168
- "Operation Vanguard Case",
169
- "Operation Wildfire Case",
170
- "Prisma Case",
171
- "Prisma 2 Case",
172
- "Revolver Case",
173
- "Shadow Case",
174
- "Shattered Web Case",
175
- "Spectrum Case",
176
- "Spectrum 2 Case",
177
- "Winter Offensive Case",
178
- "Kilowatt Case",
179
- "Gallery Case",
180
- "Fever Case",
181
- ]
82
+ PARIS_CAPSULES = {
83
+ "page": "https://steamcommunity.com/market/search?q=paris+capsule",
84
+ "items": [
85
+ "https://steamcommunity.com/market/listings/730/Paris%202023%20Legends%20Sticker%20Capsule",
86
+ "https://steamcommunity.com/market/listings/730/Paris%202023%20Challengers%20Sticker%20Capsule",
87
+ "https://steamcommunity.com/market/listings/730/Paris%202023%20Contenders%20Sticker%20Capsule",
88
+ "https://steamcommunity.com/market/listings/730/Paris%202023%20Champions%20Autograph%20Capsule",
89
+ "https://steamcommunity.com/market/listings/730/Paris%202023%20Challengers%20Autograph%20Capsule",
90
+ "https://steamcommunity.com/market/listings/730/Paris%202023%20Legends%20Autograph%20Capsule",
91
+ "https://steamcommunity.com/market/listings/730/Paris%202023%20Contenders%20Autograph%20Capsule",
92
+ ],
93
+ "names": [
94
+ "Paris Legends",
95
+ "Paris Challengers",
96
+ "Paris Contenders",
97
+ "Paris Champions Autographs",
98
+ "Paris Challengers Autographs",
99
+ "Paris Legends Autographs",
100
+ "Paris Contenders Autographs",
101
+ ],
102
+ }
103
+
104
+ COPENHAGEN_CAPSULES = {
105
+ "page": "https://steamcommunity.com/market/search?q=copenhagen+capsule",
106
+ "items": [
107
+ "https://steamcommunity.com/market/listings/730/Copenhagen%202024%20Legends%20Sticker%20Capsule",
108
+ "https://steamcommunity.com/market/listings/730/Copenhagen%202024%20Challengers%20Sticker%20Capsule",
109
+ "https://steamcommunity.com/market/listings/730/Copenhagen%202024%20Contenders%20Sticker%20Capsule",
110
+ "https://steamcommunity.com/market/listings/730/Copenhagen%202024%20Champions%20Autograph%20Capsule",
111
+ "https://steamcommunity.com/market/listings/730/Copenhagen%202024%20Challengers%20Autograph%20Capsule",
112
+ "https://steamcommunity.com/market/listings/730/Copenhagen%202024%20Legends%20Autograph%20Capsule",
113
+ "https://steamcommunity.com/market/listings/730/Copenhagen%202024%20Contenders%20Autograph%20Capsule",
114
+ ],
115
+ "names": [
116
+ "Copenhagen Legends",
117
+ "Copenhagen Challengers",
118
+ "Copenhagen Contenders",
119
+ "Copenhagen Champions Autographs",
120
+ "Copenhagen Challengers Autographs",
121
+ "Copenhagen Legends Autographs",
122
+ "Copenhagen Contenders Autographs",
123
+ ],
124
+ }
125
+
126
+ SHANGHAI_CAPSULES = {
127
+ "page": "https://steamcommunity.com/market/search?q=shanghai+capsule",
128
+ "items": [
129
+ "https://steamcommunity.com/market/listings/730/Shanghai%202024%20Legends%20Sticker%20Capsule",
130
+ "https://steamcommunity.com/market/listings/730/Shanghai%202024%20Challengers%20Sticker%20Capsule",
131
+ "https://steamcommunity.com/market/listings/730/Shanghai%202024%20Contenders%20Sticker%20Capsule",
132
+ "https://steamcommunity.com/market/listings/730/Shanghai%202024%20Champions%20Autograph%20Capsule",
133
+ "https://steamcommunity.com/market/listings/730/Shanghai%202024%20Challengers%20Autograph%20Capsule",
134
+ "https://steamcommunity.com/market/listings/730/Shanghai%202024%20Legends%20Autograph%20Capsule",
135
+ "https://steamcommunity.com/market/listings/730/Shanghai%202024%20Contenders%20Autograph%20Capsule",
136
+ ],
137
+ "names": [
138
+ "Shanghai Legends",
139
+ "Shanghai Challengers",
140
+ "Shanghai Contenders",
141
+ "Shanghai Champions Autographs",
142
+ "Shanghai Challengers Autographs",
143
+ "Shanghai Legends Autographs",
144
+ "Shanghai Contenders Autographs",
145
+ ],
146
+ }
147
+
148
+
149
+ AUSTIN_CAPSULES = {
150
+ "page": "https://steamcommunity.com/market/search?q=austin+capsule",
151
+ "items": [
152
+ "https://steamcommunity.com/market/listings/730/Austin%202025%20Legends%20Sticker%20Capsule",
153
+ "https://steamcommunity.com/market/listings/730/Austin%202025%20Challengers%20Sticker%20Capsule",
154
+ "https://steamcommunity.com/market/listings/730/Austin%202025%20Contenders%20Sticker%20Capsule",
155
+ "https://steamcommunity.com/market/listings/730/Austin%202025%20Champions%20Autograph%20Capsule",
156
+ "https://steamcommunity.com/market/listings/730/Austin%202025%20Challengers%20Autograph%20Capsule",
157
+ "https://steamcommunity.com/market/listings/730/Austin%202025%20Legends%20Autograph%20Capsule",
158
+ "https://steamcommunity.com/market/listings/730/Austin%202025%20Contenders%20Autograph%20Capsule",
159
+ ],
160
+ "names": [
161
+ "Austin Legends",
162
+ "Austin Challengers",
163
+ "Austin Contenders",
164
+ "Austin Champions Autographs",
165
+ "Austin Challengers Autographs",
166
+ "Austin Legends Autographs",
167
+ "Austin Contenders Autographs",
168
+ ],
169
+ }
170
+
171
+ CAPSULE_INFO = {
172
+ "2020 RMR Sticker Capsule": RMR_CAPSULES,
173
+ "Stockholm Sticker Capsule": STOCKHOLM_CAPSULES,
174
+ "Antwerp Sticker Capsule": ANTWERP_CAPSULES,
175
+ "Rio Sticker Capsule": RIO_CAPSULES,
176
+ "Paris Sticker Capsule": PARIS_CAPSULES,
177
+ "Copenhagen Sticker Capsule": COPENHAGEN_CAPSULES,
178
+ "Shanghai Sticker Capsule": SHANGHAI_CAPSULES,
179
+ "Austin Sticker Capsule": AUSTIN_CAPSULES,
180
+ }
182
181
 
183
182
 
184
183
  CASE_PAGES = [
@@ -1,16 +1,16 @@
1
- [2020 RMR]
1
+ [2020 RMR Sticker Capsule]
2
2
  RMR_Challengers = 0
3
3
  RMR_Legends = 0
4
4
  RMR_Contenders = 0
5
5
 
6
- [Stockholm]
6
+ [Stockholm Sticker Capsule]
7
7
  Stockholm_Challengers = 0
8
8
  Stockholm_Legends = 0
9
9
  Stockholm_Contenders = 0
10
10
  Stockholm_Champions_Autographs = 0
11
11
  Stockholm_Finalists_Autographs = 0
12
12
 
13
- [Antwerp]
13
+ [Antwerp Sticker Capsule]
14
14
  Antwerp_Challengers = 0
15
15
  Antwerp_Legends = 0
16
16
  Antwerp_Contenders = 0
@@ -19,7 +19,7 @@ Antwerp_Contenders_Autographs = 0
19
19
  Antwerp_Challengers_Autographs = 0
20
20
  Antwerp_Legends_Autographs = 0
21
21
 
22
- [Rio]
22
+ [Rio Sticker Capsule]
23
23
  Rio_Challengers = 0
24
24
  Rio_Legends = 0
25
25
  Rio_Contenders = 0
@@ -28,7 +28,7 @@ Rio_Contenders_Autographs = 0
28
28
  Rio_Challengers_Autographs = 0
29
29
  Rio_Legends_Autographs = 0
30
30
 
31
- [Paris]
31
+ [Paris Sticker Capsule]
32
32
  Paris_Challengers = 0
33
33
  Paris_Legends = 0
34
34
  Paris_Contenders = 0
@@ -37,7 +37,7 @@ Paris_Contenders_Autographs = 0
37
37
  Paris_Challengers_Autographs = 0
38
38
  Paris_Legends_Autographs = 0
39
39
 
40
- [Copenhagen]
40
+ [Copenhagen Sticker Capsule]
41
41
  Copenhagen_Challengers = 0
42
42
  Copenhagen_Legends = 0
43
43
  Copenhagen_Contenders = 0
@@ -46,7 +46,7 @@ Copenhagen_Contenders_Autographs = 0
46
46
  Copenhagen_Challengers_Autographs = 0
47
47
  Copenhagen_Legends_Autographs = 0
48
48
 
49
- [Shanghai]
49
+ [Shanghai Sticker Capsule]
50
50
  Shanghai_Challengers = 0
51
51
  Shanghai_Legends = 0
52
52
  Shanghai_Contenders = 0
@@ -55,7 +55,7 @@ Shanghai_Contenders_Autographs = 0
55
55
  Shanghai_Challengers_Autographs = 0
56
56
  Shanghai_Legends_Autographs = 0
57
57
 
58
- [Austin]
58
+ [Austin Sticker Capsule]
59
59
  Austin_Challengers = 0
60
60
  Austin_Legends = 0
61
61
  Austin_Contenders = 0
@@ -108,6 +108,6 @@ Kilowatt_Case = 0
108
108
  Gallery_Case = 0
109
109
  Fever_Case = 0
110
110
 
111
- [Proxy API Key]
111
+ [Settings]
112
112
  Use_Proxy = False
113
113
  API_Key = None
cs2tracker/main.py CHANGED
@@ -3,11 +3,18 @@ from datetime import datetime
3
3
  import urllib3
4
4
  from rich.console import Console
5
5
 
6
- from ._version import version
7
- from .application import Application
6
+ from cs2tracker._version import version # pylint: disable=E0611
7
+ from cs2tracker.application import Application
8
8
 
9
9
 
10
10
  def main():
11
+ """
12
+ The main entry point for the CS2Tracker application.
13
+
14
+ Provides a console output with the application version and date, and initializes the
15
+ application.
16
+ """
17
+
11
18
  ## disable warnings for proxy requests
12
19
  urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
13
20
 
@@ -25,7 +32,7 @@ def main():
25
32
 
26
33
 
27
34
  """
28
- + f"Version: v{version} - {datetime.today().strftime('%Y/%m/%d')} - Jannik Novak @ashiven_\n"
35
+ + f"Version: v{version} - {datetime.today().strftime('%Y/%m/%d')} - Jannik Novak @ashiven\n"
29
36
  )
30
37
 
31
38
  application = Application()
cs2tracker/scraper.py CHANGED
@@ -1,338 +1,305 @@
1
- import configparser
2
1
  import csv
3
- import datetime
4
2
  import os
5
3
  import time
4
+ from configparser import ConfigParser
5
+ from datetime import datetime
6
6
 
7
- import requests
8
7
  from bs4 import BeautifulSoup
8
+ from bs4.element import Tag
9
9
  from currency_converter import CurrencyConverter
10
+ from requests import RequestException, Session
10
11
  from requests.adapters import HTTPAdapter, Retry
11
12
  from rich.console import Console
12
- from tenacity import retry, stop_after_attempt
13
+ from tenacity import RetryError, retry, stop_after_attempt
13
14
 
14
- from .constants import (
15
- CAPSULE_HREFS,
16
- CAPSULE_NAMES,
17
- CAPSULE_NAMES_GENERIC,
18
- CAPSULE_PAGES,
15
+ from cs2tracker.constants import (
16
+ CAPSULE_INFO,
19
17
  CASE_HREFS,
20
- CASE_NAMES,
21
18
  CASE_PAGES,
22
19
  CONFIG_FILE,
23
20
  OUTPUT_FILE,
24
21
  )
25
22
 
26
23
  MAX_LINE_LEN = 72
24
+ SEPARATOR = "-"
25
+ PRICE_INFO = "Owned: {} Steam market price: ${} Total: ${}\n"
27
26
 
28
27
 
29
28
  class Scraper:
30
29
  def __init__(self):
31
- self.api_key = None
32
- self.use_proxy = False
33
-
34
- self.case_quantities = []
35
- self.rmr_quantities = []
36
- self.stockholm_quantities = []
37
- self.antwerp_quantities = []
38
- self.rio_quantities = []
39
- self.paris_quantities = []
40
- self.copenhagen_quantities = []
41
- self.shanghai_quantities = []
42
- self.austin_quantities = []
43
-
44
- self.total_price = 0
45
- self.total_price_euro = 0
46
-
47
- self.session = requests.Session()
30
+ """Initialize the Scraper class."""
31
+ self.console = Console()
32
+ self.parse_config()
33
+ self._start_session()
34
+
35
+ self.usd_total = 0
36
+ self.eur_total = 0
37
+
38
+ def parse_config(self):
39
+ """Parse the configuration file to read settings and user-owned items."""
40
+ self.config = ConfigParser()
41
+ self.config.read(CONFIG_FILE)
42
+
43
+ def _start_session(self):
44
+ """Start a requests session with custom headers and retry logic."""
45
+ self.session = Session()
48
46
  self.session.headers.update(
49
47
  {
50
48
  "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36"
51
49
  }
52
50
  )
53
- retries = Retry(
54
- total=5, backoff_factor=0.1, status_forcelist=[500, 502, 503, 504, 520]
55
- )
51
+ retries = Retry(total=5, backoff_factor=0.1, status_forcelist=[500, 502, 503, 504, 520])
56
52
  self.session.mount("http://", HTTPAdapter(max_retries=retries))
57
53
  self.session.mount("https://", HTTPAdapter(max_retries=retries))
58
54
 
59
- self.console = Console()
60
-
61
- config = self.parse_config()
62
- self.set_config(config)
63
-
64
55
  def scrape_prices(self):
65
- for capsule_page_url in CAPSULE_PAGES:
66
- capsule_hrefs = (
67
- capsule_name
68
- ) = capsule_names_generic = capsule_quantities = None
69
- if "rmr" in capsule_page_url:
70
- capsule_name = "2020 RMR"
71
- capsule_quantities = self.rmr_quantities
72
- capsule_hrefs = CAPSULE_HREFS[0:3]
73
- capsule_names_generic = CAPSULE_NAMES_GENERIC[0:3]
74
- elif "stockholm" in capsule_page_url:
75
- capsule_name = "Stockholm"
76
- capsule_quantities = self.stockholm_quantities
77
- capsule_hrefs = CAPSULE_HREFS[3:8]
78
- capsule_names_generic = CAPSULE_NAMES_GENERIC[0:4] + [
79
- CAPSULE_NAMES_GENERIC[-1]
80
- ]
81
- elif "antwerp" in capsule_page_url:
82
- capsule_name = "Antwerp"
83
- capsule_quantities = self.antwerp_quantities
84
- capsule_hrefs = CAPSULE_HREFS[8:15]
85
- capsule_names_generic = CAPSULE_NAMES_GENERIC[0:7]
86
- elif "rio" in capsule_page_url:
87
- capsule_name = "Rio"
88
- capsule_quantities = self.rio_quantities
89
- capsule_hrefs = CAPSULE_HREFS[15:22]
90
- capsule_names_generic = CAPSULE_NAMES_GENERIC[0:7]
91
- elif "paris" in capsule_page_url:
92
- capsule_name = "Paris"
93
- capsule_quantities = self.paris_quantities
94
- capsule_hrefs = CAPSULE_HREFS[22:29]
95
- capsule_names_generic = CAPSULE_NAMES_GENERIC[0:7]
96
- elif "copenhagen" in capsule_page_url:
97
- capsule_name = "Copenhagen"
98
- capsule_quantities = self.copenhagen_quantities
99
- capsule_hrefs = CAPSULE_HREFS[29:36]
100
- capsule_names_generic = CAPSULE_NAMES_GENERIC[0:7]
101
- elif "shanghai" in capsule_page_url:
102
- capsule_name = "Shanghai"
103
- capsule_quantities = self.shanghai_quantities
104
- capsule_hrefs = CAPSULE_HREFS[36:43]
105
- capsule_names_generic = CAPSULE_NAMES_GENERIC[0:7]
106
- elif "austin" in capsule_page_url:
107
- capsule_name = "Austin"
108
- capsule_quantities = self.austin_quantities
109
- capsule_hrefs = CAPSULE_HREFS[43:50]
110
- capsule_names_generic = CAPSULE_NAMES_GENERIC[0:7]
111
-
112
- self._scrape_prices_capsule(
113
- capsule_page_url,
114
- capsule_hrefs,
115
- capsule_name,
116
- capsule_names_generic,
117
- capsule_quantities,
56
+ """Scrape prices for capsules and cases, calculate totals in USD and EUR, and
57
+ print/save the results.
58
+ """
59
+ capsule_usd_total = 0
60
+ try:
61
+ capsule_usd_total = self.scrape_capsule_section_prices()
62
+ except (RequestException, AttributeError, RetryError, ValueError):
63
+ self.console.print(
64
+ "[bold red]Failed to scrape capsule prices. (Consider using proxies to prevent rate limiting)\n"
118
65
  )
119
66
 
120
- self._scrape_prices_case(
121
- self.case_quantities, CASE_PAGES, CASE_HREFS, CASE_NAMES
122
- )
67
+ case_usd_total = 0
68
+ try:
69
+ case_usd_total = self._scrape_case_prices()
70
+ except (RequestException, AttributeError, RetryError, ValueError):
71
+ self.console.print(
72
+ "[bold red]Failed to scrape case prices. (Consider using proxies to prevent rate limiting)\n"
73
+ )
123
74
 
124
- def print_total(self):
125
- usd_string = "USD Total".center(MAX_LINE_LEN, "-")
126
- self.console.print(f"[bold green]{usd_string}")
127
- self.console.print(f"${self.total_price:.2f}")
75
+ self.usd_total += capsule_usd_total
76
+ self.usd_total += case_usd_total
77
+ self.eur_total = CurrencyConverter().convert(self.usd_total, "USD", "EUR")
128
78
 
129
- self.total_price_euro = CurrencyConverter().convert(
130
- self.total_price, "USD", "EUR"
131
- )
132
- eur_string = "EUR Total".center(MAX_LINE_LEN, "-")
133
- self.console.print(f"[bold green]{eur_string}")
134
- self.console.print(f"€{self.total_price_euro:.2f}")
135
- end_string = "-" * MAX_LINE_LEN
79
+ self._print_total()
80
+ self._save_price_log()
81
+
82
+ # reset totals for next run
83
+ self.usd_total, self.eur_total = 0, 0
84
+
85
+ def _print_total(self):
86
+ """Print the total prices in USD and EUR, formatted with titles and
87
+ separators.
88
+ """
89
+ usd_title = "USD Total".center(MAX_LINE_LEN, SEPARATOR)
90
+ self.console.print(f"[bold green]{usd_title}")
91
+ self.console.print(f"${self.usd_total:.2f}")
92
+
93
+ eur_title = "EUR Total".center(MAX_LINE_LEN, SEPARATOR)
94
+ self.console.print(f"[bold green]{eur_title}")
95
+ self.console.print(f"€{self.eur_total:.2f}")
96
+
97
+ end_string = SEPARATOR * MAX_LINE_LEN
136
98
  self.console.print(f"[bold green]{end_string}\n")
137
99
 
138
- def save_to_file(self):
139
- now = datetime.datetime.now()
140
- date = now.strftime("%Y-%m-%d")
100
+ def _save_price_log(self):
101
+ """
102
+ Save the current date and total prices in USD and EUR to a CSV file.
141
103
 
104
+ This will append a new entry to the output file if no entry has been made for
105
+ today.
106
+ """
142
107
  if not os.path.isfile(OUTPUT_FILE):
143
108
  open(OUTPUT_FILE, "w", encoding="utf-8").close()
144
109
 
145
- with open(OUTPUT_FILE, "r", encoding="utf-8") as csvfile:
146
- reader = csv.reader(csvfile)
110
+ with open(OUTPUT_FILE, "r", encoding="utf-8") as price_logs:
111
+ price_logs_reader = csv.reader(price_logs)
147
112
  last_row = None
148
- for row in reader:
113
+ for row in price_logs_reader:
149
114
  last_row = row
150
115
  if last_row:
151
- last_date_str = last_row[0][:10]
116
+ last_log_date = last_row[0][:10]
152
117
  else:
153
- last_date_str = ""
154
-
155
- if date != last_date_str:
156
- today = now.strftime("%Y-%m-%d %H:%M:%S")
157
- total = f"{self.total_price:.2f}$"
158
- total_euro = f"{self.total_price_euro:.2f}€"
159
- with open(OUTPUT_FILE, "a", newline="", encoding="utf-8") as csvfile:
160
- writer = csv.writer(csvfile)
161
- writer.writerow([today, total])
162
- writer.writerow([today, total_euro])
163
-
164
- # reset total prices for next run
165
- self.total_price = 0
166
- self.total_price_euro = 0
167
-
168
- def parse_config(self):
169
- config = configparser.ConfigParser()
170
- config.read(CONFIG_FILE)
171
- return config
118
+ last_log_date = ""
119
+
120
+ today = datetime.now().strftime("%Y-%m-%d")
121
+ if last_log_date != today:
122
+ with open(OUTPUT_FILE, "a", newline="", encoding="utf-8") as price_logs:
123
+ price_logs_writer = csv.writer(price_logs)
124
+ price_logs_writer.writerow([today, f"{self.usd_total:.2f}$"])
125
+ price_logs_writer.writerow([today, f"{self.eur_total:.2f}€"])
126
+
127
+ def read_price_log(self):
128
+ """
129
+ Parse the output file to extract dates, dollar prices, and euro prices. This
130
+ data is used for drawing the plot of past prices.
131
+
132
+ :return: A tuple containing three lists: dates, dollar prices, and euro prices.
133
+ """
134
+ if not os.path.isfile(OUTPUT_FILE):
135
+ open(OUTPUT_FILE, "w", encoding="utf-8").close()
172
136
 
173
- def set_config(self, config):
174
- self.use_proxy = (
175
- False if config.get("Proxy API Key", "Use_Proxy") == "False" else True
176
- )
177
- self.api_key = config.get("Proxy API Key", "API_Key")
178
-
179
- # reset all quantities in case this is called at runtime (edit config)
180
- self.case_quantities = []
181
- self.rmr_quantities = []
182
- self.stockholm_quantities = []
183
- self.antwerp_quantities = []
184
- self.rio_quantities = []
185
- self.paris_quantities = []
186
- self.copenhagen_quantities = []
187
- self.shanghai_quantities = []
188
- self.austin_quantities = []
189
-
190
- for capsule_name in CAPSULE_NAMES:
191
- config_capsule_name = capsule_name.replace(" ", "_")
192
- if "RMR" in capsule_name:
193
- self.rmr_quantities.append(
194
- int(config.get("2020 RMR", config_capsule_name))
195
- )
196
- elif "Stockholm" in capsule_name:
197
- self.stockholm_quantities.append(
198
- int(config.get("Stockholm", config_capsule_name))
199
- )
200
- elif "Antwerp" in capsule_name:
201
- self.antwerp_quantities.append(
202
- int(config.get("Antwerp", config_capsule_name))
203
- )
204
- elif "Rio" in capsule_name:
205
- self.rio_quantities.append(int(config.get("Rio", config_capsule_name)))
206
- elif "Paris" in capsule_name:
207
- self.paris_quantities.append(
208
- int(config.get("Paris", config_capsule_name))
209
- )
210
- elif "Copenhagen" in capsule_name:
211
- self.copenhagen_quantities.append(
212
- int(config.get("Copenhagen", config_capsule_name))
213
- )
214
- elif "Shanghai" in capsule_name:
215
- self.shanghai_quantities.append(
216
- int(config.get("Shanghai", config_capsule_name))
217
- )
218
- elif "Austin" in capsule_name:
219
- self.austin_quantities.append(
220
- int(config.get("Austin", config_capsule_name))
221
- )
222
-
223
- for case_name in CASE_NAMES:
224
- config_case_name = case_name.replace(" ", "_")
225
- self.case_quantities.append(int(config.get("Cases", config_case_name)))
137
+ dates, dollars, euros = [], [], []
138
+ with open(OUTPUT_FILE, "r", newline="", encoding="utf-8") as price_logs:
139
+ price_logs_reader = csv.reader(price_logs)
140
+ for row in price_logs_reader:
141
+ date, price_with_currency = row
142
+ date = datetime.strptime(date, "%Y-%m-%d")
143
+ price = float(price_with_currency.rstrip("$€"))
144
+ if price_with_currency.endswith("€"):
145
+ euros.append(price)
146
+ else:
147
+ dollars.append(price)
148
+ # Only append every second date since the dates are the same for euros and dollars
149
+ # and we want the length of dates to match the lengths of dollars and euros
150
+ dates.append(date)
151
+
152
+ return dates, dollars, euros
226
153
 
227
154
  @retry(stop=stop_after_attempt(10))
228
155
  def _get_page(self, url):
229
- if self.use_proxy:
156
+ """
157
+ Get the page content from the given URL, using a proxy if configured. If the
158
+ request fails, it will retry up to 10 times.
159
+
160
+ :param url: The URL to fetch the page from.
161
+ :return: The HTTP response object containing the page content.
162
+ :raises RequestException: If the request fails.
163
+ :raises RetryError: If the retry limit is reached.
164
+ """
165
+ use_proxy = self.config.getboolean("Settings", "Use_Proxy", fallback=False)
166
+ api_key = self.config.get("Settings", "API_Key", fallback=None)
167
+ if use_proxy and api_key:
230
168
  page = self.session.get(
231
169
  url=url,
232
170
  proxies={
233
- "http": f"http://{self.api_key}:@smartproxy.crawlbase.com:8012",
234
- "https": f"http://{self.api_key}:@smartproxy.crawlbase.com:8012",
171
+ "http": f"http://{api_key}:@smartproxy.crawlbase.com:8012",
172
+ "https": f"http://{api_key}:@smartproxy.crawlbase.com:8012",
235
173
  },
236
174
  verify=False,
237
175
  )
238
176
  else:
239
177
  page = self.session.get(url)
240
178
 
179
+ if not page.ok or not page.content:
180
+ status = page.status_code
181
+ self.console.print(f"[bold red][!] Failed to load page ({status}). Retrying...\n")
182
+ raise RequestException(f"Failed to load page: {url}")
183
+
241
184
  return page
242
185
 
243
- def _scrape_prices_capsule(
186
+ def _parse_capsule_price(self, capsule_page, capsule_href):
187
+ """
188
+ Parse the price of a capsule from the given page and href.
189
+
190
+ :param capsule_page: The HTTP response object containing the capsule page
191
+ content.
192
+ :param capsule_href: The href of the capsule listing to find the price for.
193
+ :return: The price of the capsule as a float.
194
+ :raises ValueError: If the capsule listing or price span cannot be found.
195
+ """
196
+ capsule_soup = BeautifulSoup(capsule_page.content, "html.parser")
197
+ capsule_listing = capsule_soup.find("a", attrs={"href": f"{capsule_href}"})
198
+ if not isinstance(capsule_listing, Tag):
199
+ raise ValueError(f"Failed to find capsule listing: {capsule_href}")
200
+
201
+ price_span = capsule_listing.find("span", attrs={"class": "normal_price"})
202
+ if not isinstance(price_span, Tag):
203
+ raise ValueError(f"Failed to find price span in capsule listing: {capsule_href}")
204
+
205
+ price_str = price_span.text.split()[2]
206
+ price = float(price_str.replace("$", ""))
207
+
208
+ return price
209
+
210
+ def _scrape_capsule_prices(
244
211
  self,
245
- capsule_page_url,
246
- capsule_hrefs,
247
- capsule_name,
248
- capsule_names_generic,
249
- capsule_quantities,
212
+ capsule_section,
213
+ capsule_info,
250
214
  ):
251
- if any([quantity > 0 for quantity in capsule_quantities]):
252
- title_string = capsule_name.center(MAX_LINE_LEN, "-")
253
- self.console.print(f"[bold magenta]{title_string}")
254
-
255
- page = self._get_page(capsule_page_url)
256
- soup = BeautifulSoup(page.content, "html.parser")
257
-
258
- for href_index, href in enumerate(capsule_hrefs):
259
- if capsule_quantities[href_index] > 0:
260
- try:
261
- listing = soup.find("a", attrs={"href": f"{href}"})
262
- retries = 0
263
- while not listing and retries < 5:
264
- self.console.print(
265
- f"[bold red][!] Failed to load page ({page.status_code}). Retrying...\n"
266
- )
267
- page = self._get_page(capsule_page_url)
268
- soup = BeautifulSoup(page.content, "html.parser")
269
- listing = soup.find("a", attrs={"href": f"{href}"})
270
- retries += 1
271
-
272
- price_span = listing.find(
273
- "span", attrs={"class": "normal_price"}
274
- )
275
- price_str = price_span.text.split()[2]
276
- price = float(price_str.replace("$", ""))
277
- price_total = round(
278
- float(capsule_quantities[href_index] * price), 2
279
- )
280
-
281
- self.console.print(
282
- f"[bold red]{capsule_names_generic[href_index]}"
283
- )
284
- self.console.print(
285
- f"Owned: {capsule_quantities[href_index]} Steam market price: ${price} Total: ${price_total}"
286
- )
287
-
288
- self.total_price += price_total
289
-
290
- except (AttributeError, ValueError):
291
- self.console.print("[bold red][!] Failed to find price listing")
292
- break
293
-
294
- self.console.print("\n")
295
-
296
- def _scrape_prices_case(
297
- self, case_quantities, case_page_urls, case_hrefs, case_names
298
- ):
299
- for index, case_quantity in enumerate(case_quantities):
300
- if case_quantity > 0:
301
- title_string = case_names[index].center(MAX_LINE_LEN, "-")
302
- self.console.print(f"[bold magenta]{title_string}")
303
-
304
- page = self._get_page(case_page_urls[index])
305
- soup = BeautifulSoup(page.content, "html.parser")
306
- listing = soup.find("a", attrs={"href": case_hrefs[index]})
307
- retries = 0
308
- while retries < 5:
309
- if not listing:
310
- self.console.print(
311
- f"[bold red][!] Failed to load page ({page.status_code}). Retrying...\n"
312
- )
313
- page = self._get_page(case_page_urls[index])
314
- soup = BeautifulSoup(page.content, "html.parser")
315
- listing = soup.find("a", attrs={"href": case_hrefs[index]})
316
- retries += 1
317
- else:
318
- break
319
-
320
- try:
321
- price_class = listing.find("span", attrs={"class": "normal_price"})
322
- price_str = price_class.text.split()[2]
323
- price = float(price_str.replace("$", ""))
324
- price_total = round(float(case_quantity * price), 2)
325
-
326
- self.console.print(
327
- f"Owned: {case_quantity} Steam market price: ${price} Total: ${price_total}"
328
- )
329
-
330
- self.total_price += price_total
331
-
332
- except (AttributeError, ValueError):
333
- self.console.print("[bold red][!] Failed to find price listing")
334
-
335
- self.console.print("\n")
336
-
337
- if not self.use_proxy:
338
- time.sleep(1)
215
+ """
216
+ Scrape prices for a specific capsule section, printing the details to the
217
+ console.
218
+
219
+ :param capsule_section: The section name in the config for the capsule.
220
+ :param capsule_info: A dictionary containing information about the capsule
221
+ section,
222
+ """
223
+ capsule_title = capsule_section.center(MAX_LINE_LEN, SEPARATOR)
224
+ self.console.print(f"[bold magenta]{capsule_title}")
225
+
226
+ capsule_price_total = 0
227
+ capsule_page = capsule_info["page"]
228
+ capsule_names = capsule_info["names"]
229
+ capsule_hrefs = capsule_info["items"]
230
+ capsule_page = self._get_page(capsule_page)
231
+ for capsule_name, capsule_href in zip(capsule_names, capsule_hrefs):
232
+ config_capsule_name = capsule_name.replace(" ", "_")
233
+ owned = self.config.getint(capsule_section, config_capsule_name, fallback=0)
234
+ if owned == 0:
235
+ continue
236
+
237
+ price_usd = self._parse_capsule_price(capsule_page, capsule_href)
238
+ price_usd_owned = round(float(owned * price_usd), 2)
239
+
240
+ self.console.print(f"[bold deep_sky_blue4]{capsule_name}")
241
+ self.console.print(PRICE_INFO.format(owned, price_usd, price_usd_owned))
242
+ capsule_price_total += price_usd_owned
243
+
244
+ return capsule_price_total
245
+
246
+ def scrape_capsule_section_prices(self):
247
+ """Scrape prices for all capsule sections defined in the configuration."""
248
+ capsule_usd_total = 0
249
+ for capsule_section, capsule_info in CAPSULE_INFO.items():
250
+ # only scrape capsule sections where the user owns at least one item
251
+ if any(int(owned) > 0 for _, owned in self.config.items(capsule_section)):
252
+ capsule_usd_total += self._scrape_capsule_prices(capsule_section, capsule_info)
253
+
254
+ return capsule_usd_total
255
+
256
+ def _parse_case_price(self, case_page, case_href):
257
+ """
258
+ Parse the price of a case from the given page and href.
259
+
260
+ :param case_page: The HTTP response object containing the case page content.
261
+ :param case_href: The href of the case listing to find the price for.
262
+ :return: The price of the case as a float.
263
+ :raises ValueError: If the case listing or price span cannot be found.
264
+ """
265
+ case_soup = BeautifulSoup(case_page.content, "html.parser")
266
+ case_listing = case_soup.find("a", attrs={"href": case_href})
267
+ if not isinstance(case_listing, Tag):
268
+ raise ValueError(f"Failed to find case listing: {case_href}")
269
+
270
+ price_class = case_listing.find("span", attrs={"class": "normal_price"})
271
+ if not isinstance(price_class, Tag):
272
+ raise ValueError(f"Failed to find price class in case listing: {case_href}")
273
+
274
+ price_str = price_class.text.split()[2]
275
+ price = float(price_str.replace("$", ""))
276
+
277
+ return price
278
+
279
+ def _scrape_case_prices(self):
280
+ """
281
+ Scrape prices for all cases defined in the configuration.
282
+
283
+ For each case, it prints the case name, owned count, price per item, and total
284
+ price for owned items.
285
+ """
286
+ case_price_total = 0
287
+ for case_index, (config_case_name, owned) in enumerate(self.config.items("Cases")):
288
+ if int(owned) == 0:
289
+ continue
290
+
291
+ case_name = config_case_name.replace("_", " ").title()
292
+ case_title = case_name.center(MAX_LINE_LEN, SEPARATOR)
293
+ self.console.print(f"[bold magenta]{case_title}")
294
+
295
+ case_page = self._get_page(CASE_PAGES[case_index])
296
+ price_usd = self._parse_case_price(case_page, CASE_HREFS[case_index])
297
+ price_usd_owned = round(float(int(owned) * price_usd), 2)
298
+
299
+ self.console.print(PRICE_INFO.format(owned, price_usd, price_usd_owned))
300
+ case_price_total += price_usd_owned
301
+
302
+ if not self.config.getboolean("Settings", "Use_Proxy", fallback=False):
303
+ time.sleep(1)
304
+
305
+ return case_price_total
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cs2tracker
3
- Version: 2.1.1
3
+ Version: 2.1.2
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
@@ -11,6 +11,7 @@ Classifier: Programming Language :: Python :: 3.11
11
11
  Requires-Python: >=3.11
12
12
  Description-Content-Type: text/markdown
13
13
  License-File: LICENSE.md
14
+ Requires-Dist: numpy==1.26.4
14
15
  Requires-Dist: beautifulsoup4==4.11.1
15
16
  Requires-Dist: CurrencyConverter==0.17.9
16
17
  Requires-Dist: matplotlib==3.7.0
@@ -20,16 +21,21 @@ Requires-Dist: tenacity==8.2.2
20
21
  Requires-Dist: urllib3==2.1.0
21
22
  Dynamic: license-file
22
23
 
24
+ <div align="center">
25
+
23
26
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
27
+ [![GitHub Release](https://img.shields.io/github/v/release/ashiven/cs2tracker)](https://github.com/ashiven/cs2tracker/releases)
24
28
  [![PyPI version](https://badge.fury.io/py/cs2tracker.svg)](https://badge.fury.io/py/cs2tracker)
29
+ [![GitHub Issues or Pull Requests](https://img.shields.io/github/issues/ashiven/cs2tracker)](https://github.com/ashiven/cs2tracker/issues)
30
+ [![GitHub Issues or Pull Requests](https://img.shields.io/github/issues-pr/ashiven/cs2tracker)](https://github.com/ashiven/cs2tracker/pulls)
31
+ ![GitHub Repo stars](https://img.shields.io/github/stars/ashiven/cs2tracker)
32
+
33
+ </div>
25
34
 
26
35
  ## About
27
36
 
28
37
  **CS2Tracker** is a tool that can be used to keep track of the steam market prices of your CS2 investment.
29
38
 
30
- ![demo](https://github.com/user-attachments/assets/6bd13c96-55ea-4857-8910-f97f5ce78704)
31
-
32
-
33
39
  ## Getting Started
34
40
 
35
41
  ### Prerequisites
@@ -0,0 +1,14 @@
1
+ cs2tracker/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ cs2tracker/__main__.py,sha256=Ub--oSMv48YzfWF1CZqYlkn1-HvZ7Bhxoc7urn1oY6o,249
3
+ cs2tracker/_version.py,sha256=R_mUEqlkeWeuDiw8DRbHmYUvRxDtmQQJqhTpV8pds-Y,511
4
+ cs2tracker/application.py,sha256=8nPUPcPOnatg98_XfSpWjD06-V0jDoy7CT_DNSGuszk,3431
5
+ cs2tracker/constants.py,sha256=8jqaMM9eKEmqNTGWQ_h9EiSqYTIEPOAhe5l1zg2ycko,14953
6
+ cs2tracker/main.py,sha256=T4X-oLYfltnu0ALBTIJagZTLFmWMhxZrfD1pfFLAUpg,1310
7
+ cs2tracker/scraper.py,sha256=XiRjYSLkBd_DFjJ-1951D_3iIaL5jDMMRWCqgWGBMJ0,12201
8
+ cs2tracker/data/config.ini,sha256=zieID1dhlEAquPCXbpmvsnvxJAnrDqHSNrS6AIj1F-Q,2637
9
+ cs2tracker-2.1.2.dist-info/licenses/LICENSE.md,sha256=G5wqQ_8KGA808kVuF-Fpu_Yhteg8K_5ux9n2v8eQK7s,1069
10
+ cs2tracker-2.1.2.dist-info/METADATA,sha256=bS8LgXrOUv0_t84kiUfU6Vybyoq3U8Q4tIqeeKJMMrs,2781
11
+ cs2tracker-2.1.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
12
+ cs2tracker-2.1.2.dist-info/entry_points.txt,sha256=K8IwDIkg8QztSB9g9c89B9jR_2pG4QyJGrNs4z5RcZw,63
13
+ cs2tracker-2.1.2.dist-info/top_level.txt,sha256=2HB4xDDOxaU5BDc_yvdi9UlYLgL768n8aR-hRhFM6VQ,11
14
+ cs2tracker-2.1.2.dist-info/RECORD,,
@@ -1,14 +0,0 @@
1
- cs2tracker/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- cs2tracker/__main__.py,sha256=gvsnabJSDMbzKEcB7i3ET7AoV5OqYwUjIrkLfmN0R-U,111
3
- cs2tracker/_version.py,sha256=e93DN6a88mHp4XiW8PUMNpgcL72dYKhIYIiM2vzzNTM,511
4
- cs2tracker/application.py,sha256=sOOsZxigUSkjoAqwhUKcSw5IiZXrXoheYJmhvXJ5gZ8,3739
5
- cs2tracker/constants.py,sha256=GmCiwmzZblQT8mTXvmuQX-Bho7isBQTsRw5YmETvh_E,14881
6
- cs2tracker/main.py,sha256=aB-xglyrrlKwx-WSrsw5YvZUbK5cVKIQ_WuhCJJy3Eo,1085
7
- cs2tracker/scraper.py,sha256=W5GeqlA0XTGc3uNvzqxYjkNVbyJVNPiOAWXsIBN70bQ,13184
8
- cs2tracker/data/config.ini,sha256=j0aXl-MkZILsJ0cj6w-6b5HbqLuLl73cewVpk204Ick,2514
9
- cs2tracker-2.1.1.dist-info/licenses/LICENSE.md,sha256=G5wqQ_8KGA808kVuF-Fpu_Yhteg8K_5ux9n2v8eQK7s,1069
10
- cs2tracker-2.1.1.dist-info/METADATA,sha256=6MLVDmUHwe7yBJcuv6eWvLCXMihmg7yMUEojiqkDZNo,2328
11
- cs2tracker-2.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
12
- cs2tracker-2.1.1.dist-info/entry_points.txt,sha256=K8IwDIkg8QztSB9g9c89B9jR_2pG4QyJGrNs4z5RcZw,63
13
- cs2tracker-2.1.1.dist-info/top_level.txt,sha256=2HB4xDDOxaU5BDc_yvdi9UlYLgL768n8aR-hRhFM6VQ,11
14
- cs2tracker-2.1.1.dist-info/RECORD,,