Initial commit

This commit is contained in:
Price Hiller 2021-10-05 05:14:45 -05:00
commit ecc17b788a
6 changed files with 280 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
.idea/
.venv/
venv/
*__pycache__*

11
Database/__init__.py Normal file
View 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
View 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
View 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
View 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
View 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.'
)