Repository navigation
Expand file tree
/
Copy pathDataManager.py
More file actions
258 lines (194 loc) · 12.2 KB
/
Copy pathDataManager.py
File metadata and controls
258 lines (194 loc) · 12.2 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
import os
import json
from abc import ABC, abstractmethod
from enum import Enum
class DataFields (ABC):
"""
This class stores the constants for the specific fields of the data you are saving (e.g. user_id, item_id).
Extend this class to define the data fields.
Note: Specific to dict-type databases
"""
IDENTIFIER = "identifier" # Identifier must exist as a data field.
pass
class DataManagerErrors:
"""
This class stores the error codes for the DataManager class.
"""
class PathExists(Exception):
"""
This exception is raised when the path already exists.
"""
def __init__(self, msg: str = None):
super().__init__("Path already exists" + (f": {msg}" if msg else ""))
class PathNotExists(Exception):
"""
This exception is raised when the path does not exist.
"""
def __init__(self, msg: str = None):
super().__init__("Path does not exist" + (f": {msg}" if msg else ""))
class DataKeyNotExists(Exception):
"""
This exception is raised when the data key does not exist.
"""
def __init__(self, key: str, msg: str = None):
super().__init__(f"Data key {key} does not exist" + (f": {msg}" if msg else ""))
class ActionNotAllowed(Exception):
"""
This exception is raised when the program tries to use a function not of a certain database type.
"""
def _init_(self, msg: str = None):
super().__init__("Action not allowed" + (f": {msg}" if msg else ""))
class DataManager (ABC):
"""
This class is the abstract class for the DataManager. It is responsible for handling the data storage and retrieval.
You will not be required to extend this class if you do not want to modify the data storage and retrieval. Simply pass the datapath through while instantiating this class and it should work.
Extend this class if you want to make any edits to how data is saved (for example for `getFormattedFileType()`). Otherwise, simply instantiating this object should allow you to use the manager.
e.g. UserDataManager (DataManager), GameDataManager (DataManager),
You need one data manager for each data type you are saving. This is because the data manager is responsible for the data storage and retrieval.
If you want any quick functions to get or set data, extend this class and add the functions.
Please note all functions must use the `identifier` to identify the data type, as this class does not house that infomation and is only responsible for the data storage and retrieval.
Example:
```python
def getPassword(self, identifier: str) -> str:
return self.getData(identifier, DataFields.PASSWORD)
```
(Note: Please remember to add all constants to an extension of the DataFields class)
Definitions:
Please take a look at the following definitions to adhere to existing naming conventions.
- Datapath (DP): The folder in which a database is running (for example, `...\\root\\data\\`)
- Database (DB): Databases are based on their assigned DP. They are responsible for the data manipulation within this path.
- Datafile/document (DD): Databases store datafiles. The datafiles are (JSON) documents that store all of the data attached to an identifier.
- DataField (DF): An enum class storing all of the data a Data Document will store through consts. Data can only be accessed through there consts.
"""
_DATAPATH = None
dbtype = None
class DatabaseType (Enum):
""" Defines how you want data to be stored. """
DICT = 0
LIST = 1
pass
def __init__(self, datapath: str, type: DatabaseType):
"""
Datapath should be a string that is a valid path to a directory. Use os.path.join to make it platform independent.
"""
if not isinstance(datapath, str): # Throw error if datapath type is incorrect
raise ValueError("datapath needs to be a string")
if not os.path.exists(datapath):
os.makedirs(datapath) # Make the directory if it does not exist. This will also throw an error if a datapath style is incorrect.
self._DATAPATH = datapath
self.dbtype = type
@property
def datapath(self):
if isinstance(self._DATAPATH, str):
return self._DATAPATH
else:
raise ValueError(f"Tried to retrieve {type(self._DATAPATH)} datapath")
def getDefaultValues(self) -> dict:
""" Override this function to define the default (required) values of every datafile in a database.
Stored as `key: value`. These values correspond to the constants in DataFields. All DataFiles MUST have these values. DDs lacking any of the set keys will be repaired and set to the default state set here.
Note that `DataFields.Identifier` already exists as a defalt value.
Only works with database type dict.
"""
return {} # Default an empty dictionary here in the base class. This function is intended to be overwritten.
def getFormattedFilename(self, identifier: str) -> str:
""" Override this function to change the format of file creation.
This MUST return a string that is a valid file path (with the .json extension)
WARNING: Overriding how files look may require you to override several other functions (depending on how your are formatting your names. Take a look at Example.py.). You are much better of using databases within databases to sort data rather than formatting.
"""
return os.path.join(self.datapath, f"{identifier}.json")
def datafileExists(self, identifier: str) -> bool:
""" This function returns if a datafile to an identifier exists. """
path = self.getFormattedFilename(identifier) # Format filename for uniformity.
return os.path.exists(path)
def getData(self, identifier: str, key: DataFields = None):
"""
This function should fetch the data from a datafile and return it.
Returns `dict` unless a key is specified, in which case it returns the value of the key.
"""
path = self.getFormattedFilename(identifier) # Get formatted filepath
if not self.datafileExists(identifier):
raise DataManagerErrors.PathNotExists() # Raise targetted error
with open(path, 'r') as file: # Read contents of a file and save it to the data variable
data = json.load(file)
if self.dbtype == DataManager.DatabaseType.DICT: # Only overrides default values and uses keys for databases of type DICT
defaults = self.getDefaultValues() or {} # Ensure getDefaultValues() returns a dictionary (use empty dictionary if None)
data.update({key: value for key, value in defaults.items() if key not in data}) # Merge missing default values from getDefaultValues into the data
data.setdefault(DataFields.IDENTIFIER, identifier) # Ensure `DataFields.IDENTIFIER` exists in `data`, setting it to `identifier` if missing
with open(path, 'w') as file: # Write the default values for missing keys
json.dump(data, file, indent=4)
if key is not None:
if key not in data: # If the key does not exist return a targetted error
raise DataManagerErrors.DataKeyNotExists(str(key))
return data[key] # If a key was specified, return the value.
return data # If no key was specified (or datatype is a list), return the complete data.
def setData(self, identifier: str, field: DataFields, newVal) -> None:
""" Sets a value to a field in a datafile. Assumes the newVal is a valid value for the field.
SPECIFIC TO `DatabaseType.DICT`
"""
if not self.dbtype == self.DatabaseType.DICT: # throw error if the database type is not a dictionary.
raise DataManagerErrors.ActionNotAllowed(f"IncorrectDatabaseType: {self.dbtype}")
data = self.getData(identifier) # Get a dictionary of the data
data[str(field)] = newVal # Set (or create) a key to the new value specified
path = self.getFormattedFilename(identifier) # Get the path
try:
with open(path, 'w') as file: # Override the new copy of the data with the new information on the old one.
json.dump(data, file, indent=4)
except Exception as e: # Raise a targetted PathNotExists error if anything goes wrong.
raise DataManagerErrors.PathNotExists(str(e))
def addItem(self, identifier: str, value) -> None:
""" Adds an item to the list. Does not factor if item already exists.
SPECIFIC TO `DatabaseType.LIST`
"""
if not self.dbtype == self.DatabaseType.LIST: # throw error if the database type is not a dictionary.
raise DataManagerErrors.ActionNotAllowed(f"IncorrectDatabaseType: {self.dbtype}")
data = self.getData(identifier) # Get a copy of the data
data.extend([value]) # Add value to the list.
path = self.getFormattedFilename(identifier) # Get the path
try:
with open(path, 'w') as file: # Override the new copy of the data with the new information on the old one.
json.dump(data, file, indent=4)
except Exception as e: # Raise a targetted PathNotExists error if anything goes wrong.
raise DataManagerErrors.PathNotExists(str(e))
def createDatafile(self, identifier: str):
if self.datafileExists(identifier): # Check if the datafile already exists
raise DataManagerErrors.PathExists()
else:
path = self.getFormattedFilename(identifier) # Get the formatted filename
if self.dbtype == self.DatabaseType.DICT:
data = {str(DataFields.IDENTIFIER): identifier} # Add identifier to the data
data.update(self.getDefaultValues()) # Add the default values to the data when creating the datafile.
elif self.dbtype == self.DatabaseType.LIST:
data = [] # Initialise empty list.
else:
raise ValueError(f"Database has an incorrect or invalid type: {str(self.dbtype)}")
with open(path, 'w') as file:
json.dump(data, file, indent=4) # Write the combined data into the file.
def deleteDatafile(self, identifier: str):
"""
Delete a datafile from the database.
"""
path = self.getFormattedFilename(identifier) # Get path
if not self.datafileExists(identifier): # Ensure the path exists before deleting it
raise DataManagerErrors.PathNotExists()
try:
os.remove(path) # Delete the path
except Exception as e: # Raise any errors that may arrise.
raise Exception(f"An error occurred while deleting the database: {str(e)}")
def DANGER_DELETEALL(self):
"""
Deletes the entire database.
WARNING: THIS DELETES THE ENTIRE DATAPATH FOLDER! Please ensure only data is stored here!
"""
if not os.path.exists(self.datapath): # Ensures the datapath exists before deleting it.
raise DataManagerErrors.PathNotExists()
try:
# Remove all files in the datapath directory
for filename in os.listdir(self.datapath): # Cycle through the contents of a directory
file_path = os.path.join(self.datapath, filename) # Get the filepath of an item of the contents.
if os.path.isfile(file_path): # Check if item is a file
os.remove(file_path) # If it is a file, remove the file.
elif os.path.isdir(file_path): # Check if item is a directory
os.rmdir(file_path) # If it's a directory, remove it recursively
os.rmdir(self.datapath) # Delete the datapath itself after it's all over.
except Exception as e: # Raise any errors that my arrise.
raise Exception(f"An error occurred while deleting the entire database: {str(e)}")