cs2tracker 2.1.14__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/logs.py ADDED
@@ -0,0 +1,143 @@
1
+ import csv
2
+ from datetime import datetime
3
+
4
+ from cs2tracker.config import get_config
5
+ from cs2tracker.constants import OUTPUT_FILE
6
+ from cs2tracker.scraper.parser import Parser
7
+ from cs2tracker.util.currency_conversion import convert, to_symbol
8
+
9
+ config = get_config()
10
+
11
+
12
+ class PriceLogs:
13
+ @classmethod
14
+ def _append_latest_calculation(cls, date, usd_totals):
15
+ """Append the first price calculation of the day."""
16
+ with open(OUTPUT_FILE, "a", newline="", encoding="utf-8") as price_logs:
17
+ price_logs_writer = csv.writer(price_logs)
18
+ price_entries_today = [f"{usd_total:.2f}$" for usd_total in usd_totals]
19
+ price_logs_writer.writerow([date] + price_entries_today)
20
+
21
+ @classmethod
22
+ def _replace_latest_calculation(cls, date, usd_totals):
23
+ """Replace the last calculation of today with the most recent one of today."""
24
+ with open(OUTPUT_FILE, "r+", newline="", encoding="utf-8") as price_logs:
25
+ price_logs_reader = csv.reader(price_logs)
26
+ rows = list(price_logs_reader)
27
+ rows_without_today = rows[:-1]
28
+ price_logs.seek(0)
29
+ price_logs.truncate()
30
+
31
+ price_logs_writer = csv.writer(price_logs)
32
+ price_logs_writer.writerows(rows_without_today)
33
+ price_entries_today = [f"{usd_total:.2f}$" for usd_total in usd_totals]
34
+ price_logs_writer.writerow([date] + price_entries_today)
35
+
36
+ @classmethod
37
+ def save(cls, usd_totals):
38
+ """
39
+ Save the current date and total prices in USD to a CSV file.
40
+
41
+ This will append a new entry to the output file if no entry has been made for
42
+ today.
43
+
44
+ :param usd_totals: The total prices in USD to save.
45
+ :raises FileNotFoundError: If the output file does not exist.
46
+ :raises IOError: If there is an error writing to the output file.
47
+ """
48
+ with open(OUTPUT_FILE, "r", encoding="utf-8") as price_logs:
49
+ price_logs_reader = csv.reader(price_logs)
50
+ rows = list(price_logs_reader)
51
+ last_log_date = rows[-1][0] if rows else ""
52
+
53
+ today = datetime.now().strftime("%Y-%m-%d")
54
+ if last_log_date != today:
55
+ cls._append_latest_calculation(today, usd_totals)
56
+ else:
57
+ cls._replace_latest_calculation(today, usd_totals)
58
+
59
+ @classmethod
60
+ def read(cls, newest_first=False, with_symbols=False):
61
+ """
62
+ Parse the output file to extract dates, dollar prices, and the converted
63
+ currency prices. This data is used for drawing the plot of past prices.
64
+
65
+ :param newest_first: If True, the dates and totals will be returned in reverse
66
+ order
67
+ :param with_symbols: If True, the prices will be formatted with currency symbols
68
+ :return: A tuple containing dates and a dictionary of totals for each price
69
+ source.
70
+ :raises FileNotFoundError: If the output file does not exist.
71
+ :raises IOError: If there is an error reading the output file.
72
+ """
73
+ conversion_currency = config.conversion_currency
74
+ dates = []
75
+ totals = {
76
+ price_source: {"USD": [], conversion_currency: []} for price_source in Parser.SOURCES
77
+ }
78
+
79
+ with open(OUTPUT_FILE, "r", encoding="utf-8") as price_logs:
80
+ price_logs_reader = csv.reader(price_logs)
81
+ for row in price_logs_reader:
82
+ date, *usd_totals = row
83
+ date = datetime.strptime(date, "%Y-%m-%d")
84
+
85
+ usd_totals = [float(price_usd.rstrip("$")) for price_usd in usd_totals]
86
+ converted_totals = [
87
+ convert(price_usd, "USD", conversion_currency) for price_usd in usd_totals
88
+ ]
89
+
90
+ dates.append(date)
91
+ for price_source_index, price_source in enumerate(Parser.SOURCES):
92
+ totals[price_source]["USD"].append(usd_totals[price_source_index])
93
+ totals[price_source][conversion_currency].append(
94
+ converted_totals[price_source_index]
95
+ )
96
+
97
+ if newest_first:
98
+ dates.reverse()
99
+ for price_source in Parser.SOURCES:
100
+ totals[price_source]["USD"].reverse()
101
+ totals[price_source][conversion_currency].reverse()
102
+
103
+ if with_symbols:
104
+ for price_source in Parser.SOURCES:
105
+ totals[price_source]["USD"] = [
106
+ f"${price:.2f}" for price in totals[price_source]["USD"]
107
+ ]
108
+ totals[price_source][conversion_currency] = [
109
+ f"{to_symbol(conversion_currency)}{price:.2f}"
110
+ for price in totals[price_source][conversion_currency]
111
+ ]
112
+
113
+ return dates, totals
114
+
115
+ @classmethod
116
+ def validate_file(cls, log_file_path):
117
+ # pylint: disable=expression-not-assigned
118
+ """
119
+ Ensures that the provided price log file has the right format. This should be
120
+ used before importing a price log file to ensure it is valid.
121
+
122
+ :param log_file_path: The path to the price log file to validate.
123
+ :return: True if the price log file is valid, False otherwise.
124
+ """
125
+ try:
126
+ with open(log_file_path, "r", encoding="utf-8") as price_logs:
127
+ price_logs_reader = csv.reader(price_logs)
128
+ for row in price_logs_reader:
129
+ date_str, *usd_totals = row
130
+ datetime.strptime(date_str, "%Y-%m-%d")
131
+ [float(price_usd.rstrip("$")) for price_usd in usd_totals]
132
+ except (FileNotFoundError, IOError, ValueError, TypeError):
133
+ return False
134
+ except Exception:
135
+ return False
136
+
137
+ return True
138
+
139
+ @classmethod
140
+ def empty(cls):
141
+ """Checks if the price history is empty and returns True if it is."""
142
+ with open(OUTPUT_FILE, "r", encoding="utf-8") as price_logs:
143
+ return len(list(price_logs)) == 0
cs2tracker/main.py CHANGED
@@ -2,10 +2,10 @@ import sys
2
2
 
