Update readme, remove redundant functions, add isfetchable
This commit is contained in:
49
README.md
49
README.md
@@ -25,17 +25,44 @@ table name `student` and rows `student_id`, `student_name` with datatypes
|
|||||||
integer and text, respectively. The default value for `student_name` is
|
integer and text, respectively. The default value for `student_name` is
|
||||||
`John Smith`.
|
`John Smith`.
|
||||||
|
|
||||||
## Creating a new object instance
|
## Entry manipulation
|
||||||
|
|
||||||
If you create a new object with default Python methods, the object will not
|
After creating an object traditionally, given that you used the `sqlify` decorator,
|
||||||
be inserted into the table by default. However, the classes that are created
|
the object has two new methods: `.create_entry()` and `.remove_entry()`, you
|
||||||
with `datalite` has a argument in their init method. So, if you write
|
can add the object to its associated table using the former, and remove it
|
||||||
`Student(1, create_entry=True)` rather than just saying `Student(1)`, the
|
using the latter.
|
||||||
entry equivalent of the newly created student will be inserted into
|
|
||||||
the table without any problems.
|
|
||||||
|
|
||||||
## Deleting an object instance
|
```python
|
||||||
|
student = Student(10, "Albert Einstein")
|
||||||
|
student.create_entry() # Adds the entry to the table associated in db.db
|
||||||
|
student.remove_entry() # Removes from the table.
|
||||||
|
```
|
||||||
|
|
||||||
Another method that is added to any dataclass created with `datalite` is the
|
But what if you have created your object in a previous session, or wish
|
||||||
`.remove()` method. By deleting a class with the `.remove()` you will also
|
to remove an object unreachable? ie: If the object is already garbage
|
||||||
delete its equivalent entry from the database.
|
collected by the Python interpreter? `remove_from(class_, obj_id)` is
|
||||||
|
a function that can be used for this express purpose, for instance:
|
||||||
|
|
||||||
|
```python
|
||||||
|
remove_from(Student, 2) # Removes the Student with obj_id 2.
|
||||||
|
```
|
||||||
|
|
||||||
|
Object IDs are auto-incremented, and correspond to the order the entry were
|
||||||
|
inserted onto the system.
|
||||||
|
|
||||||
|
## Fetching Records
|
||||||
|
Finally, you may wish to recreate objects from a table that already exist, for
|
||||||
|
this purpose we have the function `fetch_from(class_, object_id)` as well
|
||||||
|
as `is_fetchable(className, object_id)` former fetches a record from the
|
||||||
|
SQL database whereas the latter checks if it is fetchable (most likely
|
||||||
|
to check if it exists.)
|
||||||
|
|
||||||
|
```python
|
||||||
|
>>> fetch_from(Student, 2)
|
||||||
|
Student(student_id=10, student_name='Albert Einstein')
|
||||||
|
```
|
||||||
|
|
||||||
|
Finally, we have two helper methods, `fetch_range(class_, range_)` and
|
||||||
|
`fetch_all(class_)` the former fetches the records fetchable from the object
|
||||||
|
id range provided by the user, whereas the latter fetches all records. Both
|
||||||
|
return a tuple of `class_` objects.
|
||||||
102
src/datalite.py
102
src/datalite.py
@@ -1,28 +1,10 @@
|
|||||||
from os.path import exists
|
from os.path import exists
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import sqlite3 as sql
|
import sqlite3 as sql
|
||||||
from dataclasses import Field, asdict
|
from dataclasses import Field, asdict, dataclass
|
||||||
from typing import List, Dict, Optional, Callable, Any
|
from typing import List, Dict, Optional, Callable, Any
|
||||||
|
|
||||||
|
|
||||||
def _database_exists(db_path: str) -> bool:
|
|
||||||
"""
|
|
||||||
Check if a given database exists.
|
|
||||||
:param db_path: Relative path of the database, including the extension.
|
|
||||||
:return: True if database exists, False otherwise.
|
|
||||||
"""
|
|
||||||
return exists(db_path)
|
|
||||||
|
|
||||||
|
|
||||||
def _create_db(db_path: str) -> None:
|
|
||||||
"""
|
|
||||||
Create the database file.
|
|
||||||
:param db_path: Relative path of the database file, including the extension.
|
|
||||||
:return: None.
|
|
||||||
"""
|
|
||||||
Path(db_path).touch()
|
|
||||||
|
|
||||||
|
|
||||||
def _convert_type(type_: Optional[type], type_overload: Dict[Optional[type], str]) -> str:
|
def _convert_type(type_: Optional[type], type_overload: Dict[Optional[type], str]) -> str:
|
||||||
"""
|
"""
|
||||||
Given a Python type, return the str name of its
|
Given a Python type, return the str name of its
|
||||||
@@ -90,45 +72,30 @@ def _create_table(class_: type, cursor: sql.Cursor, type_overload: Dict[Optional
|
|||||||
cursor.execute(f"CREATE TABLE IF NOT EXISTS {class_.__name__.lower()} ({sql_fields});")
|
cursor.execute(f"CREATE TABLE IF NOT EXISTS {class_.__name__.lower()} ({sql_fields});")
|
||||||
|
|
||||||
|
|
||||||
def _create_entry(self, cur: sql.Cursor) -> None:
|
def _create_entry(self) -> None:
|
||||||
"""
|
"""
|
||||||
Given an object, create the entry for the object. As a side-effect,
|
Given an object, create the entry for the object. As a side-effect,
|
||||||
this will set the object_id attribute of the object to the unique
|
this will set the object_id attribute of the object to the unique
|
||||||
id of the entry.
|
id of the entry.
|
||||||
:param cur: Cursor of the database.
|
:param cur: Cursor of the database.
|
||||||
:param self: Instance of the object.
|
:param self: Instance of the object.
|
||||||
:param args: Initialisation arguments.
|
|
||||||
:param kwargs: Initialisation keyword arguments.
|
|
||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
table_name: str = self.__class__.__name__.lower()
|
with sql.connect(getattr(self, "db_path")) as con:
|
||||||
kv_pairs = [item for item in asdict(self).items()]
|
cur: sql.Cursor = con.cursor()
|
||||||
kv_pairs.sort(key=lambda item: item[0]) # Sort by the name of the fields.
|
table_name: str = self.__class__.__name__.lower()
|
||||||
cur.execute(f"INSERT INTO {table_name}("
|
kv_pairs = [item for item in asdict(self).items()]
|
||||||
f"{', '.join(item[0] for item in kv_pairs)})"
|
kv_pairs.sort(key=lambda item: item[0]) # Sort by the name of the fields.
|
||||||
f" VALUES ({', '.join(_convert_sql_format(item[1]) for item in kv_pairs)});")
|
cur.execute(f"INSERT INTO {table_name}("
|
||||||
self.__setattr__("obj_id", cur.lastrowid)
|
f"{', '.join(item[0] for item in kv_pairs)})"
|
||||||
|
f" VALUES ({', '.join(_convert_sql_format(item[1]) for item in kv_pairs)});")
|
||||||
|
self.__setattr__("obj_id", cur.lastrowid)
|
||||||
def _modify_init(dataclass_: type):
|
con.commit()
|
||||||
def modifier(self, *args, **kwargs):
|
|
||||||
self.__init__()
|
|
||||||
if "create_entry" in kwargs and kwargs["create_entry"]:
|
|
||||||
try:
|
|
||||||
with sql.connect(dataclass_.__db_path__) as con:
|
|
||||||
cur: sql.Cursor = con.cursor()
|
|
||||||
self._create_entry(cur)
|
|
||||||
con.commit()
|
|
||||||
except AttributeError:
|
|
||||||
raise TypeError("Are you sure this is a datalite class?")
|
|
||||||
return modifier
|
|
||||||
|
|
||||||
|
|
||||||
def sqlify(db_path: str, type_overload: Optional[Dict[Optional[type], str]] = None,
|
def sqlify(db_path: str, type_overload: Optional[Dict[Optional[type], str]] = None,
|
||||||
*args, **kwargs) -> Callable:
|
*args, **kwargs) -> Callable:
|
||||||
def decorator(dataclass_: type, *args_i, **kwargs_i):
|
def decorator(dataclass_: type, *args_i, **kwargs_i):
|
||||||
if not _database_exists(db_path):
|
|
||||||
_create_db(db_path)
|
|
||||||
type_table: Dict[Optional[type], str] = {None: "NULL", int: "INTEGER", float: "REAL",
|
type_table: Dict[Optional[type], str] = {None: "NULL", int: "INTEGER", float: "REAL",
|
||||||
str: "TEXT", bytes: "BLOB"}
|
str: "TEXT", bytes: "BLOB"}
|
||||||
if type_overload is not None:
|
if type_overload is not None:
|
||||||
@@ -136,7 +103,48 @@ def sqlify(db_path: str, type_overload: Optional[Dict[Optional[type], str]] = No
|
|||||||
with sql.connect(db_path) as con:
|
with sql.connect(db_path) as con:
|
||||||
cur: sql.Cursor = con.cursor()
|
cur: sql.Cursor = con.cursor()
|
||||||
_create_table(dataclass_, cur, type_table)
|
_create_table(dataclass_, cur, type_table)
|
||||||
dataclass_.__db_path__ == db_path # We add the path of the database to class itself.
|
setattr(dataclass_, 'db_path', db_path) # We add the path of the database to class itself.
|
||||||
dataclass_.__init__ = _modify_init(dataclass_) # Replace the init method.
|
dataclass_.create_entry = _create_entry
|
||||||
return dataclass_
|
return dataclass_
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def is_fetchable(class_: type, obj_id: int) -> bool:
|
||||||
|
"""
|
||||||
|
Check if a record is fetchable given its obj_id and
|
||||||
|
class_ type.
|
||||||
|
:param class_: Class type of the object.
|
||||||
|
:param obj_id: Unique obj_id of the object.
|
||||||
|
:return: If the object is fetchable.
|
||||||
|
"""
|
||||||
|
with sql.connect(getattr(class_, 'db_path')) as con:
|
||||||
|
cur: sql.Cursor = con.cursor()
|
||||||
|
try:
|
||||||
|
cur.execute(f"SELECT 1 FROM {class_.__name__.lower()} WHERE obj_id = {obj_id};")
|
||||||
|
except sql.OperationalError:
|
||||||
|
raise KeyError(f"Table {class_.__name__.lower()} does not exist.")
|
||||||
|
return bool(cur.fetchall())
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_from(class_: type, obj_id: int) -> Any:
|
||||||
|
"""
|
||||||
|
Fetch a class_ type variable from its bound db.
|
||||||
|
:param class_: Class to fetch.
|
||||||
|
:param obj_id: Unique object id of the class.
|
||||||
|
:return: The object whose data is taken from the database.
|
||||||
|
"""
|
||||||
|
table_name = class_.__name__.lower()
|
||||||
|
if not is_fetchable(class_, obj_id):
|
||||||
|
raise KeyError(f"An object with the id {obj_id} in table {table_name} does not exist."
|
||||||
|
f"or is otherwise unable to be fetched.")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
@sqlify(db_path="db.db")
|
||||||
|
@dataclass
|
||||||
|
class Student:
|
||||||
|
student_id: int
|
||||||
|
student_name: str = "John Smith"
|
||||||
|
is_fetchable(Student, 2)
|
||||||
Reference in New Issue
Block a user