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.
- smog_usage_stats/IndividualLookup.py +134 -0
- smog_usage_stats/SQLInterface.py +139 -0
- smog_usage_stats/Search.py +115 -0
- smog_usage_stats/Update.py +145 -0
- smog_usage_stats/UsageStatsLookup.py +192 -0
- smog_usage_stats/Validation.py +91 -0
- smog_usage_stats/__init__.py +11 -0
- smog_usage_stats-1.0.0.dist-info/LICENSE +21 -0
- smog_usage_stats-1.0.0.dist-info/METADATA +120 -0
- smog_usage_stats-1.0.0.dist-info/RECORD +12 -0
- smog_usage_stats-1.0.0.dist-info/WHEEL +5 -0
- smog_usage_stats-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -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,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 @@
|
|
|
1
|
+
smog_usage_stats
|