3
3
  import urllib3
4
4
 
5
- from cs2tracker.app import Application
5
+ from cs2tracker.app.app import Application
6
6
  from cs2tracker.constants import AUTHOR_STRING, BANNER, OS, OSType
7
- from cs2tracker.scraper import Scraper
8
- from cs2tracker.util import get_console
7
+ from cs2tracker.scraper.scraper import Scraper
8
+ from cs2tracker.util.padded_console import get_console
9
9
 
10
10
 
11
11
  def main():
@@ -1,9 +0,0 @@
1
- from cs2tracker.scraper.background_task import ( # noqa: F401 # pylint:disable=unused-import
2
- BackgroundTask,
3
- )
4
- from cs2tracker.scraper.discord_notifier import ( # noqa: F401 # pylint:disable=unused-import
5
- DiscordNotifier,
6
- )
7
- from cs2tracker.scraper.scraper import ( # noqa: F401 # pylint:disable=unused-import
8
- Scraper,
9
- )
@@ -9,7 +9,7 @@ from cs2tracker.constants import (
9
9
  RUNNING_IN_EXE,
10
10
  OSType,
11
11
  )
12
- from cs2tracker.util import get_console
12
+ from cs2tracker.util.padded_console import get_console
13
13
 
14
14
  WIN_BACKGROUND_TASK_NAME = "CS2Tracker Daily Calculation"
15
15
  WIN_BACKGROUND_TASK_SCHEDULE = "DAILY"
@@ -1,13 +1,16 @@
1
1
  import requests
2
2
  from requests.exceptions import RequestException
3
3
 
4
- from cs2tracker.util import PriceLogs, get_console
4
+ from cs2tracker.config import get_config
5
+ from cs2tracker.logs import PriceLogs
6
+ from cs2tracker.util.padded_console import get_console
5
7
 
6
8
  DC_WEBHOOK_USERNAME = "CS2Tracker"
7
9
  DC_WEBHOOK_AVATAR_URL = "https://img.icons8.com/?size=100&id=uWQJp2tLXUH6&format=png&color=000000"
8
10
  DC_RECENT_HISTORY_LIMIT = 5
9
11
 
10
12
  console = get_console()
13
+ config = get_config()
11
14
 
12
15
 
13
16
  class DiscordNotifier:
@@ -19,42 +22,41 @@ class DiscordNotifier:
19
22
 
20
23
  :return: A list of embeds for the Discord message.
