smog-usage-stats 1.0.0__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.
@@ -0,0 +1,134 @@
1
+ import requests
2
+ from typing import Literal
3
+ from Search import Search
4
+ from UsageStatsLookup import BaseStatsSearch
5
+
6
+ # TODO: Account for rating when gen is current gen to ONLY be 1695, otherwise be 1630
7
+
8
+
9
+ # Chaos searches are basically individual pokemon lookups, where one can get deeper analysis
10
+ # such as movesets, held items, EV spreads, etc
11
+ class ChaosSearch(Search):
12
+ def __init__(
13
+ self,
14
+ year: str | int,
15
+ month: Literal["Must be a two digit month string eg: '01' for January"],
16
+ gen: str | int,
17
+ ) -> None:
18
+ super().__init__(year, month, gen)
19
+
20
+ def search(self, pokemon: str) -> object | None:
21
+ res = requests.get(self.base)
22
+ page_object = res.json()["data"]
23
+ page_object = self.lower_keys(page_object)
24
+
25
+ try:
26
+ lookup = page_object[pokemon.lower()]
27
+ return lookup
28
+ except:
29
+ return None
30
+
31
+ # We will be doing this a lot so make it a function.
32
+ def lower_keys(self, dictionary: dict) -> dict:
33
+ """
34
+ Returns a dictionary with all keys lower-cased.
35
+ """
36
+ return {k.lower(): v for k, v in dictionary.items()}
37
+
38
+
39
+ class BaseChaosSearch(ChaosSearch):
40
+ def __init__(
41
+ self,
42
+ year: str | int,
43
+ month: Literal["Must be a two digit month string eg: '01' for January"],
44
+ gen: str | int,
45
+ tier: str,
46
+ ) -> None:
47
+ super().__init__(year, month, gen)
48
+ self.tier = tier.lower()
49
+ self.base = f"https://www.smogon.com/stats/{self.year}-{self.month}/chaos/"
50
+
51
+ @property
52
+ def tier(self) -> str:
53
+ return self._tier
54
+
55
+ @tier.setter
56
+ def tier(self, value):
57
+ self._tier = value
58
+
59
+ def build_url(self):
60
+ self.base += f"gen{self.gen}{self.tier}-1500.json"
61
+
62
+
63
+ class MonotypeChaosSearch(ChaosSearch):
64
+ def __init__(
65
+ self,
66
+ year: str | int,
67
+ month: Literal["Must be a two digit month string eg: '01' for January"],
68
+ gen: str | int,
69
+ typing: str,
70
+ ) -> None:
71
+ super().__init__(year, month, gen)
72
+ self.base = (
73
+ f"https://www.smogon.com/stats/{self.year}-{self.month}/monotype/chaos/"
74
+ )
75
+ self.typing = typing.lower()
76
+
77
+ @property
78
+ def typing(self) -> str:
79
+ return self._typing
80
+
81
+ @typing.setter
82
+ def typing(self, value):
83
+ self._typing = value
84
+
85
+ def build_url(self):
86
+ self.base += f"gen{self.gen}monotype-mono{self.typing}-1500.json"
87
+
88
+
89
+ # this is to search stats individually, but its a bit more involved, a WIP
90
+ class IndividualStatsLookup(BaseStatsSearch):
91
+ def __init__(
92
+ self,
93
+ year: str | int,
94
+ month: Literal["Must be a two digit month string eg: '01' for Janu…"],
95
+ gen: str | int,
96
+ tier: str,
97
+ name: str,
98
+ ) -> None:
99
+ super().__init__(year, month, gen, tier)
100
+ self.name = name.lower()
101
+
102
+ @property
103
+ def name(self):
104
+ return self._name
105
+
106
+ @name.setter
107
+ def name(self, value):
108
+ self._name = value.lower()
109
+
110
+ def find_individual_usage(self, resmatrix: list[list] = None) -> dict | None:
111
+ # resmatrix is the resulting 2d array from the self.search() function
112
+ if not resmatrix:
113
+ resmatrix = self.search()
114
+ for m in resmatrix:
115
+ if m[1] == self.name:
116
+ return {m[1]: dict(zip(resmatrix[0], m))}
117
+ print("No result found.")
118
+ return None
119
+
120
+
121
+ if __name__ == "__main__":
122
+ # search = BaseChaosSearch("2022", "07", 8, "uu")
123
+ # search.build_url()
124
+ # result = search.search("SKARMORY")
125
+ # print(result)
126
+
127
+ # monosearch = MonotypeChaosSearch(year=2022, month="11", gen=9, typing="fairy")
128
+ # monosearch.build_url()
129
+ # monoresult = monosearch.search("Gardevoir")
130
+ # print(monoresult)
131
+
132
+ indysearch = IndividualStatsLookup(2022, "11", "8", "ou", "scizor")
133
+ result = indysearch.find_individual_usage()
134
+ print(result)
@@ -0,0 +1,139 @@
1
+ import os
2
+ import psycopg2
3
+ from dotenv import load_dotenv
4
+ from os.path import dirname as up
5
+
6
+ try:
7
+ load_dotenv()
8
+ except:
9
+ print("No local environment variables found.")
10
+
11
+
12
+ _COLUMNS = (
13
+ "rank",
14
+ "pokemon",
15
+ "usage_pct",
16
+ "raw_usage",
17
+ "raw_pct",
18
+ "real",
19
+ "real_pct",
20
+ "date",
21
+ "tier"
22
+ )
23
+
24
+
25
+ class SQLInterface:
26
+ def __init__(
27
+ self,
28
+ db_name: str = None,
29
+ username: str = None,
30
+ pwd: str = None,
31
+ host: str = None,
32
+ port: str = None,
33
+ ) -> None:
34
+ self.db_name = db_name
35
+ self.username = username
36
+ self.pwd = pwd
37
+ self.host = host
38
+ self.port = port
39
+ self.conn = self.connect(db_name, username, pwd, host, port)
40
+ # self.cur = self.conn.cursor() if self.conn else None
41
+
42
+ def connect(
43
+ self,
44
+ database: str = None,
45
+ user: str = None,
46
+ password: str = None,
47
+ host: str = None,
48
+ port: str = None,
49
+ ) -> psycopg2.extensions.connection:
50
+
51
+ connection = psycopg2.connect(
52
+ database=database if database else os.environ.get("LOCAL_DATABASE"),
53
+ user=user if user else os.environ.get("LOCAL_USER"),
54
+ password=password if password else os.environ.get("LOCAL_PASS"),
55
+ host=host if host else os.environ.get("LOCAL_HOST"),
56
+ port=port if port else os.environ.get("LOCAL_PORT"),
57
+ )
58
+
59
+ if connection:
60
+ self.conn = connection
61
+ print("connected")
62
+ else:
63
+ raise ConnectionError("No database connection was established. Please check your credentials.")
64
+ return connection
65
+
66
+ def _create_cursor(self) -> psycopg2.extensions.cursor:
67
+ """
68
+ If the user hasn't connected manually, no self.conn exists, so we must call it and create the cursor from default values.
69
+
70
+ This function is here because its good practice to close the cursor after executing a command. So it is called at the top of a
71
+ SQL-performing function and closed after it.
72
+ """
73
+ if not self.conn:
74
+ self.conn = self.connect()
75
+ curr = self.conn.cursor()
76
+ return curr
77
+
78
+ def update_tables(self) -> None:
79
+ db_names = ("current", "previous", "tma")
80
+ columns = (
81
+ "id_ SERIAL PRIMARY KEY,\n"
82
+ + _COLUMNS[0]
83
+ + " INTEGER,\n"
84
+ + _COLUMNS[1]
85
+ + " VARCHAR(50),\n"
86
+ + _COLUMNS[2]
87
+ + " FLOAT,\n"
88
+ + _COLUMNS[3]
89
+ + " INTEGER,\n"
90
+ + _COLUMNS[4]
91
+ + " FLOAT,\n"
92
+ + _COLUMNS[5]
93
+ + " INTEGER,\n"
94
+ + _COLUMNS[6]
95
+ + " FLOAT, \n"
96
+ + _COLUMNS[7]
97
+ + " VARCHAR(50), \n"
98
+ + _COLUMNS[8]
99
+ + " VARCHAR(50)"
100
+ )
101
+
102
+ cursor = self._create_cursor()
103
+
104
+ sql_cmd = f"DROP TABLE IF EXISTS {db_names[-1]}; \n"
105
+ sql_cmd += f"DROP TABLE IF EXISTS {db_names[1]};\n"
106
+ sql_cmd += f"DROP TABLE IF EXISTS {db_names[0]};\n"
107
+ sql_cmd += f"CREATE TABLE {db_names[0]} ({columns});\n"
108
+ sql_cmd += f"CREATE TABLE {db_names[1]} ({columns});\n"
109
+ sql_cmd += f"CREATE TABLE {db_names[-1]} ({columns});\n"
110
+
111
+ cursor.execute(sql_cmd)
112
+ self.conn.commit()
113
+ self._close_cursor(cursor)
114
+ return
115
+
116
+ def load_data_to_table(self, target_table: str) -> None:
117
+ cursor = self._create_cursor()
118
+
119
+ target_dir = os.path.abspath(
120
+ os.path.join(
121
+ f"{up(up(__file__))}\\data\\", target_table
122
+ )
123
+ )
124
+
125
+ print(target_dir)
126
+ for source in os.listdir(target_dir):
127
+ with open(os.path.join(target_dir, source), "r") as truth:
128
+ next(truth)
129
+ cursor.copy_from(truth, target_table, columns=_COLUMNS, sep=",")
130
+ self.conn.commit()
131
+ self._close_cursor(cursor)
132
+
133
+ def close_connection(self) -> None:
134
+ """Closes database connection."""
135
+ self.conn.close()
136
+
137
+ def _close_cursor(self, cursor: psycopg2.extensions.cursor) -> None:
138
+ """Closes cursor."""
139
+ cursor.close()
@@ -0,0 +1,115 @@
1
+ from typing import Literal
2
+ from os.path import dirname as up
3
+ import os
4
+ import shutil
5
+ from datetime import datetime
6
+ import csv
7
+
8
+
9
+ # "Mega"-parent searching class. Everything contained within this class will be available,
10
+ # but not necessarily used by child classes.
11
+ class Search:
12
+ """Parent searcher class.
13
+
14
+ ::params:
15
+ >year (str or int): a string or integer year
16
+ >month (str): two-digit string month
17
+ >gen: (str or int): an integer for whichever Pokemon generation is being queried."""
18
+
19
+ def __init__(
20
+ self,
21
+ year: str | int,
22
+ month: Literal["Must be a two digit month string eg: '01' for January"],
23
+ gen: str | int,
24
+ ) -> None:
25
+ self.year = year
26
+ self.month = month
27
+ self.date = datetime.strptime(("-".join([str(year), month])), "%Y-%m")
28
+ self.gen = gen
29
+ self.base = "https://www.smogon.com/stats/"
30
+
31
+ @property
32
+ def year(self) -> str | int:
33
+ return self._year
34
+
35
+ @property
36
+ def month(self) -> str:
37
+ return self._month
38
+
39
+ @property
40
+ def gen(self) -> str:
41
+ return self._gen
42
+
43
+ @gen.setter
44
+ def gen(self, value):
45
+ self._gen = value
46
+
47
+ @year.setter
48
+ def year(self, value):
49
+ self._year = value
50
+
51
+ @month.setter
52
+ def month(self, value):
53
+ self._month = value
54
+
55
+ def _save_output(
56
+ self,
57
+ data: list[list],
58
+ ending: str,
59
+ pathname: str | os.PathLike = None,
60
+ ) -> None:
61
+ if pathname:
62
+ storage_dir = os.path.join(
63
+ f"{self.locate_base_data_directory()}/", pathname
64
+ )
65
+ else:
66
+ storage_dir = os.path.join(f"{self.locate_base_data_directory()}/cache")
67
+
68
+ if not os.path.exists((storage_dir)):
69
+ os.mkdir(storage_dir)
70
+ # make the document
71
+ filepath = os.path.join(storage_dir, f"{self.year}-{self.month}_{ending}.csv")
72
+ with open(
73
+ filepath,
74
+ "w",
75
+ newline="",
76
+ ) as file:
77
+ csvWriter = csv.writer(file, delimiter=",")
78
+ csvWriter.writerows(data)
79
+
80
+ if os.stat(filepath).st_size < 100:
81
+ os.remove(filepath)
82
+ print(f"File {filepath} deleted as it contained no data.")
83
+
84
+ def search_and_save(self, pathname: str | os.PathLike = None) -> None:
85
+ """
86
+ Searches the smogon stats repo and saves files to system.
87
+ """
88
+ data = self.search()
89
+ if self.ending:
90
+ ending = self.ending.split("-")[0]
91
+ self._save_output(data, ending, pathname=pathname)
92
+ else:
93
+ print("No data was saved.")
94
+ return None
95
+
96
+ def create_validation_object(self):
97
+ this = vars(self)
98
+ validation_object = {k.replace("_", ""): v for k, v in this.items()}
99
+ return validation_object
100
+
101
+ def locate_base_data_directory(self, reference: str | os.PathLike) -> os.PathLike:
102
+ """"""
103
+ base_dir = up(up(reference))
104
+ # set up the cached dir, theres probably a better way to do this but for now it will suffice
105
+ cache_dir = os.path.join(base_dir, "data")
106
+ return cache_dir
107
+
108
+ @staticmethod
109
+ def clear_cache() -> None:
110
+ """Clears cache files if there are any."""
111
+ base_dir = up(up(__file__))
112
+ # set up the cached dir, theres probably a better way to do this but for now it will suffice
113
+ cache_dir = os.path.join(base_dir, "data\\cache")
114
+ if os.path.exists(cache_dir):
115
+ shutil.rmtree(cache_dir)
@@ -0,0 +1,145 @@
1
+ import UsageStatsLookup as StatsSearch
2
+ import IndividualLookup as IndividualSearch
3
+ from SQLInterface import SQLInterface
4
+ from Search import Search
5
+ from datetime import datetime
6
+ from dateutil.relativedelta import relativedelta
7
+ import requests
8
+ from bs4 import BeautifulSoup
9
+
10
+ # param_dict something like
11
+
12
+
13
+ # {
14
+ # branch: 'BaseStats', 'MonoStats', 'BaseChaos', 'MonoChaos"
15
+ # year, gen, month, tier or typing all as their own fields
16
+ # isMonotype: True or False
17
+ # }
18
+ class Updater:
19
+ @staticmethod
20
+ def _set_query_object(param_dict: dict) -> Search:
21
+ """
22
+ Basically a backend router that returns a Search.<subclass> object
23
+ """
24
+ match param_dict["branch"]:
25
+ case "BaseStats":
26
+ new_query = StatsSearch.BaseStatsSearch(
27
+ year=param_dict["year"],
28
+ month=param_dict["month"],
29
+ gen=param_dict["gen"],
30
+ tier=param_dict["branch_param"],
31
+ )
32
+ return new_query
33
+ case "MonoStats":
34
+ new_query = StatsSearch.MonotypeStatsSearch(
35
+ year=param_dict["year"],
36
+ month=param_dict["month"],
37
+ gen=param_dict["gen"],
38
+ typing=param_dict["branch_param"],
39
+ )
40
+ case "BaseChaos":
41
+ new_query = IndividualSearch.BaseChaosSearch(
42
+ year=param_dict["year"],
43
+ month=param_dict["month"],
44
+ gen=param_dict["gen"],
45
+ tier=param_dict["branch_param"],
46
+ )
47
+ case "MonoChaos":
48
+ new_query = IndividualSearch.MonotypeChaosSearch(
49
+ year=param_dict["year"],
50
+ month=param_dict["month"],
51
+ gen=param_dict["gen"],
52
+ typing=param_dict["branch_param"],
53
+ )
54
+ return new_query
55
+
56
+ @staticmethod
57
+ def _update_database() -> None:
58
+ """Updates the database with new data."""
59
+ sqli = SQLInterface()
60
+ sqli.connect()
61
+ sqli.update_tables()
62
+ for i in ("current", "previous", "tma"):
63
+ sqli.load_data_to_table(i)
64
+
65
+ sqli.close_connection()
66
+ print("Database successfully updated.")
67
+
68
+ def update_monthly(self) -> None:
69
+ """Called on a monthly basis to update the database as new stats are released."""
70
+ today = datetime.now()
71
+
72
+ # realistically, I should only have to need the current data, and then I can
73
+ # just reshuffle the folders with shutil functions, which I will probably
74
+ # change to, but I need to get a working amount of data to work with and have
75
+ # something to fall back on if I somehow delete a bunch of data I cannot get
76
+ # back.
77
+ # I kind of already did this in the old version, so I'll just look there and
78
+ # see how to handle it.
79
+ date_dict = {
80
+ "current": today - relativedelta(months=1),
81
+ "previous": today - relativedelta(months=2),
82
+ "tma": today - relativedelta(months=3),
83
+ }
84
+
85
+ gens = ["1", "2", "3", "4", "5", "6", "7", "8", "9"]
86
+
87
+ def get_tiers(date_obj: datetime) -> tuple[str]:
88
+ """Internal function to get the available tiers to query. Faster than
89
+ iterating as it reduces the number of tiers by only picking from the
90
+ available ones. There's definitely a better way to do this which I am
91
+ going to change to that just uses the anchor tags ending in -1500.txt,
92
+ but at the moment this is good enough for a solution to work on perfecting.
93
+
94
+ Params:
95
+ date_object (datetime): a datetime object of at least YYYY-MM format
96
+
97
+ Returns:
98
+ tuple[str] -> a tuple of tiers, as string values
99
+ """
100
+ year = date_obj.strftime("%Y")
101
+ month = date_obj.strftime("%m")
102
+ url = f"https://www.smogon.com/stats/{year}-{month}/"
103
+
104
+ r = requests.get(url)
105
+ soup = BeautifulSoup(r.text, "html.parser")
106
+ anchors = soup.find_all("a")
107
+ available_tiers = []
108
+ for a in anchors[6:]:
109
+ a = a.text.strip("gen")
110
+ a = a.split("-")[0]
111
+ for g in gens:
112
+ if a.startswith(g):
113
+ a = a[1:]
114
+ print(a)
115
+ available_tiers.append(a)
116
+ return tuple(set(available_tiers))
117
+
118
+ def get_data(date_obj: datetime, table: str, isMonotype: bool = False) -> None:
119
+ tiers = get_tiers(date_obj)
120
+ for g in gens:
121
+ for t in tiers:
122
+ print(
123
+ date_dict[table].strftime("%Y"),
124
+ )
125
+ q = self._set_query_object(
126
+ {
127
+ "year": date_dict[table].strftime("%Y"),
128
+ "month": date_dict[table].strftime("%m"),
129
+ "gen": g,
130
+ "branch_param": t,
131
+ "branch": "BaseStats",
132
+ "isMonotype": isMonotype,
133
+ }
134
+ )
135
+ print(q.base)
136
+ q.search_and_save(pathname=table)
137
+
138
+ for k in date_dict.keys():
139
+ get_data(date_dict[k], k)
140
+
141
+ self._update_database()
142
+
143
+
144
+ # Updater().update_monthly()
145
+ # Updater._update_database()
@@ -0,0 +1,192 @@
1
+ from typing import Literal
2
+ from Search import Search
3
+ import requests
4
+ from bs4 import BeautifulSoup
5
+ from Validation import Validations
6
+
7
+
8
+ class StatsSearch(Search):
9
+ def __init__(
10
+ self,
11
+ year: str | int,
12
+ month: Literal["Must be a two digit month string eg: '01' for January"],
13
+ gen: str | int
14
+ ) -> None:
15
+ super().__init__(year, month, gen)
16
+ self.ending = None
17
+
18
+ ### NOT USED IN CHAOS BRANCH SEARCHES ###
19
+ def _remove_formatting(self, data: list) -> list[list]:
20
+ # Set the column names first
21
+ outlist = [
22
+ [
23
+ "rank",
24
+ "pokemon",
25
+ "usage_pct",
26
+ "raw_usage",
27
+ "raw_pct",
28
+ "real",
29
+ "real_pct",
30
+ "date",
31
+ "tier"
32
+ ]
33
+ ]
34
+ # Remove the formatting from the webpage
35
+ for line in data:
36
+ # remove all linebreaks,symbols and make lowercase
37
+ line = line.strip("\n")
38
+ line = line.replace("|", ",").replace(" ", "").replace("%", "").lower()
39
+ if line.startswith(","):
40
+ # remove leading and trailing commas
41
+ line = line[1:-1]
42
+ # turns it from a list of strings toa 2-d array
43
+ line = line.split(",")
44
+ line.append(f'{self.year}-{self.month}')
45
+ if self.tier:
46
+ line.append(f'gen{self.gen}{self.tier}')
47
+ elif self.typing:
48
+ line.append(f'mono{self.typing}')
49
+
50
+ # add it to the list to be returned
51
+ outlist.append(line)
52
+ return outlist
53
+
54
+ @staticmethod
55
+ def _vintage_search(year, month, gen, tier) -> str:
56
+ r = requests.get(f"https://www.smogon.com/stats/{year}-{month}/")
57
+ soup = BeautifulSoup(r.text, "html.parser")
58
+ anchors = soup.find_all("a")
59
+
60
+ tiers = tuple(set(a.text.strip(".txt").split("-")[0] for a in anchors[6:]))
61
+
62
+ try:
63
+ if tiers.index(f"gen{gen}{tier}"):
64
+ return f"gen{gen}{tier}-1500.txt"
65
+ except ValueError:
66
+ try:
67
+ if tiers.index(tier) and tier == (6 | 7):
68
+ return f"{tier}-1500.txt"
69
+ except ValueError:
70
+ return "Nothing found."
71
+
72
+
73
+ def search(self) -> list[list]:
74
+ """
75
+ Searches the smogon stats repository and returns a list of lists.
76
+ """
77
+ # send the request to get the data
78
+ res = requests.get(self.base)
79
+ # get the text if there is any (should add a check in here)
80
+ data = res.text
81
+ # send each "unit" of information to a list by splitting on linebreaks
82
+ data = data.split("\n")
83
+ # truncate to get rid of the non-relevant prefixes and last two lines of styling
84
+ data = data[5:-2]
85
+ # remove the formatting
86
+ data = self._remove_formatting(data)
87
+
88
+ return data
89
+
90
+
91
+ class BaseStatsSearch(StatsSearch):
92
+ """>tier (str): string of tier to be queried."""
93
+
94
+ __doc__ = Search.__doc__ + __doc__
95
+
96
+ def __init__(
97
+ self,
98
+ year: str | int,
99
+ month: Literal["Must be a two digit month string eg: '01' for Janu…"],
100
+ gen: str | int,
101
+ tier: str,
102
+ ) -> None:
103
+ super().__init__(year, month, gen)
104
+ self.tier = tier.lower()
105
+ self.base += f"{self.year}-{self.month}/"
106
+ self._build_url()
107
+
108
+ @property
109
+ def tier(self) -> str:
110
+ return self._tier.lower()
111
+
112
+ @tier.setter
113
+ def tier(self, value):
114
+ self._tier = value
115
+
116
+
117
+ def _build_url(self):
118
+ # Rating of 1500 is defined by Smogon as their target of a normal player
119
+
120
+ validation_object = self.create_validation_object()
121
+ validator = Validations(validation_object)
122
+ # check dates for valid tiers
123
+ # since for now I am only looking up newer stats for database, it will be fine
124
+ if validator.validate():
125
+ if Validations.is_modern_format(validation_object):
126
+ ending = f"gen{self.gen}{self.tier}-1500.txt"
127
+ self.ending = ending
128
+ self.base += ending
129
+ else:
130
+ ending = self._vintage_search(
131
+ self.year, self.month, self.gen, self.tier
132
+ )
133
+ # below can be deleted after validations are done
134
+ if ending.endswith(".txt"):
135
+ self.ending = ending
136
+ self.base += ending
137
+ # else:
138
+ # raise Exception('Invalid information supplied, check your parameters.')
139
+ else:
140
+ print("Something isn't correct.")
141
+
142
+
143
+ class MonotypeStatsSearch(StatsSearch):
144
+ """>typing (str): string of type to be queried."""
145
+
146
+ __doc__ = Search.__doc__ + __doc__
147
+
148
+ def __init__(
149
+ self,
150
+ year: str | int,
151
+ month: Literal["Must be a two digit month string eg: '01' for Janu…"],
152
+ gen: str | int,
153
+ typing: Literal["Must be string of type, such as 'fighting' or 'psychic'"],
154
+ ) -> None:
155
+ super().__init__(year, month, gen)
156
+ self.typing = typing.lower()
157
+ self.base += f"{year}-{month}/monotype/"
158
+ self._build_url
159
+
160
+ @property
161
+ def typing(self) -> str:
162
+ return self._typing.lower()
163
+
164
+ @typing.setter
165
+ def typing(self, value):
166
+ self._typing = value
167
+
168
+
169
+ # I could probably combine this and the other into a singular _build_url() that lives in the parent class, but I think this is simpler
170
+ def _build_url(self):
171
+ # Rating of 1500 is defined by Smogon as their target of a normal player
172
+
173
+ validation_object = self.create_validation_object()
174
+ validator = Validations(validation_object)
175
+ # check dates for valid tiers
176
+ # since for now I am only looking up newer stats for database, it will be fine
177
+ if validator.validate(isMonotype=True):
178
+ ending = f"/gen{self.gen}monotype-mono{self.typing}-1500.txt"
179
+ self.ending = ending
180
+ self.base += ending
181
+ else:
182
+ print("Something isn't correct.")
183
+
184
+
185
+ if __name__ == "__main__":
186
+ search = BaseStatsSearch(2015, "09", 4, "ou")
187
+ # query = search.search()
188
+ # search._save_output(query)
189
+ search.search_and_save(pathname="current")
190
+ # search.clear_cache()
191
+
192
+ pass
@@ -0,0 +1,91 @@
1
+ from datetime import datetime
2
+
3
+ types = [
4
+ "normal",
5
+ " fire",
6
+ " water",
7
+ " grass",
8
+ " flying",
9
+ " fighting",
10
+ " poison",
11
+ " electric",
12
+ " ground",
13
+ " rock",
14
+ " psychic",
15
+ " ice",
16
+ " bug",
17
+ " ghost",
18
+ " steel",
19
+ " dragon",
20
+ " dark",
21
+ " fairy",
22
+ ]
23
+
24
+
25
+ class Validations:
26
+ def __init__(self, validation_object: dict) -> None:
27
+ self.validation_object = validation_object
28
+
29
+ def validate(self, isMonotype: bool = False) -> bool:
30
+ """
31
+ Runs the validation checks, if any of them return False, this also returns False.
32
+ """
33
+ if isMonotype:
34
+ if self.typing not in types:
35
+ return False
36
+ if not (self._is_valid_date(self.validation_object)):
37
+ return False
38
+ else:
39
+ if not (self._is_correct_gen(self.validation_object)):
40
+ print("Generation choice is invalid")
41
+ return False
42
+ if not (self._is_valid_date(self.validation_object)):
43
+ print("Date and generation combination is invalid.")
44
+ return False
45
+ return True
46
+
47
+ @staticmethod
48
+ def is_modern_format(validation_object: dict) -> bool:
49
+ """Checks if the date submitted uses the old non-standardized labeling or not. Anything before Jul 2017 uses non-standard."""
50
+ if (int(validation_object["date"].year) == 2017) and (
51
+ int(validation_object["date"].month) <= 6
52
+ ):
53
+ return False
54
+ elif int(validation_object["date"].year) < 2017:
55
+ return False
56
+ return True
57
+
58
+ @staticmethod
59
+ def _is_correct_gen(validation_object: dict) -> bool:
60
+ """Checks if the generation is valid for the date submitted."""
61
+ cutoff_dates = {
62
+ "7": datetime.strptime("2016-11", "%Y-%m"),
63
+ "8": datetime.strptime("2020-11", "%Y-%m"),
64
+ "9": datetime.strptime("2022-11", "%Y-%m"),
65
+ }
66
+ # something like having an object of dates this is where it makes sense to
67
+ # have dt objects rather than strings
68
+ # if they fail, return false.
69
+
70
+ if int(validation_object["gen"]) <= 6:
71
+ return True
72
+ elif (int(validation_object["gen"]) > 6) and (
73
+ validation_object["date"] < cutoff_dates[str(validation_object["gen"])]
74
+ ):
75
+ return False
76
+ else:
77
+ return True
78
+
79
+ @staticmethod
80
+ def _is_valid_date(validation_object: dict) -> bool:
81
+ """Makes sure theres no date submitted before Nov 2014, as there is no data available before then."""
82
+ year = int(validation_object["date"].year)
83
+ month = int(validation_object["date"].month)
84
+
85
+ if year < 2014:
86
+ return False
87
+ elif year == 2014:
88
+ if month != (11 | 12):
89
+ return False
90
+ else:
91
+ return True
@@ -0,0 +1,11 @@
1
+ import sys
2
+ import os
3
+
4
+ __version__ = "1.0.0"
5
+ __author__ = "Alan Nardo"
6
+
7
+ # Get the parent directory
8
+ parent_dir = os.path.dirname(os.path.realpath(__file__))
9
+
10
+ # Add the parent directory to sys.path
11
+ sys.path.append(parent_dir)
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Alan Nardo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,120 @@
1
+ Metadata-Version: 2.1
2
+ Name: smog-usage-stats
3
+ Version: 1.0.0
4
+ Author-email: "stu.gotz.dev" <gotz.stu.dev@gmail.com>
5
+ License: MIT License
6
+
7
+ Copyright (c) 2023 Alan Nardo
8
+
9
+ Permission is hereby granted, free of charge, to any person obtaining a copy
10
+ of this software and associated documentation files (the "Software"), to deal
11
+ in the Software without restriction, including without limitation the rights
12
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ copies of the Software, and to permit persons to whom the Software is
14
+ furnished to do so, subject to the following conditions:
15
+
16
+ The above copyright notice and this permission notice shall be included in all
17
+ copies or substantial portions of the Software.
18
+
19
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25
+ SOFTWARE.
26
+ Keywords: pokemon,usage,pokemon showdown,smogon
27
+ Classifier: License :: OSI Approved :: MIT License
28
+ Classifier: Intended Audience :: Developers
29
+ Classifier: Programming Language :: Python :: 3
30
+ Classifier: Operating System :: OS Independent
31
+ Requires-Python: >=3.7
32
+ Description-Content-Type: text/markdown
33
+ License-File: LICENSE
34
+ Requires-Dist: beautifulsoup4
35
+ Requires-Dist: pathlib
36
+ Requires-Dist: psycopg ==3.1.12
37
+ Requires-Dist: psycopg-binary ==3.1.12
38
+ Requires-Dist: psycopg2 ==2.9.5
39
+ Requires-Dist: python-dateutil
40
+ Requires-Dist: python-dotenv
41
+ Requires-Dist: requests
42
+ Requires-Dist: soupsieve
43
+ Requires-Dist: typing-extensions
44
+ Provides-Extra: dev
45
+ Requires-Dist: black ; extra == 'dev'
46
+ Requires-Dist: bumpver ; extra == 'dev'
47
+ Requires-Dist: isort ; extra == 'dev'
48
+ Requires-Dist: pip-tools ; extra == 'dev'
49
+ Requires-Dist: pytest ; extra == 'dev'
50
+
51
+ # smog_usage_stats
52
+
53
+ This is a bit of a pet project of mine that has been in the works for some while and
54
+ seen several iterations.
55
+
56
+ In short, it pulls usage stats from [smogon.com's stats
57
+ pages](https://smogon.com/stats) and turns them into a usable data. Originally, it
58
+ was a very functional solution, currently I am working to shift it to OOP and once
59
+ that's sorted, ship it as a package so other people can use it.
60
+
61
+ ## Quick Rundown
62
+
63
+ ### Installing:
64
+
65
+ Because its not a package yet, you will have to download this to somewhere on your
66
+ PC. Just download it the same way you normally copy code repositories from Git. Open
67
+ it in your favourite IDE or text editor and have fun.
68
+
69
+ ### How it all works
70
+
71
+ `Search.py` is the uber parent searcher class, that all other searcher classes
72
+ inherit from. You shouldn't need to ever call this directly.
73
+
74
+ The four primary search classes are:
75
+
76
+ `BaseStatsSearch` -> searches the normal stats directory
77
+
78
+ `MonotypeStatsSearch` -> searches the monotype stats directory
79
+
80
+ `BaseChaosSearch` -> searches the chaos data contained within a certain months stats
81
+ data
82
+
83
+ `MonotypeChaosSearch` -> searches the monotype chaos data contained within a certain
84
+ months stats data
85
+
86
+ The latter two are more individual searches and there is also a (WIP)
87
+ `IndividualStatsSearch` class which will be used to search for individual usage stats
88
+ data from the Base or Monotype stats search classes.
89
+
90
+ `Validation.py` handles the validation of query parameters for the queries, making
91
+ sure everything is lined up to actually return data. The first instance of this
92
+ project had a very shotgun-blast approach which was massively ineffective and did not
93
+ account for discrepencies in labeling conventions used by Smogon in their earlier
94
+ days of stats collection. You also should never need to call this directly, it is
95
+ effectively a helper class.
96
+
97
+ `SQLInterface.py` does what it says on the tin, it allows interacting with a (in
98
+ this case) PostgreSQL database instance. I HIGHLY recomend using a .env file if you
99
+ use this in any way, and add it to your `.gitignore`.
100
+
101
+ `Update.py` is a class to push new data to the database. The SQL data serves as a
102
+ backend for a full-stack application I am working on as well, so it's probably not
103
+ necessary for this nor `SQLInterface.py` to be used for most cases, but it is there
104
+ if someone maybe wants to make a Mongo interface or anything else.
105
+
106
+ ## Future plans:
107
+
108
+ - Finish up individual stats search
109
+ - Clean up directory to get rid of old data
110
+ - Test test test
111
+ - Write update function to work as a scheduled task
112
+
113
+ ## Contributing:
114
+
115
+ If you find a bug, please make
116
+ an issue and then submit a pull request when it's sorted.
117
+
118
+ If you are interested in just using this on your own, feel free to copy and make
119
+ changes as you like, it's not owned by me, just a fun project. I'll add in licensing
120
+ stuff once I package it up, but it will always be open-source.
@@ -0,0 +1,12 @@
1
+ smog_usage_stats/IndividualLookup.py,sha256=ootatwj8uqOvZKURAxOEpif7UfJNTsAJBYgY911aPQw,4053
2
+ smog_usage_stats/SQLInterface.py,sha256=waRapB6a_kITDsbESjHkblMyNdpM9cEBSfLYASpKgNI,4296
3
+ smog_usage_stats/Search.py,sha256=2rB27QEo3J13IJmRM2hYs3x09aB0eVy3Vvm08pi7KTQ,3621
4
+ smog_usage_stats/Update.py,sha256=oNn8ssKgGyJp3071uGq_9rH1ZltcMXBq-E77EeFAK58,5680
5
+ smog_usage_stats/UsageStatsLookup.py,sha256=wsLvV8cLcZoYrjeDHazZHjHBOINB-v3uTwiAJlnB7ZE,6550
6
+ smog_usage_stats/Validation.py,sha256=-xU0k1jdDqkNlC8RHBp3jw3Pupos_R9GpuNqJJKiIr0,2969
7
+ smog_usage_stats/__init__.py,sha256=ilx4c230MJ9n-Ea7QBBRuYR0oHwZ4AmC4BoJROt1G5M,234
8
+ smog_usage_stats-1.0.0.dist-info/LICENSE,sha256=QeNT41FhSfJaY6-r7ImGGyG2bmNSZGKUCPfRnqqZd4Q,1086
9
+ smog_usage_stats-1.0.0.dist-info/METADATA,sha256=b27vAp3ZV9JI-q1FGvnFQf61gPLvHKu2yfuVsh0pABo,5197
10
+ smog_usage_stats-1.0.0.dist-info/WHEEL,sha256=Xo9-1PvkuimrydujYJAjF7pCkriuXBpUPEjma1nZyJ0,92
11
+ smog_usage_stats-1.0.0.dist-info/top_level.txt,sha256=7X2lZfrImNekiwdq0QFhDbXKglahGMfN3B0sK7twOCo,17
12
+ smog_usage_stats-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: bdist_wheel (0.41.3)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ smog_usage_stats