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