Add pagination support

This commit is contained in:
Ege Emir Özkan
2020-08-13 18:17:19 +03:00
parent c051cddac5
commit 6adda2e719
4 changed files with 74 additions and 11 deletions

View File

@@ -11,7 +11,23 @@ using it is extremely simple, say that you have a dataclass definition,
just add the decorator `@datalite(db_name="db.db")` to the top of the just add the decorator `@datalite(db_name="db.db")` to the top of the
definition, and the dataclass will now be bound to the file `db.db` definition, and the dataclass will now be bound to the file `db.db`
For example: ## Download and Install
You can install `datalite` simply by
```shell script
pip install datalite
```
Or you can clone the repository and run
```shell script
python setup.py
```
Datalite has no dependencies! As it is built on Python 3.7+ standard library. Albeit, its tests require `unittest` library.
## Datalite in Action
```python ```python
from dataclasses import dataclass from dataclasses import dataclass
@@ -30,7 +46,9 @@ 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`.
## Entry manipulation ##Basic Usage
### Entry manipulation
After creating an object traditionally, given that you used the `datalite` decorator, After creating an object traditionally, given that you used the `datalite` decorator,
the object has three new methods: `.create_entry()`, `.update_entry()` the object has three new methods: `.create_entry()`, `.update_entry()`
@@ -81,4 +99,10 @@ The last two helper methods, `fetch_if(class_, condition)` fetches all
the records of type `class_` that fit a certain condition. Here conditions the records of type `class_` that fit a certain condition. Here conditions
must be written is SQL syntax. For easier, only one conditional checks, there must be written is SQL syntax. For easier, only one conditional checks, there
is `fetch_equals(class_, field, value)` that checks the value of only one `field` is `fetch_equals(class_, field, value)` that checks the value of only one `field`
and returns the object whose `field` equals the provided `value`. and returns the object whose `field` equals the provided `value`.
#### Pagination
`datalite` also supports pagination on `fetch_if`, `fetch_all` and `fetch_where`,
you can specify `page` number and `element_count` for each page (default 10), for
these functions in order to get a subgroup of records.

View File

@@ -3,6 +3,19 @@ from typing import List, Tuple, Any
from .commons import _convert_sql_format from .commons import _convert_sql_format
def insert_pagination(query: str, page: int, element_count: int) -> str:
"""
Insert the pagination arguments if page number is given.
:param query: Query to insert to
:param page: Page to get.
:param element_count: Element count in each page.
:return: The modified (or not) query.
"""
if page:
query += f" ORDER BY obj_id LIMIT {element_count} OFFSET {(page - 1) * element_count}"
return query + ";"
def is_fetchable(class_: type, obj_id: int) -> bool: def is_fetchable(class_: type, obj_id: int) -> bool:
""" """
Check if a record is fetchable given its obj_id and Check if a record is fetchable given its obj_id and
@@ -83,25 +96,27 @@ def _convert_record_to_object(class_: type, record: Tuple[Any], field_names: Lis
return obj return obj
def fetch_if(class_: type, condition: str) -> tuple: def fetch_if(class_: type, condition: str, page: int = 0, element_count: int = 10) -> tuple:
""" """
Fetch all class_ type variables from the bound db, Fetch all class_ type variables from the bound db,
provided they fit the given condition provided they fit the given condition
:param class_: Class type to fetch. :param class_: Class type to fetch.
:param condition: Condition to check for. :param condition: Condition to check for.
:param page: Which page to retrieve, default all. (0 means closed).
:param element_count: Element count in each page.
:return: A tuple of records that fit the given condition :return: A tuple of records that fit the given condition
of given type class_. of given type class_.
""" """
table_name = class_.__name__.lower() table_name = class_.__name__.lower()
with sql.connect(getattr(class_, 'db_path')) as con: with sql.connect(getattr(class_, 'db_path')) as con:
cur: sql.Cursor = con.cursor() cur: sql.Cursor = con.cursor()
cur.execute(f"SELECT * FROM {table_name} WHERE {condition};") cur.execute(insert_pagination(f"SELECT * FROM {table_name} WHERE {condition}", page, element_count))
records: list = cur.fetchall() records: list = cur.fetchall()
field_names: List[str] = _get_table_cols(cur, table_name) field_names: List[str] = _get_table_cols(cur, table_name)
return tuple(_convert_record_to_object(class_, record, field_names) for record in records) return tuple(_convert_record_to_object(class_, record, field_names) for record in records)
def fetch_where(class_: type, field: str, value: Any) -> tuple: def fetch_where(class_: type, field: str, value: Any, page: int = 0, element_count: int = 10) -> tuple:
""" """
Fetch all class_ type variables from the bound db, Fetch all class_ type variables from the bound db,
provided that the field of the records fit the provided that the field of the records fit the
@@ -109,9 +124,11 @@ def fetch_where(class_: type, field: str, value: Any) -> tuple:
:param class_: Class of the records. :param class_: Class of the records.
:param field: Field to check. :param field: Field to check.
:param value: Value to check for. :param value: Value to check for.
:param page: Which page to retrieve, default all. (0 means closed).
:param element_count: Element count in each page.
:return: A tuple of the records. :return: A tuple of the records.
""" """
return fetch_if(class_, f"{field} = {_convert_sql_format(value)}") return fetch_if(class_, f"{field} = {_convert_sql_format(value)}", page, element_count)
def fetch_range(class_: type, range_: range) -> tuple: def fetch_range(class_: type, range_: range) -> tuple:
@@ -125,10 +142,12 @@ def fetch_range(class_: type, range_: range) -> tuple:
return tuple(fetch_from(class_, obj_id) for obj_id in range_ if is_fetchable(class_, obj_id)) return tuple(fetch_from(class_, obj_id) for obj_id in range_ if is_fetchable(class_, obj_id))
def fetch_all(class_: type) -> tuple: def fetch_all(class_: type, page: int = 0, element_count: int = 10) -> tuple:
""" """
Fetchall the records in the bound database. Fetchall the records in the bound database.
:param class_: Class of the records. :param class_: Class of the records.
:param page: Which page to retrieve, default all. (0 means closed).
:param element_count: Element count in each page.
:return: All the records of type class_ in :return: All the records of type class_ in
the bound database as a tuple. the bound database as a tuple.
""" """
@@ -139,7 +158,7 @@ def fetch_all(class_: type) -> tuple:
with sql.connect(db_path) as con: with sql.connect(db_path) as con:
cur: sql.Cursor = con.cursor() cur: sql.Cursor = con.cursor()
try: try:
cur.execute(f"SELECT * FROM {class_.__name__.lower()}") cur.execute(insert_pagination(f"SELECT * FROM {class_.__name__.lower()}", page, element_count))
except sql.OperationalError: except sql.OperationalError:
raise TypeError(f"No record of type {class_.__name__.lower()}") raise TypeError(f"No record of type {class_.__name__.lower()}")
records = cur.fetchall() records = cur.fetchall()

View File

@@ -5,7 +5,7 @@ with open("README.md", "r") as fh:
setuptools.setup( setuptools.setup(
name="datalite", # Replace with your own username name="datalite", # Replace with your own username
version="0.5.1", version="0.5.2",
author="Ege Ozkan", author="Ege Ozkan",
author_email="egeemirozkan24@gmail.com", author_email="egeemirozkan24@gmail.com",
description="A small package that binds dataclasses to an sqlite database", description="A small package that binds dataclasses to an sqlite database",

View File

@@ -3,6 +3,7 @@ from datalite import datalite
from datalite.fetch import fetch_if, fetch_all, fetch_range, fetch_from, fetch_equals, fetch_where from datalite.fetch import fetch_if, fetch_all, fetch_range, fetch_from, fetch_equals, fetch_where
from sqlite3 import connect from sqlite3 import connect
from dataclasses import dataclass, asdict from dataclasses import dataclass, asdict
from math import floor
from os import remove from os import remove
@@ -73,7 +74,6 @@ class DatabaseMain(unittest.TestCase):
self.assertEqual(len(objects), init_len) self.assertEqual(len(objects), init_len)
class DatabaseFetchCalls(unittest.TestCase): class DatabaseFetchCalls(unittest.TestCase):
def setUp(self) -> None: def setUp(self) -> None:
self.objs = [FetchClass(1, 'a'), FetchClass(2, 'b'), FetchClass(3, 'b')] self.objs = [FetchClass(1, 'a'), FetchClass(2, 'b'), FetchClass(3, 'b')]
@@ -107,5 +107,25 @@ class DatabaseFetchCalls(unittest.TestCase):
[obj.remove_entry() for obj in self.objs] [obj.remove_entry() for obj in self.objs]
class DatabaseFetchPaginationCalls(unittest.TestCase):
def setUp(self) -> None:
self.objs = [FetchClass(i, f'{floor(i/10)}') for i in range(30)]
[obj.create_entry() for obj in self.objs]
def testFetchAllPagination(self):
t_objs = fetch_all(FetchClass, 1, 10)
self.assertEqual(tuple(self.objs[:10]), t_objs)
def testFetchWherePagination(self):
t_objs = fetch_where(FetchClass, 'str_', '0', 2, 5)
self.assertEqual(tuple(self.objs[5:10]), t_objs)
def testFetchIfPagination(self):
t_objs = fetch_if(FetchClass, 'str_ = "0"', 1, 5)
self.assertEqual(tuple(self.objs[:5]), t_objs)
def tearDown(self) -> None:
[obj.remove_entry() for obj in self.objs]
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()