Initial commit
This commit is contained in:
commit
ecc17b788a
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
.idea/
|
||||
.venv/
|
||||
venv/
|
||||
*__pycache__*
|
11
Database/__init__.py
Normal file
11
Database/__init__.py
Normal file
@ -0,0 +1,11 @@
|
||||
"""
|
||||
See __main__.py for an example, further documentation can be found within both base.py and database.py
|
||||
"""
|
||||
|
||||
from .base import Base
|
||||
from .database import Database
|
||||
|
||||
__all__ = [
|
||||
'Base',
|
||||
'database'
|
||||
]
|
78
Database/__main__.py
Normal file
78
Database/__main__.py
Normal file
@ -0,0 +1,78 @@
|
||||
import asyncio
|
||||
|
||||
import sqlalchemy
|
||||
from sqlalchemy import orm as sqlalchemy_orm
|
||||
from sqlalchemy import future as sqlalchemy_future
|
||||
|
||||
from Database.base import Base
|
||||
from Database.database import Database
|
||||
|
||||
|
||||
class A(Base):
|
||||
__tablename__ = "a"
|
||||
|
||||
data = sqlalchemy.Column(sqlalchemy.String)
|
||||
create_date = sqlalchemy.Column(sqlalchemy.DateTime, server_default=sqlalchemy.func.now())
|
||||
bs = sqlalchemy_orm.relationship("B")
|
||||
|
||||
# required in order to access columns with server defaults
|
||||
# or SQL expression defaults, subsequent to a flush, without
|
||||
# triggering an expired load
|
||||
__mapper_args__ = {"eager_defaults": True}
|
||||
|
||||
|
||||
class B(Base):
|
||||
__tablename__ = "b"
|
||||
a_id = sqlalchemy.Column(sqlalchemy.ForeignKey("a.id"))
|
||||
data = sqlalchemy.Column(sqlalchemy.String)
|
||||
|
||||
|
||||
async def main():
|
||||
db = Database(
|
||||
"postgresql://localhost:5432/testing",
|
||||
)
|
||||
|
||||
await db.drop_all()
|
||||
await db.create_all()
|
||||
|
||||
# expire_on_commit=False will prevent attributes from being expired
|
||||
# after commit.
|
||||
|
||||
async with db.async_session() as session:
|
||||
async with session.begin():
|
||||
session.add_all(
|
||||
[
|
||||
A(bs=[B(), B()], data="a1"),
|
||||
A(bs=[B()], data="a2"),
|
||||
A(bs=[B(), B()], data="a3"),
|
||||
]
|
||||
)
|
||||
|
||||
stmt = sqlalchemy_future.select(A).options(sqlalchemy_orm.selectinload(A.bs))
|
||||
|
||||
result = await session.execute(stmt)
|
||||
|
||||
for a1 in result.scalars():
|
||||
print(a1)
|
||||
print(f"created at: {a1.create_date}")
|
||||
for b1 in a1.bs:
|
||||
print(b1)
|
||||
|
||||
result = await session.execute(sqlalchemy_future.select(A).order_by(A.id))
|
||||
|
||||
a1 = result.scalars().first()
|
||||
|
||||
a1.data = "new data"
|
||||
|
||||
await session.commit()
|
||||
|
||||
# access attribute subsequent to commit; this is what
|
||||
# expire_on_commit=False allows
|
||||
print(a1.data)
|
||||
|
||||
# for AsyncEngine created in function scope, close and
|
||||
# clean-up pooled connections
|
||||
await db.async_engine.dispose()
|
||||
|
||||
|
||||
asyncio.run(main())
|
70
Database/base.py
Normal file
70
Database/base.py
Normal file
@ -0,0 +1,70 @@
|
||||
import sqlalchemy.orm
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
|
||||
|
||||
@sqlalchemy.orm.as_declarative()
|
||||
class Base:
|
||||
"""
|
||||
Standard base class to define a table in a postgresql database
|
||||
|
||||
Default Columns:
|
||||
-> id
|
||||
- Defines a UUID generated via postgresql's in-built "uuid_generate_v4" function,
|
||||
requires the "uuid-ossp" to be installed to the given database. The extension can be installed via
|
||||
"CREATE EXTENSION IF NOT EXISTS "uuid-ossp";"
|
||||
- May never be null
|
||||
-> creation
|
||||
- Defines the creation date in ISO 8601 format based on the sqlalchemy DateTime class and generated by the
|
||||
database using func.now()
|
||||
- May never be null
|
||||
-> Modification
|
||||
- Defines a ISO 8601 timestamp that is updated anytime the data in a column is modified for a given row
|
||||
- Can be null
|
||||
|
||||
Usage:
|
||||
This class, "Base", must be inherited by subclasses to define tables within a database. A subclass must define
|
||||
a single string with a variable name of "__tablename__" which defines the actual table name within the database.
|
||||
|
||||
-> Example Implementation
|
||||
>>> class SomeTable(Base):
|
||||
>>> __tablename__ = "Some Table"
|
||||
>>> data = sqlalchemy.Column(sqlalchemy.String)
|
||||
"""
|
||||
|
||||
__table__: sqlalchemy.Table
|
||||
|
||||
# This ID column expects the extension "uuid-ossp" to be installed on the postgres DB
|
||||
# Can be done via "CREATE EXTENSION IF NOT EXISTS "uuid-ossp";"
|
||||
id = sqlalchemy.Column(
|
||||
UUID,
|
||||
default=sqlalchemy.text("uuid_generate_v4()"),
|
||||
primary_key=True,
|
||||
)
|
||||
|
||||
creation: sqlalchemy.Column = sqlalchemy.Column(
|
||||
sqlalchemy.DateTime(timezone=True),
|
||||
key="creation",
|
||||
name="creation",
|
||||
index=True,
|
||||
quote=True,
|
||||
unique=False,
|
||||
default=None,
|
||||
nullable=False,
|
||||
primary_key=False,
|
||||
autoincrement=False,
|
||||
server_default=sqlalchemy.func.now(),
|
||||
)
|
||||
|
||||
modification: sqlalchemy.Column = sqlalchemy.Column(
|
||||
sqlalchemy.DateTime(timezone=True),
|
||||
key="Modification",
|
||||
name="modification",
|
||||
index=True,
|
||||
quote=True,
|
||||
unique=False,
|
||||
default=None,
|
||||
nullable=True,
|
||||
onupdate=sqlalchemy.func.now(),
|
||||
primary_key=False,
|
||||
autoincrement=False
|
||||
)
|
105
Database/database.py
Normal file
105
Database/database.py
Normal file
@ -0,0 +1,105 @@
|
||||
import sqlalchemy.orm
|
||||
import sqlalchemy.ext.asyncio
|
||||
|
||||
from .base import Base
|
||||
|
||||
|
||||
class Database:
|
||||
"""
|
||||
A class that is a composition of several other classes useful for accessing and manipulating a Postgresql database
|
||||
asynchronously.
|
||||
|
||||
Usage:
|
||||
Database must first be initialized with a given postgresql connection url like so:
|
||||
>>> db = Database("postgresql://localhost:5432/postgres")
|
||||
After Database has been initialized two important objects that are composed into Database become available:
|
||||
-> async_engine
|
||||
- Relevant for handling the creation and deletion of many tables at once, for example:
|
||||
>>> async with self.async_engine.begin() as conn:
|
||||
>>> await conn.run_sync(self.Base.metadata.drop_all)
|
||||
>>> await conn.run_sync(self.Base.metadata.create_all)
|
||||
- See the sqlalchemy documentation for futher details at
|
||||
https://docs.sqlalchemy.org/en/14/orm/extensions/asyncio.html#sqlalchemy.ext.asyncio.AsyncEngine
|
||||
-> async_session
|
||||
- Relevant for properly manipulating rows and columns within the database, for example:
|
||||
>>> from sqlalchemy import orm as sqlalchemy_orm
|
||||
>>> from sqlalchemy import future as sqlalchemy_future
|
||||
>>>
|
||||
>>> async with db.async_session() as session:
|
||||
>>> async with session.begin():
|
||||
>>> session.add_all(
|
||||
>>> [
|
||||
>>> A(bs=[B(), B()], data="a1"),
|
||||
>>> A(bs=[B()], data="a2"),
|
||||
>>> A(bs=[B(), B()], data="a3"),
|
||||
>>> ]
|
||||
>>> )
|
||||
>>>
|
||||
>>> stmt = sqlalchemy_future.select(A).options(sqlalchemy_orm.selectinload(A.bs))
|
||||
>>>
|
||||
>>> result = await session.execute(stmt)
|
||||
>>> await session.commit()
|
||||
- See the sqlalchemy documentation for further details at
|
||||
https://docs.sqlalchemy.org/en/14/orm/extensions/asyncio.html#sqlalchemy.ext.asyncio.AsyncConnection
|
||||
"""
|
||||
|
||||
Base = Base
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
db_url: str,
|
||||
async_session_expire_on_commit: bool = False,
|
||||
**engine_kwargs
|
||||
):
|
||||
"""
|
||||
Constructor for the class
|
||||
|
||||
Args:
|
||||
db_url: A string defining a standard postgresql url, for instance: postgresql://localhost:5432/postgres
|
||||
async_session_expire_on_commit: A boolean for determining if the given connection via a async
|
||||
context manager should close after the session.commit() function is called.
|
||||
**engine_kwargs: Arguments that can be passed to sqlalchemy's create_async_engine function
|
||||
"""
|
||||
self.connection_url = db_url
|
||||
self.async_engine = sqlalchemy.ext.asyncio.create_async_engine(
|
||||
self.connection_url,
|
||||
**engine_kwargs
|
||||
)
|
||||
self.async_session = sqlalchemy.orm.sessionmaker(
|
||||
self.async_engine,
|
||||
expire_on_commit=async_session_expire_on_commit,
|
||||
class_=sqlalchemy.ext.asyncio.AsyncSession
|
||||
)
|
||||
|
||||
@property
|
||||
def connection_url(self) -> str:
|
||||
"""
|
||||
Getter for self.connection_url
|
||||
|
||||
Returns:
|
||||
The postgresql connection string
|
||||
"""
|
||||
return self._connection_url
|
||||
|
||||
@connection_url.setter
|
||||
def connection_url(self, url: str):
|
||||
"""
|
||||
Converts a given typical postgresql string to our asynchronous driver used with sqlalchemy
|
||||
|
||||
Args:
|
||||
url: The given normal postgresql URL
|
||||
|
||||
Returns:
|
||||
Nothing, setter for self.connection_url in the constructor
|
||||
"""
|
||||
self._connection_url = f"postgresql+asyncpg://{url.split('://')[-1]}"
|
||||
|
||||
async def drop_all(self):
|
||||
"""Drops all information from the connected database for the given Base class"""
|
||||
async with self.async_engine.begin() as conn:
|
||||
await conn.run_sync(self.Base.metadata.drop_all)
|
||||
|
||||
async def create_all(self):
|
||||
"""Creates all tables for the given Base class"""
|
||||
async with self.async_engine.begin() as conn:
|
||||
await conn.run_sync(self.Base.metadata.create_all)
|
12
setup.py
Normal file
12
setup.py
Normal file
@ -0,0 +1,12 @@
|
||||
from setuptools import setup
|
||||
|
||||
setup(
|
||||
name='Async-Postgresql-Wrapper',
|
||||
version='0.8.0',
|
||||
packages=['Database'],
|
||||
url='gitlab.orion-technologies.io/Open-Source/Async-Postgresql-Wrapper',
|
||||
license='MIT',
|
||||
author='pricehiller',
|
||||
author_email='philler3138@gmail.com',
|
||||
description='A simple sqlalchemly database wrapper to simplify asynchronous connections to Postgresql databases with an orm.'
|
||||
)
|
Loading…
Reference in New Issue
Block a user