21
24
  """
22
- dates, usd_prices, eur_prices = PriceLogs.read()
23
- dates, usd_prices, eur_prices = reversed(dates), reversed(usd_prices), reversed(eur_prices)
25
+ dates, totals = PriceLogs.read(newest_first=True, with_symbols=True)
24
26
 
25
- date_history, usd_history, eur_history = [], [], []
26
- for date, usd_log, eur_log in zip(dates, usd_prices, eur_prices):
27
- if len(date_history) >= DC_RECENT_HISTORY_LIMIT:
28
- break
29
- date_history.append(date.strftime("%Y-%m-%d"))
30
- usd_history.append(f"${usd_log:.2f}")
31
- eur_history.append(f"€{eur_log:.2f}")
32
-
33
- date_history = "\n".join(date_history)
34
- usd_history = "\n".join(usd_history)
35
- eur_history = "\n".join(eur_history)
27
+ date_field = [
28
+ {
29
+ "name": "Date",
30
+ "value": "\n".join([date.strftime("%Y-%m-%d") for date in dates][:DC_RECENT_HISTORY_LIMIT]), # type: ignore
31
+ "inline": True,
32
+ },
33
+ ]
34
+ price_fields = [
35
+ {
36
+ "name": f"{price_source.name.title()} (USD | {config.conversion_currency})",
37
+ "value": "\n".join(
38
+ [
39
+ f"{usd_total} | {converted_total}"
40
+ for usd_total, converted_total in zip(
41
+ totals[price_source]["USD"][:DC_RECENT_HISTORY_LIMIT],
42
+ totals[price_source][config.conversion_currency][
43
+ :DC_RECENT_HISTORY_LIMIT
44
+ ],
45
+ )
46
+ ]
47
+ ),
48
+ "inline": True,
49
+ }
50
+ for price_source in totals
51
+ ][
52
+ :2
53
+ ] # Limit to the first two price sources because Discord can only display 3 fields per line (Date + 2 Price Sources)
36
54
 
37
55
  embeds = [
38
56
  {
39
- "title": "📊 Recent Price History",
57
+ "title": "📊 Recent Investment History",
40
58
  "color": 5814783,
41
- "fields": [
42
- {
43
- "name": "Date",
44
- "value": date_history,
45
- "inline": True,
46
- },
47
- {
48
- "name": "USD Total",
49
- "value": usd_history,
50
- "inline": True,
51
- },
52
- {
53
- "name": "EUR Total",
54
- "value": eur_history,
55
- "inline": True,
56
- },
57
- ],
59
+ "fields": date_field + price_fields,
58
60
  }
59
61
  ]
60
62
 
@@ -5,9 +5,9 @@ from urllib.parse import unquote
5
5
  from bs4 import BeautifulSoup
6
6
  from bs4.element import Tag
7
7
 
8
+ from cs2tracker.config import get_config
8
9
  from cs2tracker.constants import CAPSULE_PAGES
9
- from cs2tracker.util import get_console
10
- from cs2tracker.util.validated_config import get_config
10
+ from cs2tracker.util.padded_console import get_console
11
11
 
12
12
  config = get_config()
13
13
  console = get_console()
@@ -17,9 +17,11 @@ class PriceSource(Enum):
17
17
  STEAM = "steam"
18
18
  BUFF163 = "buff163"
19
19
  SKINPORT = "skinport"
20
+ YOUPIN898 = "youpin"
21
+ CSFLOAT = "csfloat"
20
22
 
21
23
 
22
- class Parser(ABC):
24
+ class BaseParser(ABC):
23
25
  @classmethod
24
26
  @abstractmethod
25
27
  def get_item_page_url(cls, item_href, source=PriceSource.STEAM) -> str:
@@ -46,7 +48,7 @@ class Parser(ABC):
46
48
  """
47
49
 
48
50
 
49
- class SteamParser(Parser):
51
+ class SteamParser(BaseParser):
50
52
  STEAM_MARKET_SEARCH_PAGE_BASE_URL = "https://steamcommunity.com/market/search?q={}"
51
53
  PRICE_INFO = "Owned: {:<10} {} price: ${:<10} Total: ${:<10}"
52
54
  NEEDS_TIMEOUT = True
@@ -60,7 +62,7 @@ class SteamParser(Parser):
60
62
  # Therefore, if the provided item is a sticker capsule we return a search page defined in CAPSULE_PAGES
61
63
  # where all of the sticker capsules of one section are listed
62
64
  for section in config.sections():
63
- if section in ("Custom Items", "Cases", "User Settings", "App Settings"):
65
+ if section in ("Skins", "Stickers", "Cases", "User Settings", "App Settings"):
64
66
  continue
