Skip to main content

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 dbworkobj as the parent by default; a different one can be set via Meta
  • UserBase and UserChild are completely independent in the database: UserBase(id=1) and UserChild(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
  • None if 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 Field
  • python_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__() output
  • init (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

db_attribute-2.1.2.tar.gz (38.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

db_attribute-2.1.2-py3-none-any.whl (34.0 kB view details)

Uploaded Python 3

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

Hashes for db_attribute-2.1.2.tar.gz
Algorithm Hash digest
SHA256 cd5bb0ee8de5b28416d62e24b5f0b4358e0dc4d0de837bbf2e9aa45943c099b5
MD5 593719ea34224f8d9f3cfbf541ecdeb0
BLAKE2b-256 10824e541c9490a7e39ba88dd75067cd5d003c5827ac1bb49269ad40be2f7b8c

See more details on using hashes here.

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

Hashes for db_attribute-2.1.2-py3-none-any.whl
Algorithm Hash digest
SHA256 87ccfb2a5562432a61045518d165f28a502cd73d33add7797e610ca893079299
MD5 a4060c59e7c6124bbf2e0f69dbb3682f
BLAKE2b-256 6c70f0690701a766952892da9c71c279dbae39bed8b4e2cca69c893f232ab4b6

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page