Skip to main content

RedBeanPython ORM

Project description

Idea

One of Python's most significant advantages is that we can make applications fast. Boilerplate is low, but we can focus on business logic, quickly deliver value, and make projects production-ready. But even if, with current powerful libraries, "time-to-market" is low, it can be lowered even more.

The idea of RedBeanPython ORM is to decrease the time to market for applications by providing an absolutely zero configuration ORM and, simultaneously, heaving the possibility of zero effort transition to advanced ORM when needed.

RedBeanPython idea is inspired by the matured RedBeanPHP ORM (since 2009 and still running).

RedBeanPython ORM's idea is not to replace the more advanced ORMs (we should not reinvent the wheel) but to cooperate with them. Initially, it can be used as only ORM. When projects evolve, both code bases (RedBeanPython and SQLAlchemy) can coexist and, finally, can be swiftly replaced by SQLAlchemy (if needed).

Tests on PostgreSQL Tests on MySQL Tests on SQLite Coverage

Quality Gate Status Vulnerabilities Security Rating

Installation

RedBeanPython is available on PyPI and can be installed with pip:

pip install redbeanpython

RedBeanPython on PyPI

Documentation

Documentation is available on redbeanpython.org.

Requirements

RedBeanPython requires

  • Python 3.11 or newer.
  • SQLAlchemy 2.0 or newer.
  • alembic 1.7 or newer.

Database requirements

For PostgreSQL support RedBeanPython requires:

  • psycopg 3.0.18 or newer.

For MySQL/MariaDB support RedBeanPython requires:

  • PyMySQL 1.0.2 or newer.

How it works? Let's see an example of a usual project lifecycle.

Stage 1. Development

We are starting a fresh project. We have an Idea, and we want to deliver MVP to production as soon as possible.

We have an empty database (or no database at all at that moment).

Let's store some data:

from datetime import datetime
from decimal import Decimal

from redbeanpython import redbean, Bean

redbean.setup(dsn=False) #(1)

user = Bean('user')
user.name = 'Adam'
user.age = 42
user.subscription_price = Decimal("12.20")
user.subscription_end = datetime.fromisoformat('2024-10-08T16:20:00')

redbean.store(user)
  1. For a temporary SQLite database call redbean.setup(dsn=False). For a production database, DSN consistent with SQLAlchemy should be provided. See connection for more details. For example, you may run a docker container with PostgreSQL:
    docker run -p 5432:5432 -e POSTGRES_PASSWORD=pass -d postgres
    
    And use it:
    redbean.setup('postgresql+psycopg://postgres:pass@localhost/postgres')
    

And that's all.

No schema is need to be created, no migrations need to be run, no config files to update.

Just run the code, and RedBeanPython will create everything automatically.

If we take a look into db, we will see that table was created:

create table user
(
    age                BIGINT,
    id                 VARCHAR(255) not null primary key,
    name               TEXT,
    subscription_end   DATETIME,
    subscription_price NUMERIC(30, 10)
);

In the background, the SQLAlchemy model was created:

from sqlalchemy import Column, BigInteger, String, Text, DateTime, Numeric

from redbeanpython import Model


class User(Model):
    __tablename__ = 'user'
    __table_args__ = {'extend_existing': True}

    age = Column(BigInteger, nullable=True)
    id = Column(String(255), primary_key=True)
    name = Column(Text, nullable=True)
    subscription_end = Column(DateTime, nullable=True)
    subscription_price = Column(Numeric(precision=30, scale=10), nullable=True)

    def __json__(self):
        return self.as_dict()

    def as_dict(self):
        return {
            'age': self.age,
            'id': self.id,
            'name': self.name,
            'subscription_end': self.subscription_end,
            'subscription_price': self.subscription_price,
        }

We do not need this information now, but it might be helpful later. (See section: hybrid mode)

Let's load it from the database, and as we see, the types are preserved:

>>> from redbeanpython import redbean
>>> redbean.setup(dsn=False)
>>> users = list(redbean.find('user'))
>>> user = users[0]
>>> print(dict(user))
{
    'age': 42, 
    'id': '1f0e6e46-6e47-404f-9547-9dc3e9741ec6', 
    'name': 'Adam', 
    'subscription_end': datetime.datetime(2024, 10, 8, 16, 20), 
    'subscription_price': Decimal('12.2000000000')
}
>>> print(type(user.subscription_end), user.subscription_end)
<class 'datetime.datetime'> 2024-10-08 16:20:00

Let's go on; development continues.

In the early stage of the project, life changes are widespread. Often, we even rewrite the first ideas.

With RedBeanPython, this is no problem; we can change the code and run it again. RedBeanPython will adjust everything automatically.

For example, we decided that the user should have a birthday. We can add in code:

# ...
user.birthday = date.fromisoformat('1981-12-01')
# ...

