-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathclasses.py
More file actions
414 lines (312 loc) · 15.6 KB
/
classes.py
File metadata and controls
414 lines (312 loc) · 15.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
from os import mkdir
from os.path import exists, splitext
from typing import Any, Dict, Iterable, Optional, Union, List
import json
from discord import Guild
import copy
import glob
from .enums import DatabaseUpdateType, SortBy
import logging
class DatabaseTable(object):
def __init__(self, name : str, *, perGuild : bool = False) -> None:
"""
Our magic table stuff. This stuff will basically dictate the actual file, and not the data in it.
"""
self.name = name.lower()
self.per_guild = perGuild # Whether or not the database will be put it its own folder, and used per-guild.
self.columns = []
self._database = None
@property
def fileName(self) -> str:
if not self.per_guild: # If it's not supposed to be a guild-specific filesystem
return self.name.replace(' ', '_') + ".json"
else: # If it is supposed to be guild-specific
return self.name.replace(' ', '_')
@property
def fullFilePath(self) -> str:
"""
Returns the entire path of the file
"""
if self.per_guild:
return f"./{self._database.root_name}/{self.fileName}/%(guildid)s.json"
return f"./{self._database.root_name}/{self.fileName}"
@property
def _columnTypeReference(self) -> List[type]:
"""
A list which has all of the column types in it.
"""
return [col.type for col in self.columns]
@property
def _columnStrList(self) -> List[str]:
"""
Just returns a list of all of the columns' names.
"""
return [i.name for i in self.columns]
def _fetchAllParticipatingGuilds(self) -> List[int]:
"""
Returns a list of guild ids who are using this table if it is per-guild.
If the table is not per-guild this will return an empty list.
This is purely based on filenames.
"""
if not self.per_guild:
return []
g = glob.glob(f"./{self._database.root_name}/{self.fileName}/*.json")
return list(map(lambda i: int(splitext(i)[0][-18:]), g))
def addColumn(self, name, type, *, default_value = None):
"""
Creates a database column through a specific
"""
# Create an actual column object
col = DatabaseColumn(
table=self,
columnId=len(self.columns),
name=name,
type=type,
default_value=default_value
)
col.columnId = len(self.columns)
# Then append that to the list.
self.columns.append(col)
# idk then return the column or something?
return col
def getColumnByID(self, id : int):
"""
Returns the index of a specific column
"""
for column in self.columns:
if column.columnId == id:
return column
return None
def _init_post_database_assign(self) -> None:
"""
A function which is called post database assign.
This is where the program ensures the folders and files exist.
"""
self._database.logger.info("Ensuring directories exist for table `%s`" % self.name)
if self.per_guild:
if not exists(f"./{self._database.root_name}/{self.fileName}/"):
mkdir(f"./{self._database.root_name}/{self.fileName}/")
self._database.logger.info("Table `%s` is per-guild, created directory for per-guild data storage." % self.name)
else:
if not exists(self.fullFilePath):
with open(self.fullFilePath, 'w+') as j:
json.dump([], j)
j.close()
self._database.logger.info("Table `%s` had no json file, created a new empty one for you <3" % self.name)
self._database.logger.info("Assigned table `%s` with %s columns (per-guild: %s)" % (self.name, len(self.columns), self.per_guild))
def fetchAllData(self, guild : Guild = None, *, RAW_ONLY : bool = False):
if guild == None:
with open(self.fullFilePath, 'r') as js:
data = json.load(js)
else:
with open(self.fullFilePath % {"guildid" : guild.id}) as js:
data = json.load(js)
if RAW_ONLY:
return data
return QueryHandler(self, data, guild)
def updateTableData(self, new_data : List[dict], guild : Guild = None) -> None:
# Dump new data into file
if guild is None:
with open(self.fullFilePath, 'w+') as j:
json.dump(
new_data,
j,
indent = 4
)
return
with open(self.fullFilePath % {"guildid" : guild.id}, 'w+') as j:
json.dump(
new_data,
j,
indent=4
)
class DatabaseColumn(object):
def __init__(self, table : DatabaseTable, columnId : int, name : str, type : type, default_value : Any = None):
self.name = name.lower() # We aint about that gay ass uppercase name shit
self.type = type
self.columnId = 0
self.default_value = default_value
self._database = None
def _initialize_with_database(self, database) -> None:
"""
This function is ran immediately after the database acknowledges that it has this column.
This is basically reserved for any sorting issues I have to fix later on.
"""
self._database = database
return
def _check_value(self, value) -> bool:
"""
Checks to see if the value provided is one that abides by the type of this column.
"""
return isinstance(value, self.type) # Just a simple check
class QueryHandler(object):
"""
The class which allows users to handle data recieved from the handler.
This makes it easier to find certain things rather than having to sift through tons
and tons of queries.
"""
def __init__(self, table : DatabaseTable, results, guild : Guild) -> None:
self.table = table
self.columns = self.table.columns
self.guild = Guild
self.results = results
self.__index_all_results() # Basically allows us to tell apart all of the data
def __index_all_results(self) -> None:
if len(self.results) > 0:
if "__META_DB_INDEX" in self.results[0].keys():
print("already was indexed")
return
for index, item in enumerate(self.results):
# print(item, type(item), sep="\t") # Debug purposes
item["__META_DB_INDEX"] = index
def __get_unindexed_all_results(self) -> List[dict]:
x = copy.deepcopy(self.results)
for item in x:
del item['__META_DB_INDEX']
return x
def __unindex_this(self, d : List[Dict]) -> List[dict]:
d = copy.deepcopy(d)
for item in d:
del item['__META_DB_INDEX']
return d
def __fetch_all_item_indexes(self) -> Iterable:
return [k['__META_DB_INDEX'] for k in self.results]
def All(self) -> List[dict]:
"""
Just returns all of the results as a dict
"""
# Default return if nothing matches the query
if len(self.results) < 1:
return []
return self.__get_unindexed_all_results()
def First(self, n : int = 1) -> Union[dict, list]:
"""
Returns the first `n` items from the results.
Default value is 1, and when `n` is 1 the function will
return it as a dict, otherwise it will return it as a list.
If the size of the result set is smaller than `n`, this function
will return the equivalent of calling QueryHandler.All()
"""
# Default return if nothing matches the query
if len(self.results) < 1:
return None
if n > 1:
unind = self.__get_unindexed_all_results()[:n]
else:
unind = self.__get_unindexed_all_results()[0]
return unind
def Seek(self, column_name : str, value : Any):
"""
Searches for a specific value in all rows based on a singular column.
This returns another QueryHandler. So you'll be able to search again,
and use any of the other functionalities that come with this class.
"""
# Fetching the actual column object so we can do some wacky stuff with it
specific_column = None
for c in self.columns:
if c.name.lower() == column_name.lower():
specific_column = c
break
if specific_column is None:
raise ValueError("Column `%s` does not exist" % column_name.lower())
if not specific_column._check_value(value):
raise TypeError("Type provided for search query is not corresponding type to column. Column type is `%s`, you provided value with type: `%s`" % (specific_column.type, type(value)))
return QueryHandler(self.table, [res for res in self.results if res[specific_column.name.lower()] == value], guild=self.guild)
def AdvancedFilter(self, column_name : str, func : callable):
"""
This filter will basically return all the values that are true based on the `func` argument.
Only one argument will be provided to the function. Which will be the column value for the row.
This function WILL be ran for EVERY row you have in the database. So please dont make it too cpu-intensive.
This function returns a new QueryHandler instance with all of the results that are true based on the function.
"""
# This var will store the DatabaseColumn object
specific_column = None
# Searching for our column
for c in self.columns:
if c.name.lower() == column_name.lower():
specific_column = c
break
# Return an error if the column doesnt exist
if specific_column is None:
raise ValueError("Column `%s` does not exist" % column_name.lower())
# Return the query handler
return QueryHandler(
table = self.table,
results = [i for i in self.results if func(i[specific_column.name])],
guild=self.guild
)
def Update(self, column_name : str, value : Any, update_type : DatabaseUpdateType = DatabaseUpdateType.SET, overwrite_file : bool = False):
"""
Sets the value for all data in a specific column which also resides in `self.results`.
Meaning that anything that you have in this specific object, will have a column updated
when this function is used.
This function returns this same QueryHandler object, but the results are updated
to fit the new updates made to them.
"""
# This var will store the DatabaseColumn object
specific_column = None
# Searching for our column
for c in self.columns:
if c.name.lower() == column_name.lower():
specific_column = c
break
# Return an error if the column doesnt exist
if specific_column is None:
raise ValueError("Column `%s` does not exist" % column_name.lower())
# Then we check and see if the value that is to be updated is actually of the type that the column requests.
if not specific_column._check_value(value):
raise TypeError("Type provided for search query is not corresponding type to column. Column type is `%s`, you provided value with type: `%s`" % (specific_column.type, type(value)))
new_data = self.results
# Now we update each result
for result in new_data:
self.table._database.logger.info("Updating data in table %s. (Setting: %s) Value=%s" % (self.table.name, update_type.name, value))
# Setting the value has no requirements
if update_type == DatabaseUpdateType.SET:
result[specific_column.name.lower()] = value
# Incrementing the value of the column requires additional checks to ensure both the value provided, and the column's type, can be added to
elif update_type == DatabaseUpdateType.INCREMENT:
# Making sure you can actually add to the item
if not hasattr(result[specific_column.name.lower()], "__add__"):
raise TypeError(f"Error when updating value `{specific_column.name.lower()}`. Type `{type(result[specific_column.name.lower()])}` has no built-in adding feature.")
# Checking the value provided to make sure you can add with that too
if not hasattr(value, "__add__"):
raise TypeError(f"Error while updating value `{specific_column.name.lower()}`. The value you provided of type `{type(value)}` is not one that can be added to.")
# Actually adding to the value
result[specific_column.name.lower()] += value
# Decrementing the value of the column also requires additional checks.
elif update_type == DatabaseUpdateType.DECREMENT:
# Making sure you can actually subtract from the item
if not hasattr(result[specific_column.name.lower()], "__sub__"):
raise TypeError(f"Error when updating value `{specific_column.name.lower()}`. Type `{type(result[specific_column.name.lower()])}` has no built-in subtraction feature.")
# Checking the value provided to make sure you can actually subtract from it.
if not hasattr(value, "__sub__"):
raise TypeError(f"Error while updating value `{specific_column.name.lower()}`. The value you provided of type `{type(value)}` is not one that can be subtracted from.")
# Actually decrementing the value.
result[specific_column.name.lower()] -= value
# Now we fetch old data for comparison
old_data = self.table.fetchAllData(self.guild)
# Attempting to merge the data
merged_data = []
new_data_indexes = self.__fetch_all_item_indexes()
# Allocating spots for new data in old data's spaces
for row in old_data.results:
# Keep the old data if no new data is created
if row['__META_DB_INDEX'] not in new_data_indexes:
merged_data.append(row)
# Actually doing the merge
merged_data.extend(new_data)
# Sort the indexes so it stays in order
merged_data.sort(
key = lambda x: x['__META_DB_INDEX'],
reverse=False
)
unindexed_merged = self.__unindex_this(merged_data)
self.table.updateTableData(unindexed_merged)
return QueryHandler(self.table, unindexed_merged, self.guild)
def Sort(self, column_name : str, sort_type : SortBy = SortBy.ASCENDING):
"""
Sorts the data by a column, By default it sorts by ascending.
Setting `sort_type` to SortBy.DESCENDING will obviously set it to
descending
"""
return QueryHandler(self.table, sorted(self.results, reverse=bool(sort_type.value)), self.guild)