pleasant-database 1.3.0__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ # MIT License
2
+
3
+ Copyright (c) 2025 EliasRodkey
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,13 @@
1
+ Metadata-Version: 2.4
2
+ Name: pleasant-database
3
+ Version: 1.3.0
4
+ Summary: package for handling local database files and data
5
+ Author-email: Elias Rodkey <elias.rodkey@gmail.com>
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Requires-Dist: requests
9
+ Requires-Dist: numpy
10
+ Requires-Dist: sqlalchemy
11
+ Requires-Dist: loggers
12
+ Requires-Dist: pandas
13
+ Dynamic: license-file
@@ -0,0 +1,115 @@
1
+ # local_db_handler
2
+
3
+ this package is a modular python package that allows for easy
4
+ creation, reading, editing, and deleting local database files
5
+
6
+ ## License
7
+
8
+ [MIT](https://choosealicense.com/licenses/mit/)
9
+
10
+ ## Usage/Examples
11
+
12
+ **Install Package**
13
+ Ensure you have access to the github repository
14
+ Run the command:
15
+ pip install git+<https://github.com/EliasRodkey/local_db.git>
16
+
17
+ ### Import Package
18
+
19
+ ```python
20
+ From local_db_handler import DatabaseFile, DatabaseManager, BaseTable
21
+ ```
22
+
23
+ ### Set Up Table Object
24
+
25
+ Use the BaseTable class to create a table object that will be connected to using the DatabaseManager
26
+
27
+ ```python
28
+ class TableName(BaseTable):
29
+ """A table object"""
30
+ __tablename__ = "table_name"
31
+ id = Column(Integer, primary_key=True, autoincrement=True)
32
+ name = Column(String(50), nullable=False)
33
+ age = Column(Integer)
34
+ email = Column(String(100), unique=True)
35
+ ```
36
+
37
+ The attributes here represent column names in the table, additional funcionality can be added to avoid duplicate entries, for example:
38
+
39
+ ```python
40
+ email = Column(String(100), unique=True)
41
+ ```
42
+
43
+ This forces all emails to be unique in the table.
44
+ Another option is to add a special hash row that would be unique to each entry:
45
+
46
+ ```python
47
+ import hashlib
48
+
49
+ def generate_hash(row):
50
+ # Concatenate values of all relevant columns
51
+ data = f"{row["col1"]}_{row["col2"]}_{row["col3"]}"
52
+ return hashlib.sha256(data.encode()).hexdigest()
53
+
54
+ df["unique_id"] = df.apply(generate_hash, axis=1)
55
+ ```
56
+
57
+ Additionally the \_\_tableargs\_\_ can be updated to make a custom unique filter based on multiple columns to the class attributes:
58
+
59
+ ```python
60
+ __table_args__ = (
61
+ UniqueConstraint("column1", "column2", name="unique_combo"), # Combination must be unique
62
+ )
63
+ ```
64
+
65
+ ### Creating the DataFile
66
+
67
+ The DatabaseFile object represents the actual file of the database and is required to initialize a DatabaseManager object.
68
+
69
+ ```python
70
+ file = DatabaseFile(db_name, directory="data/dbs")
71
+
72
+ db_name: valid .db filename
73
+ directory: relative path to database directory (default data//dbs)
74
+ ```
75
+
76
+ DatabaseFile funcitons:
77
+
78
+ ```python
79
+ file.create() # Creates a db file in the directory location
80
+ file.move(target_directory) # Moves the db file to a new directory
81
+ file.exists() # Returns boolean, if the file exists
82
+ file.delete() # Deletes the actual db file
83
+ ```
84
+
85
+ ### Create DatabaseManager
86
+
87
+ The DatabaseManager takes a file and table as arguments and allows for common database operations on that table
88
+
89
+ ```python
90
+ manager = DatabaseManager(table_class: BaseTable, databasefile: DatabaseFile)
91
+ ```
92
+
93
+ The DatabaseFile functions can be accessed through the DatabaseManager.file attribute
94
+ Basic database funcitons:
95
+
96
+ ```python
97
+ manager.add_item(entry: dictionary with columns matching the db table class)
98
+ manager.add_multiple_items(entries: list of entries)
99
+ manager.append_dataframe(df) # pandas DataFrame with columns that match the db table class
100
+
101
+ manager.fetch_all(as_dataframe=True) # returns all table class instances in the table
102
+ manager.fetch_item_by_id(id: int, as_dataframe=True) # returns an individual table class instance with data
103
+ manager.fetch_items_by_attribute(**kwargs, as_dataframe=True) # allows filtering of table by kwargs
104
+ manager.filter_items(filters: dict, use_or=False, as_dataframe=True) # allows filtering of database table and reading of filtered items
105
+ manager.to_dataframe() -> Returns the entire database as a pandas DataFrame
106
+
107
+ manager.update_item(item_id: int, **kwargs) # updates values kwargs of an item with a given ID
108
+ manager.delete_item(item_id: int) # Deletes an item with the given item id
109
+ manager.delete_items_by_attribute(**kwargs) # Deletes items in a database where column values match kwargs.
110
+ manager.delete_items_by_filter(filters: dict, use_or=False) # Deletes items in a database where filters apply.
111
+ manager.clear_table() # Deletes all items in the database table.
112
+
113
+ manager.start_session() # initiates when instance initialized
114
+ manager.end_session() # should be called before exiting program
115
+ ```
@@ -0,0 +1,43 @@
1
+ #!python3
2
+ """
3
+ local_db_handler
4
+
5
+ This package contains modules related to local database connection / creation / deletion / editing / etc.
6
+
7
+ Modules:
8
+ - database_connections.py
9
+ - database_file.py:
10
+ - database_manager.py:
11
+ - utils.py:
12
+ """
13
+ # Metadata
14
+ __version__ = "1.3.0"
15
+ __author__ = "Elias Rodkey"
16
+
17
+ # Package Level Constants
18
+ import os
19
+ DEFAULT_DB_DIRECTORY = os.path.join(os.getcwd(), "data", "dbs")
20
+ LOG_DIRECTORY = os.path.join(os.getcwd(), "data", "logs")
21
+
22
+ # Configure root logger
23
+ from loggers import configure_logging
24
+ handler_controller = configure_logging(log_directory=LOG_DIRECTORY)
25
+
26
+ # Imports modules, functions, and classes for clean package interface
27
+ from .base_table import BaseTable, DatabaseIntegrityError
28
+ from .database_connections import create_engine_conn, create_session
29
+ from .database_file import DatabaseFile
30
+ from .database_manager import DatabaseManager
31
+ from .utils import check_db_exists, is_db_file
32
+
33
+ # Defines importable functions for package
34
+ __all__ = [
35
+ "check_db_exists",
36
+ "is_db_file",
37
+ "create_engine_conn",
38
+ "create_session",
39
+ "DatabaseFile",
40
+ "DatabaseManager",
41
+ "BaseTable",
42
+ "DatabaseIntegrityError"
43
+ ]
@@ -0,0 +1,127 @@
1
+ """
2
+ local_db.base_table.py
3
+ This module defines a base table class using SQLAlchemy"s declarative base.
4
+ It provides a foundation for creating database models and includes utility
5
+ functions to retrieve column names and their corresponding types.
6
+
7
+ Classes:
8
+ Base: The declarative base class for all database models.
9
+ BaseTable: An abstract base class for database tables with utility methods.
10
+ """
11
+
12
+ # Standard library imports
13
+ from datetime import datetime, date, time, timedelta
14
+ from decimal import Decimal
15
+ from enum import Enum
16
+ import re
17
+ from typing import Any
18
+ import uuid
19
+
20
+ from sqlalchemy.orm import declarative_base
21
+ from sqlalchemy import (
22
+ Column, # Defines a column in a table
23
+ UniqueConstraint, # Ensures unique values in specified columns
24
+ Integer, # Integer type
25
+ String, # String type with optional length
26
+ Float, # Floating-point number
27
+ Boolean, # Boolean type (True/False)
28
+ Date, # Date type (year, month, day)
29
+ DateTime, # Date and time type
30
+ Time, # Time type (hour, minute, second)
31
+ Text, # Large text field
32
+ LargeBinary, # Binary data (e.g., files, images)
33
+ JSON, # JSON-encoded data
34
+ Enum, # Enumerated type with fixed values
35
+ Numeric, # Fixed-precision number
36
+ SmallInteger, # Smaller integer type
37
+ BigInteger, # Larger integer type
38
+ Interval, # Time interval
39
+ Unicode, # Unicode string
40
+ UnicodeText, # Large Unicode text field
41
+ PickleType, # Stores Python objects serialized via pickle
42
+ UUID, # Universally unique identifier
43
+ ARRAY, # Array type (PostgreSQL-specific)
44
+ )
45
+
46
+ SQLA_TYPE_MAP = {
47
+ Integer: int,
48
+ String: str,
49
+ Float: float,
50
+ Boolean: bool,
51
+ Date: date,
52
+ DateTime: datetime,
53
+ Time: time,
54
+ Text: str,
55
+ LargeBinary: bytes,
56
+ JSON: dict,
57
+ Enum: Enum,
58
+ Numeric: Decimal,
59
+ SmallInteger: int,
60
+ BigInteger: int,
61
+ Interval: timedelta,
62
+ Unicode: str,
63
+ UnicodeText: str,
64
+ PickleType: Any,
65
+ UUID: uuid.UUID,
66
+ ARRAY: list,
67
+ }
68
+
69
+ Base = declarative_base()
70
+
71
+
72
+
73
+ class BaseTable(Base):
74
+ """
75
+ Abstract base class for database tables.
76
+
77
+ properties:
78
+ - column_names: Returns a list of column names for the model.
79
+ - column_types: Returns a dictionary mapping column names to their types.
80
+ """
81
+ __abstract__ = True # Prevents SQLAlchemy from creating a table for this class
82
+
83
+ @classmethod
84
+ def get_column_names(cls):
85
+ """Returns a list of column names for the model."""
86
+ return [column.name for column in cls.__table__.columns]
87
+
88
+
89
+ @classmethod
90
+ def get_column_sqla_types(cls):
91
+ """Returns a dictionary mapping column names to their types."""
92
+ return {column.name: type(column.type) for column in cls.__table__.columns}
93
+
94
+ @classmethod
95
+ def get_column_python_types(cls):
96
+ """Returns a dictionary mapping column names to their types."""
97
+ return {column.name: SQLA_TYPE_MAP[type(column.type)] for column in cls.__table__.columns}
98
+
99
+ @property
100
+ def column_names(self):
101
+ """Returns a list of column names for the instance."""
102
+ return self.get_column_names()
103
+
104
+ @property
105
+ def column_types(self):
106
+ """Returns a dictionary mapping column names to their types for the instance."""
107
+ return self.get_column_sqla_types()
108
+
109
+
110
+
111
+ class DatabaseIntegrityError(Exception):
112
+ def __init__(self, orig: Exception, table: "BaseTable"):
113
+ self.table_name = table.__tablename__
114
+ self.message = str(orig.orig)
115
+ match = re.search(r"UNIQUE constraint failed: \w+\.(\w+)", self.message)
116
+ self.column = match.group(1) if match else None
117
+ detail = f", column: {self.column}" if self.column else ""
118
+ super().__init__(f"Database integrity error in {self.table_name}{detail}. {self.message}")
119
+
120
+
121
+
122
+ class ItemNotFoundError(Exception):
123
+ def __init__(self, item_id, table: BaseTable, message: str="Item not found in database:"):
124
+ self.item_id = item_id
125
+ self.table_name = table.__tablename__
126
+ self.message = f"{message} {self.table_name}, id: {item_id}"
127
+ super().__init__(self.message)
@@ -0,0 +1,57 @@
1
+ #!python3
2
+ """
3
+ local_db.database_connections.py
4
+
5
+ This module contains functions which allow for connections to existing local database files
6
+
7
+ Functions:
8
+ - create_engine_conn: creates a SQLalchemy engine object
9
+ - create_session: creates and returns a SQLalchemy Session class object
10
+ """
11
+
12
+ # Third-Party library imports
13
+ from sqlalchemy import create_engine
14
+ from sqlalchemy.orm import sessionmaker
15
+
16
+ # Local Imports
17
+ from pleasant_database import DEFAULT_DB_DIRECTORY
18
+ from pleasant_database.utils import LoggingExtras
19
+
20
+ # Initialize module logger
21
+ import logging
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ # Functions
26
+ def create_engine_conn(relative_db_path:str=DEFAULT_DB_DIRECTORY) -> object:
27
+ """
28
+ Create and returns an SQLalchemy engine from a given db URL string
29
+
30
+ Args:
31
+ relative_db_path (str): the relative path to the database file, must be a valid .db filename
32
+
33
+ Returns:
34
+ engine: a SQLalchemy engine object which can be used to connect to the database
35
+ """
36
+
37
+ logger.info(f"Creating connection engine to {relative_db_path}...", extra={
38
+ LoggingExtras.FILE: relative_db_path
39
+ })
40
+
41
+ return create_engine(f"sqlite:///{relative_db_path}")
42
+
43
+
44
+ def create_session(engine):
45
+ """
46
+ Create and return a SQLalchemy session
47
+
48
+ Args:
49
+ engine: a SQLalchemy engine object which can be used to connect to the database
50
+
51
+ Returns:
52
+ Session: a SQLalchemy session object which can be used to interact with the database
53
+ """
54
+ logger.info(f"Creating session for {engine}...")
55
+
56
+ Session = sessionmaker(bind=engine)
57
+ return Session()
@@ -0,0 +1,129 @@
1
+ #!python3
2
+ """
3
+ local_db.database_file.py
4
+
5
+ this module creates, deletes, and archives database files within a python project
6
+
7
+ required project structure:
8
+
9
+ root\\
10
+ |───data
11
+ | └──db_files
12
+ | └──log_dir
13
+ |───src
14
+ └ └──package
15
+
16
+ Class:
17
+ - DatabaseFile: a class which represents a db file object
18
+ """
19
+
20
+ # Standard library imports
21
+ import sqlite3
22
+
23
+ # Third-party imports
24
+ import os
25
+
26
+ # local imports
27
+ from . import DEFAULT_DB_DIRECTORY
28
+ from .utils import LoggingExtras, check_db_exists, is_db_file
29
+
30
+ # Initialize module logger
31
+ import logging
32
+ logger = logging.getLogger(__name__)
33
+
34
+
35
+
36
+ class DatabaseFile():
37
+ """
38
+ Class which manages a single database file, name attribute must be valid .db filename
39
+
40
+ Functions:
41
+ - exxists: checks if the database file exists in the given directory
42
+ - create: creates a new database file in the given directory
43
+ - move: moves the database file to a new directory
44
+ - delete: deletes the database file from the filesystem
45
+ """
46
+ def __init__(self, name, directory=DEFAULT_DB_DIRECTORY):
47
+ """
48
+ initializes a DatabaseFile object with a name and directory
49
+
50
+ Args:
51
+ name (str): the name of the database file, must be a valid .db filename
52
+ directory (str): the relative directory where the database file is located, default is "os.curdir/data/db_files"
53
+ """
54
+ # Check if the name is a valid database filename
55
+ if is_db_file(name):
56
+ self.name = name
57
+ else:
58
+ logger.error(f"{name} is not a valid database filename.", extra={LoggingExtras.FILE: name})
59
+ raise ValueError(f"Database file {name} is not a valid database filename.")
60
+
61
+ # Initialize the directory and path attributes
62
+ self.directory = directory
63
+ self.file_path = os.path.join(self.directory, self.name)
64
+ self.abspath = os.path.abspath(self.file_path)
65
+
66
+ # If the directory does not exist, create it
67
+ if not os.path.exists(self.directory):
68
+ logger.warning(f"Database directory {directory} does not exist. Creating directory...", extra={LoggingExtras.FILE: directory})
69
+ os.makedirs(directory)
70
+
71
+
72
+ def exists(self) -> bool:
73
+ """checks the if the database name exists in the given directiry"""
74
+ return check_db_exists(self.name, self.directory)
75
+
76
+
77
+ def create(self):
78
+ """creates a new database file in a given directory, default .\\data."""
79
+ logger.info(f"Creating database file {self.name}...")
80
+ if self.exists():
81
+ logger.info(f"Database file {self.name} already exists in {self.directory}", extra={LoggingExtras.FILE: os.path.join(self.directory, self.name)})
82
+ else:
83
+ sqlite3.connect(self.file_path).close()
84
+ logger.info(f"Database file {self.name} created.", extra={LoggingExtras.FILE: self.name})
85
+ logger.debug(f"New db file path: {self.file_path}", extra={LoggingExtras.FILE: self.file_path})
86
+
87
+
88
+ def move(self, target_directory: str) -> None:
89
+ """moves a DatabaseFile object to a new directory"""
90
+ logger.info(f"Moving {self.name} from {self.directory} to {target_directory}...", extra={
91
+ LoggingExtras.FILE: self.name,
92
+ LoggingExtras.FILE: target_directory
93
+ })
94
+
95
+ # Check if db with that filename already exists in target dir. return False, not moved
96
+ target_exists = check_db_exists(self.name, target_directory)
97
+ if target_exists:
98
+ # Does nothing if it already exists
99
+ logger.error(f"Database file {self.name} already exists in {target_directory}.", extra={
100
+ LoggingExtras.FILE: self.name,
101
+ LoggingExtras.FILE: target_directory
102
+ })
103
+ elif self.exists():
104
+ # Moves to new directory if it doesn"t exist and the current file exists
105
+ new_path = os.path.join(target_directory, self.name)
106
+ os.rename(self.file_path, new_path)
107
+ self.file_path = new_path
108
+ self.abspath = os.path.abspath(self.file_path)
109
+ self.directory = target_directory
110
+ else:
111
+ logger.error(f"Database file {self.name} cannot be moved because it doesn't exist in {self.directory}", extra={
112
+ LoggingExtras.FILE: self.name,
113
+ LoggingExtras.FILE: self.directory
114
+ })
115
+
116
+
117
+ def delete(self):
118
+ """deletes the file managed by the DatabaseFile instance"""
119
+ if os.path.exists(self.abspath):
120
+ os.remove(self.abspath)
121
+ logger.info(f"Database file {self.name} deleted from {self.directory}...", extra={
122
+ LoggingExtras.FILE: self.name,
123
+ LoggingExtras.FILE: self.directory
124
+ })
125
+ else:
126
+ logger.error(f"DatabaseFile.delete() -> {self.name} does not exist in {self.directory}. Cannot delete.", extra={
127
+ LoggingExtras.FILE: self.name,
128
+ LoggingExtras.FILE: self.directory
129
+ })