DataBase attribute package
Project description
DbAttribute - Database Attribute
DbAttribute is an ORM library designed to simplify database interactions. Core capabilities:
- Automatic state synchronization
Object attribute changes are automatically tracked and persisted to the database without requiring explicit commit calls.
- Direct object manipulation Supports both value assignment (obj.attr = value) and in-place modification of container types:
obj.books.append("New Book")
obj.settings["theme"] = "dark"
- Expressive query syntax Filtering uses Python operators with natural syntax:
# Find users older than 18 named John
User.get((User.age > 18) & (User.name == "John"))
# Get all users named Bob
[user for user in User if user.name == "Bob"]
The library provides tools for declarative model definition, relationship management, and database operation optimization through configurable synchronization modes.
Table of contents
Supported types
This module supports standard types: int, float, str, bool, None, tuple, list, set, dict, datetime.
If a developer needs other data types, they will need to write an adapter class.
Install
The package can be obtained from PyPI and installed in a single step:
pip install db_attribute
It can also be obtained from source (requires git):
pip install git+https://github.com/shutkanos/Db-Attribute.git
How to use it
Connect to DB
MySQL
Connect to MySQL database by providing host, credentials, and database name:
from db_attribute import db_work, connector
connect_obj = connector.MySQLConnection(host='localhost', user='root', password='password', database='mydb')
db_work_obj = db_work.Db_work(connect_obj)
SQLite
Connect to SQLite database (local file or in-memory):
from db_attribute import db_work, connector
# File-based database
connect_obj = connector.SQLiteConnection('/path/to/database.db')
db_work_obj = db_work.Db_work(connect_obj)
# In-memory database
connect_obj = connector.SQLiteConnection(':memory:')
db_work_obj = db_work.Db_work(connect_obj)
# With specific options
connect_obj = connector.SQLiteConnection('/path/to/database.db', timeout=10)
db_work_obj = db_work.Db_work(connect_obj)
Create class
To create any class (Table):
- Set metaclass
DbAttributeMetaclass - Inheritance the
DbAttribute(optional, since it inherits automatically when using a metaclass) - Set dbworkobj for connect to database
- Define fields using annotations or DbField for database columns
from db_attribute import DbAttribute, DbAttributeMetaclass, db_work, connector
from db_attribute.db_types import DbField
connect_obj = connector.MySQLConnection(host='localhost', user='root', password='password', database='mydb')
db_work_obj = db_work.Db_work(connect_obj)
class User(DbAttribute, metaclass=DbAttributeMetaclass, __dbworkobj__=db_work_obj):
name: str = DbField(default='NotSet') # Ok
age: int = -1 # Ok
ban = DbField(default=False) # Ok
other_int_information = 100 # Need annotation or DbField - not error, but not saved
list_of_books = DbField(default_factory=lambda: ['name of first book']) # Ok
settings: dict = DbField(default_factory=dict) # Ok
Each instance has a unique id identifier. It is inherited from DbAttribute and stored in __dict__
Options
Options can be set in different ways:
class User(DbAttribute, metaclass=DbAttributeMetaclass, __dbworkobj__ = db_work_obj):
pass
class User(DbAttribute, metaclass=DbAttributeMetaclass):
__dbworkobj__ = db_work_obj
class User(DbAttribute, metaclass=DbAttributeMetaclass):
class Meta:
__dbworkobj__ = db_work_obj
class BaseMeta:
__dbworkobj__ = dbworkobj
class Class_A(DbAttribute, metaclass=DbAttributeMetaclass):
Meta = BaseMeta
class Class_B(DbAttribute, metaclass=DbAttributeMetaclass):
Meta = BaseMeta
All options:
__dbworkobj__- database work object (required parameter),__max_repr_recursion_limit__- maximum recursion limit for__repr__of DbAttribute__repr_class_name__- sets the name of this class when using the method__repr__of DbAttribute__table_name__- sets custom table name instead of the class name. By default, it is inherited (like any other class attribute), so child classes automatically share the same tables as the parent.
Register db work object
If dbworkobj is not available at the time of class definition, you can defer registration using one of two approaches.
Using __skip_dbworkobj__ = True
Set __skip_dbworkobj__ = True in Meta, then call register_dbworkobj() on each class individually:
class BaseMeta:
__skip_dbworkobj__ = True
class User(DbAttribute, metaclass=DbAttributeMetaclass):
Meta = BaseMeta
class Book(DbAttribute, metaclass=DbAttributeMetaclass):
Meta = BaseMeta
User.register_dbworkobj(dbworkobj)
Book.register_dbworkobj(dbworkobj)
Using DbWorkMarker (recommended for multiple classes)
Define a marker with a group name, place it in Meta.__dbworkobj__, then connect all classes at once via DbWorkManager:
from db_attribute.db_types import DbWorkMarker, DbWorkManager
class MainMeta:
__dbworkobj__ = DbWorkMarker('main')
class User(DbAttribute, metaclass=DbAttributeMetaclass):
Meta = MainMeta
class Book(DbAttribute, metaclass=DbAttributeMetaclass):
Meta = MainMeta
DbWorkManager.connect('main', dbworkobj)
Class inheritance
A developer can create a child class from an existing DbAttribute class. The child class stores all its fields (including inherited ones) in its own separate database tables, fully isolated from the parent's tables.
from db_attribute.db_types import DbField
class UserBase(DbAttribute, metaclass=DbAttributeMetaclass):
Meta = BaseMeta
nameuser: str = DbField(default="")
rank: str = DbField(default="User")
def promote(self):
self.rank = 'Admin'
class UserChild(UserBase):
some_data: int = DbField(default=0)
user1 = UserChild(id=1, nameuser="Oleg", some_data=1)
user2 = UserChild(id=2, nameuser="Bob", some_data=2)
print(user1) # UserChild(id=1, nameuser='Oleg', rank='User', some_data=1)
print(user2) # UserChild(id=2, nameuser='Bob', rank='User', some_data=2)
user2.promote()
print(user2) # UserChild(id=2, nameuser='Bob', rank='Admin', some_data=2)
Key points:
- The child class inherits all fields and methods from the parent
- No need to repeat
metaclass=DbAttributeMetaclass— it is inherited automatically - The child uses the same
dbworkobjas the parent by default; a different one can be set viaMeta UserBaseandUserChildare completely independent in the database:UserBase(id=1)andUserChild(id=1)are different records stored in different tables- Methods defined in the parent work correctly on child instances and write to the child's own tables:
user1.promote()
print(user1.rank) # 'Admin' — written to table 'cls userchild atr rank', not 'cls userbase atr rank'
To use a different dbworkobj for the child class, set it via Meta:
class UserPremium(UserBase):
class Meta:
__dbworkobj__ = other_dbworkobj
premium_level: int = DbField(default=1)
Work with obj
Create new object
To create an object, use an id (optional) and other fields (optional),
obj = User(id=3) # other field set to defaults value
print(obj) # User(id=3, name=*default value*)
# or:
obj = User.get(id=3)
print(obj) # User(id=3, name=*default value*)
obj = User(name='Ben', id=3)
print(obj) # User(id=3, name='Ben')
obj = User(name='Alica')
print(obj) # User(id=4, name='Alica')
obj = User(name='Alica')
print(obj) # User(id=5, name='Alica')
If a developer needs to recreate an object, he can call DbAttribute cls with id.
obj = User(name='Ben', age=20, id=3) #insert obj to db
print(obj) # User(id=3, name='Ben', age=20)
obj = User(id=3)
print(obj) # User(id=3, name='Ben', age=20)
obj = User('Anna', id=3)
print(obj) # User(id=3, name='Anna', age=20)
obj = User(age=25, id=3)
print(obj) # User(id=3, name='Anna', age=25)
obj = User(id=3)
print(obj) # User(id=3, name='Anna', age=25)
Finding objects
If a developer needs to find an object, they can use the 'get' method.
The get() method returns:
- Single object if found
- Object with smallest ID if multiple matches exist
Noneif no matches found
#create objs
obj = User(name='Bob', age=20, id=1)
obj = User(name='Bob', age=30, id=2)
obj = User(name='Anna', age=20, id=3)
#finds objs
print(User.get(id=2)) # User(id=2, name='Bob', age=30)
print(User.get((User.age == 30) & (User.name == 'Bob')))# User(id=2, name='Bob', age=30)
print(User.get(User.name == 'Anna')) # User(id=3, name='Anna', age=20)
print(User.get(User.name == 'Bob')) # User(id=1, name='Bob', age=20)
print(User.get(User.name == 'Other name')) # None
#finds all objs
print(User.gets(User.name == 'Bob')) # [User(id=1, name='Bob', age=20), User(id=2, name='Bob', age=30)]
print(User.gets([2, 3])) # [User(id=2, name='Bob', age=30), User(id=3, name='Anna', age=20)]
To check the correctness of writing a logical expression, you can:
print(User.name == 'Anna') # (User.name = 'Anna')
print((User.age == 30) & (User.name == 'Bob')) # ((User.age = 30) and (User.name = 'Bob'))
Use '&' and '|' instead of the 'and' and 'or' operators. The 'and' and 'or' operators are not supported
Iterations
If a developer needs to iterate through all the elements of a class, they can use standard Python tools.
print(list(User))
# [User(id=1, name='Bob', age=30), User(id=2, name='Bob', age=20), User(id=3, name='Anna', age=20)]
print([i for i in User if i.age < 30])
# [User(id=2, name='Bob', age=20), User(id=3, name='Anna', age=20)]
for i in User:
print(i)
# User(id=1, name='Bob', age=30)
# User(id=2, name='Bob', age=20)
# User(id=3, name='Anna', age=20)
⚠️ Iterations loads all objects - not recommended for large tables
Change attribute of obj
obj = User(name='Bob', list_of_books=[], id=1)
print(obj) # User(id=1, name='Bob', list_of_books=[])
obj.name = 'Anna'
obj.list_of_books.append('Any name of book')
print(obj) # User(id=1, name='Anna', list_of_books=['Any name of book'])
Dump mode
If in any function you will work with obj, you can activate manual_dump_mode (auto_dump_mode is the default),
auto_dump_mode: attributes don't save in self.dict, all changes automatic dump in db.manual_dump_mode: attributes save in self.dict, and won't dump in db until self.db_attribute_set_dump_mode is called. this helps to quickly perform operations on containers db attributes
DbAttribute.set_auto_dump_mode set auto_dump_mode and call dump
DbAttribute.set_manual_dump_mode set manual_dump_mode
user = User(id=1, any_db_data1=531, any_db_data2='string')
print(user.__dict__)
# {'id': 1}
user.set_manual_dump_mode()
print(user.__dict__)
# {'id': 1, '_any_db_data1': 531, '_any_db_data2': 'string'}
Or set dump mode for individual attributes
user = User(id=1, any_db_data1=531, any_db_data2='string')
print(user.__dict__)
# {'id': 1}
user.set_manual_dump_mode({'any_db_data1'})
print(user.__dict__)
# {'id': 1, '_any_db_data1': 531}
user = User(id=1, list_of_books=[])
user.set_manual_dump_mode()
for i in range(10 ** 5):
user.list_of_books.append(i)
user.set_auto_dump_mode()
If a developer needs to dump attributes to db with manual_dump_mode, you can use DbAttribute.db_attribute_dump
user = User(id=1, list_of_books=[])
user.set_manual_dump_mode()
for i in range(10 ** 4):
user.list_of_books.append(i)
user.dump() # dump the list_of_books to db
for i in range(10 ** 4):
user.list_of_books.append(i)
user.set_auto_dump_mode()
Types
DbField
Dbfield is used to configure fields, namely:
default(Any): the default value of this Field (default takes precedence over the default_factory)default_factory(Any): the default factory of this Fieldpython_type(Any): python type of data, example: str, int (python_type takes precedence over the data type specified in the annotation)mysql_type(str): mysql type of data, example: 'varchar(50)', 'bigint'repr(bool): Include field in__repr__()outputinit(bool): Include field in constructor (__init__())search_default(bool): When True, applies default value during searches if record is missing in this field's table (Use this parameter if you understand what it is responsible for.)
For example:
class User(DbAttribute, metaclass=DbAttributeMetaclass):
Meta = BaseMeta
name: str = DbField(default='NotSet', mysql_type='varchar(32)')
hash: str = DbField(default_factory=lambda: str(uuid.uuid4()), mysql_type='varchar(512)', repr=False, init=False, search_default=False)
Db attribute
A developer can set the Db attribute class as data type for another Db attribute class
from db_attribute.db_types import TableType
class Class_A(DbAttribute, metaclass=DbAttributeMetaclass):
Meta = BaseMeta
obj_b: TableType('Class_B')
class Class_B(DbAttribute, metaclass=DbAttributeMetaclass):
Meta = BaseMeta
obj_a: Class_A
To create an object:
obj_a = Class_A(id=15, name='Anna', obj_b=1)
obj_b = Class_B(id=1, name='Bob', obj_a=15)
print(obj_b) # Class_B(id=1, name='Bob', obj_a=Class_A(id=15, name='Anna', obj_b=Class_B(id=1, ...)))
#or
obj_a = Class_A(id=15, name='Anna', obj_b=obj_b)
print(obj_a) # Class_A(id=15, name='Anna', obj_b=Class_B(id=1, name='Bob', obj_a=Class_A(id=15, ...)))
For found obj:
Class_A(id=15, name='Anna', obj_b=1)
obj = Class_B(id=1, name='Bob', obj_a=15)
obj = Class_A.get(Class_A.obj_b == obj)
print(obj) # Class_A(id=15, name='Anna', obj_b=Class_B(id=1, name='Bob', obj_a=Class_A(id=15, ...)))
# And Found with use id of obj:
obj = Class_A.get(Class_A.obj_b == 1)
print(obj) # Class_A(id=15, name='Anna', obj_b=Class_B(id=1, name='Bob', obj_a=Class_A(id=15, ...)))
One-to-Many relationship:
from db_attribute.db_types import DbField
class Author(DbAttribute, metaclass=DbAttributeMetaclass):
Meta = BaseMeta
name: str = ""
books: list = DbField(default_factory=list)
class Book(DbAttribute, metaclass=DbAttributeMetaclass):
Meta = BaseMeta
title: str = ""
author: Author
author = Author(name="George Orwell")
book = Book(title="1984", author=author)
author.books.append(book)
print(author) # Author(id=1, name='George Orwell', books=[Book(id=1, title='1984', author=Author(id=1, ...))])
print(book) # Book(id=1, title='1984', author=Author(id=1, name='George Orwell', books=[Book(id=1, ...)]))
Db classes
When collections are stored in memory, they converted to Db classes
obj = User(1, list_of_books=[1, 2, 3])
print(type(obj.list_of_books)) # DbList
obj = User(1, times=[datetime(2024, 1, 1)])
print(type(obj.times[0])) # DbDatetime
And when collections dumped to db, they converted to json
obj = User(1, list_of_books=[1, 2, 3])
print(obj.list_of_books.dumps()) # {"t": "DbList", "d": [1, 2, 3]}
obj = User(1, times=[datetime(2024, 1, 1), datetime(2027, 7, 7)])
print(obj.list_of_books.dumps())
# {"t": "DbList", "d": [{"t": "DbDatetime", "d": "2024-01-01T00:00:00"}, {"t": "DbDatetime", "d": "2027-07-07T00:00:00"}]}
Custom Db Classes
And to create a custom 'Db class', you need to
- Create regular class
- Inherit from DbClass (DbClass - first. It is important) and your regular class for custom Db class
- Set a Decorator with or without the necessary parameters
- Set at least the
__convert_to_db__module, according to the documentation - add additional modules.
from db_attribute import db_class
# for exemple you have your class:
class UserDataClass:
def __init__(self, value = None):
self.value = value
def __repr__(self):
return f'UserDataClass(value={self.value})'
@db_class.DbClassDecorator
class DbUserDataClass(db_class.DbClass, UserDataClass):
def __init__(self, value=None, **kwargs):
# This is not a mandatory method
super().__init__(_call_init=False, **kwargs) # But this call is mandatory
self.__dict__['value'] = value
# Here we set the value of a variable using __dict__.
# This is not necessary, but it speeds up the work with the class.
@classmethod
def __convert_to_db__(cls, obj: UserDataClass, **kwargs):
"""Methode for convert obj to dbclass - need @classmethod and kwargs"""
# This is a mandatory method
# Call with _user_db=True
# Example:
# print(type(DbUserDataClass(value=10))) #UserDataClass
# print(type(DbUserDataClass(value=10, _use_db=True))) #DbUserDataClass
return cls(_use_db=True, value=obj.value, **kwargs)
def __convert_from_db__(self):
"""Reverse convert"""
# This is not a mandatory method.
return self._standart_class(value=self.value)
For example:
class User(DbAttribute, metaclass=DbAttributeMetaclass):
Meta = BaseMeta
data: UserDataClass
user = User(id=1, data=UserDataClass(10))
print(user.data) # UserDataClass(value=10)
user.data.value = 5
print(user.data) # UserDataClass(value=5)
Json type
DbAttribute supports tuple, list, dict, other collections, but these types are slow, because uses Db classes (see speed test).
To solve this problem, use a Json convertation
from db_attribute.db_types import JsonType, DbField
class User(DbAttribute, metaclass=DbAttributeMetaclass):
Meta = BaseMeta
settings: JsonType = DbField(default_factory=lambda: {})
obj = User(1, settings={1: 2, 3: [4, 5]})
print(obj.settings) # {'1': 2, '3': [4, 5]}
print(type(obj.settings)) # dict
- If Developer change obj with JsonType, this obj don't dump to db, you need set the new obj
- JsonType only supports:
dict,list,str,int,float,bool,None
obj = User(1, settings={1: 2, 3: [4, 5]})
del obj.settings['3'] # not changed
obj.settings['1'] = 3 # not changed
obj.settings |= {4: 5} # not changed
print(obj.settings) #{'1': 2, '3': [4, 5]}
obj.settings = {1: 3} # changed
print(obj.settings) #{'1': 3}
Speed Test
The execution speed may vary from computer to computer, so you need to focus on the specified number of operations per second of a regular mysql
- mysql
select- 12500 op/sec - mysql
insert- 8500 op/sec
Get attr
Mysql select - 12500 op/sec
| Type | Operation/seconds | Performance impact |
|---|---|---|
| int | 11658 op/sec | -6% |
| str | 11971 op/sec | -4% |
| tuple | 9685 op/sec | -22% |
| list | 9630 op/sec | -23% |
| dict | 9545 op/sec | -23% |
| JsonType | 11937 op/sec | -4% |
Set attr
Mysql insert - 8500 op/sec
| Type | Operation/seconds | Performance impact |
|---|---|---|
| int | 8056 op/sec | -5% |
| str | 8173 op/sec | -3% |
| tuple | 6284 op/sec | -26% |
| list | 6043 op/sec | -28% |
| dict | 6354 op/sec | -25% |
| JsonType | 7297 op/sec | -14% |
Data base
This module uses MySQL db (License), and for use it, you need install mysql
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file db_attribute-2.1.2.tar.gz.
File metadata
- Download URL: db_attribute-2.1.2.tar.gz
- Upload date:
- Size: 38.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cd5bb0ee8de5b28416d62e24b5f0b4358e0dc4d0de837bbf2e9aa45943c099b5
|
|
| MD5 |
593719ea34224f8d9f3cfbf541ecdeb0
|
|
| BLAKE2b-256 |
10824e541c9490a7e39ba88dd75067cd5d003c5827ac1bb49269ad40be2f7b8c
|
File details
Details for the file db_attribute-2.1.2-py3-none-any.whl.
File metadata
- Download URL: db_attribute-2.1.2-py3-none-any.whl
- Upload date:
- Size: 34.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
87ccfb2a5562432a61045518d165f28a502cd73d33add7797e610ca893079299
|
|
| MD5 |
a4060c59e7c6124bbf2e0f69dbb3682f
|
|
| BLAKE2b-256 |
6c70f0690701a766952892da9c71c279dbae39bed8b4e2cca69c893f232ab4b6
|