And rerun it. RedBeanPython will adequately adjust the database schema.

The database schema has been adjusted.

create table user
(
    age                BIGINT,
    id                 VARCHAR(255) not null primary key,
    name               TEXT,
    subscription_end   DATETIME,
    subscription_price NUMERIC(30, 10),
    birthday           DATE  -- (1)
);
  1. A new column was added.

Same as model:

class User(Model):
    # (...)
    name = Column(Text, nullable=True)
    subscription_end = Column(DateTime, nullable=True) # (1)
    subscription_price = Column(Numeric(precision=30, scale=10), nullable=True)
    # (...)

    def as_dict(self):
        return {
            # (...)
            'name': self.name,
            'subscription_end': self.subscription_end, # (2)
            'subscription_price': self.subscription_price,
        }
  1. A new property was added.
  2. A new property was added.

And alembic migration was created automatically:

def upgrade() -> None:
    # ### commands auto generated by Alembic - please adjust! ###
    with op.batch_alter_table('user', schema=None) as batch_op:
        batch_op.add_column(sa.Column('birthday', sa.Date(), nullable=True))

def downgrade() -> None:
    # ### commands auto generated by Alembic - please adjust! ###
    with op.batch_alter_table('user', schema=None) as batch_op:
        batch_op.drop_column('birthday')

:information: Migrations will be useful later when the project is production-ready.

Now, when we have MVP ready, we can go to production.

Stage 2: Production

When the project stays production-ready, we would like to freeze production database to avoid potential unexpected changes. No automatic changes will be made to production since that moment.

To freeze the database, we just need to add one parameter to redbean.setup() call:

redbean.setup('...', frozen=True)

Or use REDBEAN_DB_FROZEN=1 environment variable.

And, from that time on production, we can use alembic migrations to manage production database schema changes safely. We fully control how and when the database schema will be changed.

Because alembic migrations are created automatically by RedBeanPython, we can just run:

cd redbean/migrations 
DB_DSN="sqlite:////tmp/redbean.sqlite" alembic upgrade head

on production to apply all changes.

At the same time, everything will work as before in the development environment, and we still have all the benefits from automatic creation.

Stage 3: Project growth

When the project grows, we may need more performance or more sophisticated database features. As RedBeanPython is not intended to be a replacement for SQLAlchemy, instead of extending RedBeanPython forever, we can switch to SQLAlchemy ORM and take full advantage of the giants.

Moreover, we can use both ORMs simultaneously and switch to more advanced ORMs with zero effort for specific cases only.

As SQLAlchemy Models has already been created automatically, we can use it.

Let's see how it works.

from redbean.models.customer import Customer
from redbean.models.invoice import Invoice

redbean.setup(dsn=False)

# redbeanpython code
customer = Bean('customer')
customer.name = 'John'

invoice = Bean('invoice')
invoice.amount = Decimal("12.20")
invoice.customer_id = customer.id

redbean.store_many([invoice, customer])

# sqlalchemy code
session = redbean.session_maker()
query = session.query(Customer, Invoice)
query = query.filter(Customer.id == Invoice.customer_id)
query = query.filter(Invoice.amount > 10)
result = query.all()
for customer, invoice in result:
    print(customer.name, invoice.amount)

query = session.query(
    Invoice.customer_id,
    func.count('*').label('amount'),
    func.sum(Invoice.amount).label('amount'),
)
query = query.group_by(Invoice.customer_id)
for customer_id, invoices_count, invoices_total, in query.all():
    print(f"{customer_id} has: {invoices_count} invoices with a sum: {invoices_total}")

    # redbeanpython code again
    print(customer_id)
    customer = redbean.load('customer', customer_id)
    print(dict(customer))

We can freely mix both ORMs in the same code base. This is a hybrid mode.

Finally, when our project succeeds, it will be on the market for months or years. Development team grows. Codebase enlarges - we can want to switch to SQLAlchemy ORM entirely.

Stage 4: Maturity

When the project matures and we decide to switch entirely to SQLAlchemy ORM, we can say goodbye to RedBeanPython methods and start to use SQLAlchemy only.

Starting with previously automatically generated Models, we will continue to extend them and work with them in a standard SQLAlchemy way.

And then you can let RedBeanPython go. RedBeanPython did its job. It's time for retirement.

It was a beautiful journey!

Check more on redbeanpython.org.

Licence

Licence: MIT

Issue tracker

Issue tracker is available on GitHub.

Contact

Adam Puza: adampuza@redbeanpython.org

___

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

redbeanpython-1.0.2.tar.gz (25.1 kB view hashes)

Uploaded Source

Built Distribution

redbeanpython-1.0.2-py3-none-any.whl (31.1 kB view hashes)

Uploaded Python 3

Supported by

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