65
67
  if any(item_href == option for option in config.options(section)):
66
68
  return CAPSULE_PAGES[section]
@@ -89,24 +91,7 @@ class SteamParser(Parser):
89
91
  return price
90
92
 
91
93
 
92
- class SkinLedgerParser(Parser):
93
- SKINLEDGER_PRICE_LIST = ""
94
- PRICE_INFO = "Owned: {:<10} {} price: ${:<10} Total: ${:<10}"
95
- NEEDS_TIMEOUT = False
96
- SOURCES = [PriceSource.STEAM, PriceSource.BUFF163, PriceSource.SKINPORT]
97
-
98
- @classmethod
99
- def get_item_page_url(cls, item_href, source=PriceSource.STEAM) -> str:
100
- _ = source
101
- return super().get_item_page_url(item_href)
102
-
103
- @classmethod
104
- def parse_item_price(cls, item_page, item_href, source=PriceSource.STEAM) -> float:
105
- _, _ = item_href, source
106
- return super().parse_item_price(item_page, item_href)
107
-
108
-
109
- class ClashParser(Parser):
94
+ class ClashParser(BaseParser):
110
95
  CLASH_ITEM_API_BASE_URL = "https://inventory.clash.gg/api/GetItemPrice?id={}"
111
96
  PRICE_INFO = "Owned: {:<10} {} price: ${:<10} Total: ${:<10}"
112
97
  NEEDS_TIMEOUT = True
@@ -138,11 +123,11 @@ class ClashParser(Parser):
138
123
  return price
139
124
 
140
125
 
141
- class CSGOTrader(Parser):
126
+ class CSGOTraderParser(BaseParser):
142
127
  CSGOTRADER_PRICE_LIST = "https://prices.csgotrader.app/latest/{}.json"
143
128
  PRICE_INFO = "Owned: {:<10} {:<10}: ${:<10} Total: ${:<10}"
144
129
  NEEDS_TIMEOUT = False
145
- SOURCES = [PriceSource.STEAM, PriceSource.BUFF163, PriceSource.SKINPORT]
130
+ SOURCES = [PriceSource.STEAM, PriceSource.BUFF163, PriceSource.YOUPIN898]
146
131
 
147
132
  @classmethod
148
133
  def get_item_page_url(cls, item_href, source=PriceSource.STEAM):
@@ -154,6 +139,7 @@ class CSGOTrader(Parser):
154
139
 
155
140
  @classmethod
156
141
  def parse_item_price(cls, item_page, item_href, source=PriceSource.STEAM):
142
+ # pylint: disable=too-many-branches
157
143
  _ = source
158
144
 
159
145
  price_list = item_page.json()
@@ -167,13 +153,14 @@ class CSGOTrader(Parser):
167
153
  raise ValueError(f"CSGOTrader: Could not find item price info: {url_decoded_name}")
168
154
 
169
155
  if source == PriceSource.STEAM:
170
- price = price_info.get("last_24h")
171
- if not price:
172
- price = price_info.get("last_7d")
173
- if not price:
174
- raise ValueError(
175
- f"CSGOTrader: Could not find steam price of the past 7 days: {url_decoded_name}"
176
- )
156
+ for timestamp in ("last_24h", "last_7d", "last_30d", "last_90d"):
157
+ price = price_info.get(timestamp)
158
+ if price:
159
+ break
160
+ else:
161
+ raise ValueError(
162
+ f"CSGOTrader: Could not find steam price info for the past 3 months: {url_decoded_name}"
163
+ )
177
164
  elif source == PriceSource.BUFF163:
178
165
  price = price_info.get("starting_at")
179
166
  if not price:
@@ -183,6 +170,12 @@ class CSGOTrader(Parser):
183
170
  raise ValueError(
184
171
  f"CSGOTrader: Could not find recent buff163 price: {url_decoded_name}"
185
172
  )
173
+ elif source == PriceSource.YOUPIN898:
174
+ price = price_info
175
+ if not price:
176
+ raise ValueError(
177
+ f"CSGOTrader: Could not find recent youpin898 price: {url_decoded_name}"
178
+ )
186
179
  else:
187
180
  price = price_info.get("starting_at")
188
181
  if not price:
@@ -190,3 +183,7 @@ class CSGOTrader(Parser):
190
183
 
191
184
  price = float(price)
192
185
  return price
186
+
187
+
188
+ # Default parser used by the scraper
189
+ Parser = CSGOTraderParser