-
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add package files, basic SQL objects.
Table is in an incomplete state, will need to complete later.
- Loading branch information
Showing
15 changed files
with
1,345 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
[flake8] | ||
max-line-length=120 | ||
docstring-convention=all | ||
import-order-style=pycharm | ||
application_import_names=everstone | ||
exclude=.cache,.venv,.git,.idea,unfinished | ||
suppress-none-returning=true | ||
ignore= | ||
B311,W503,E226,S311,T000 | ||
# Missing Docstrings | ||
D100,D104,D105,D107, | ||
# Docstring Whitespace | ||
D203,D212,D214,D215, | ||
# Docstring Quotes | ||
D301,D302, | ||
# Docstring Content | ||
D400,D401,D402,D404,D405,D406,D407,D408,D409,D410,D411,D412,D413,D414,D416,D417 | ||
# Type Annotations | ||
ANN002,ANN003,ANN101,ANN102,ANN204,ANN206 | ||
per-file-ignores=everstone/__init__.py:F401 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
from .database import Database | ||
|
||
db = Database("__default__") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import typing as t | ||
|
||
|
||
class LimitInstances: | ||
"""Ensures only a single instance exists per name, returning old ones if existing.""" | ||
|
||
__instances__: t.Dict[str, object] = dict() | ||
|
||
def __new__(cls, name: str, *args, **kwds): | ||
"""Set the class to a single instance per name.""" | ||
instance = cls.__instances__.get(name) | ||
if instance is None: | ||
cls.__instances__[name] = instance = object.__new__(cls) | ||
return instance |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
from __future__ import annotations | ||
|
||
import json | ||
import logging | ||
import typing as t | ||
|
||
import asyncpg | ||
import sqlparse | ||
|
||
from .bases import LimitInstances | ||
from .sql import types | ||
from .sql.schema import Schema | ||
from .sql.table import Table | ||
|
||
log = logging.getLogger(__name__) | ||
|
||
|
||
class Database(LimitInstances): | ||
"""Represents a database.""" | ||
|
||
def __init__(self, name: str): | ||
self.name = name | ||
self.user: t.Optional[str] = None | ||
self.url: t.Optional[str] = None | ||
|
||
self.pool: t.Optional[asyncpg.Pool] = None | ||
|
||
self.type = types | ||
self.schemas: t.Set[Schema] = set() | ||
|
||
self._mock = False | ||
self._prepared = False | ||
|
||
@classmethod | ||
def connect(cls, name: str, user: str, password: str, *, host: str = "localhost", port: int = 5432) -> Database: | ||
"""Establish the connection URL and name for the database, returning the instance representing it.""" | ||
if len(cls.__instances__) == 1: | ||
db = cls.__instances__["__default__"] | ||
cls.__instances__[name] = db | ||
db.name = name | ||
db.user = user | ||
db.url = f"postgres://{user}:{password}@{host}:{port}/{name}" | ||
# noinspection PyTypeChecker | ||
return db | ||
|
||
return Database(name) | ||
|
||
def __call__(self, name: str) -> Database: | ||
"""Return the instance representing the given database name.""" | ||
return Database(name) | ||
|
||
def __str__(self): | ||
"""Return the URL representation of the given database instance, if set.""" | ||
return self.url or self.name | ||
|
||
def __repr__(self): | ||
if self.user: | ||
return f"<Database name='{self.name}' user='{self.user}'>" | ||
else: | ||
return f"<Database '{self.name}'>" | ||
|
||
def __hash__(self): | ||
return hash(str(self)) | ||
|
||
def __eq__(self, other: t.Any): | ||
if isinstance(other, Database): | ||
return str(self) == str(self) | ||
return False | ||
|
||
async def create_pool(self): | ||
"""Create the asyncpg connection pool for this database connection to use.""" | ||
if self.pool: | ||
self.pool.close() | ||
self.pool = await asyncpg.create_pool(self.url, init=self._enable_json) | ||
|
||
@staticmethod | ||
async def _enable_json(conn: asyncpg.Connection): | ||
await conn.set_type_codec("jsonb", encoder=json.dumps, decoder=json.loads, schema="pg_catalog") | ||
await conn.set_type_codec("json", encoder=json.dumps, decoder=json.loads, schema="pg_catalog") | ||
|
||
async def prepare(self): | ||
"""Prepare all child objects for this database.""" | ||
for schema in self.schemas: | ||
await schema.prepare() | ||
self._prepared = True | ||
|
||
def disable_execution(self): | ||
"""Don't execute SQL statements, divert them to console output instead.""" | ||
self._mock = True | ||
|
||
def enable_execution(self): | ||
"""Restore normal SQL execution status instead of diverting statements to console output.""" | ||
self._mock = False | ||
|
||
async def close(self): | ||
"""Close the asyncpg connection pool for this database.""" | ||
if self.pool: | ||
await self.pool.close() | ||
|
||
async def execute(self, sql: str, *args, timeout: t.Optional[float] = None) -> str: | ||
"""Execute an SQL statement.""" | ||
if self._mock: | ||
pretty_sql = sqlparse.parse(sql) | ||
print(pretty_sql, *args) | ||
return pretty_sql | ||
return await self.pool.execute(sql, *args, timeout=timeout) | ||
|
||
def Schema(self, name: str) -> Schema: | ||
"""Return a bound Schema for this database.""" | ||
return Schema(name, self) | ||
|
||
def Table(self, name: str) -> Table: | ||
"""Return a bound Table for the public schema on this database.""" | ||
return Table(name, self) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
class DBError(Exception): | ||
"""Base exception for database errors.""" | ||
|
||
|
||
class SchemaError(DBError): | ||
"""Exception for schema-specific errors.""" | ||
|
||
|
||
class ResponseError(DBError): | ||
"""Exception for database response errors.""" |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
from __future__ import annotations | ||
|
||
import typing as t | ||
|
||
from . import comparisons, table | ||
|
||
if t.TYPE_CHECKING: | ||
from .column import Column | ||
|
||
|
||
class Aggregate(comparisons.Comparable): | ||
"""Represents an aggregate SQL function.""" | ||
|
||
name: str | ||
|
||
def __init__(self, column: t.Optional[Column, str], alias: t.Optional[str] = None): | ||
self.column = column | ||
self.alias = alias | ||
|
||
@property | ||
def sql(self) -> str: | ||
"""Generates the SQL statement representing the aggregate function.""" | ||
if self.alias: | ||
return f"{self.name}({self.column}) AS {self.alias}" | ||
return f"{self.name}({self.column})" | ||
|
||
@property | ||
def distinct(self) -> Aggregate: | ||
"""Sets column values to be DISTINCT when used in the aggregate function.""" | ||
self.column = f"DISTINCT {self.column}" | ||
return self | ||
|
||
def as_(self, alias: str) -> Aggregate: | ||
"""Sets an alias name to represent the result of the aggregate function.""" | ||
self.alias = alias | ||
return self | ||
|
||
def __repr__(self) -> str: | ||
if self.alias: | ||
return f"<{self.__class__.__name__} alias={self.alias} column={self.column} sql='{self.sql}'>" | ||
return f"<{self.__class__.__name__} column={self.column} sql='{self.sql}'>" | ||
|
||
def __str__(self) -> str: | ||
return self.sql | ||
|
||
|
||
class Avg(Aggregate): | ||
"""Computes the average of all non-null input values.""" | ||
|
||
name = "avg" | ||
|
||
|
||
class BitAnd(Aggregate): | ||
"""Computes the bitwise AND of all non-null input values.""" | ||
|
||
name = "bit_and" | ||
|
||
|
||
class BitOr(Aggregate): | ||
"""Computes the bitwise OR of all non-null input values.""" | ||
|
||
name = "bit_or" | ||
|
||
|
||
class BoolAnd(Aggregate): | ||
"""Returns TRUE if all non-null input values are TRUE, otherwise FALSE.""" | ||
|
||
name = "bool_and" | ||
|
||
|
||
class BoolOr(Aggregate): | ||
"""Returns TRUE if any non-null input value is TRUE, otherwise FALSE.""" | ||
|
||
name = "bool_or" | ||
|
||
|
||
class Count(Aggregate): | ||
"""Computes the number of input rows, counting only non-nulls if a column is specified.""" | ||
|
||
name = "count" | ||
|
||
def __init__(self, value: t.Optional[Column, table.Table, str], alias: t.Optional[str] = None): | ||
if isinstance(value, table.Table): | ||
super().__init__(f"{value}.*" if value else "*", alias) | ||
else: | ||
super().__init__(f"{value}", alias) | ||
|
||
@classmethod | ||
def all(cls, alias: t.Optional = None) -> Count: | ||
"""Computes the number of all input rows in a table.""" | ||
return cls("*", alias) | ||
|
||
|
||
class Max(Aggregate): | ||
"""Computes the maximum of the non-null input values.""" | ||
|
||
name = "max" | ||
|
||
|
||
class Min(Aggregate): | ||
"""Computes the minimum of the non-null input values.""" | ||
|
||
name = "min" | ||
|
||
|
||
class Sum(Aggregate): | ||
"""Computes the sum of the non-null input values.""" | ||
|
||
name = "sum" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
from __future__ import annotations | ||
|
||
import typing as t | ||
|
||
from . import aggregates, comparisons | ||
|
||
if t.TYPE_CHECKING: | ||
from .constraints import Constraint | ||
from .types import SQLType | ||
from .table import Table | ||
|
||
|
||
class Column(comparisons.Comparable): | ||
"""Reprents an SQL column.""" | ||
|
||
def __init__(self, name: str, type: SQLType, constraint: t.Optional[Constraint] = None): | ||
self.name = name | ||
self.type = type | ||
self.constraint = constraint | ||
self.table: t.Optional[Table] = None | ||
|
||
@property | ||
def definition(self) -> str: | ||
"""Return the SQL definition for this column.""" | ||
sql = f"{self.name} {self.type}" | ||
if self.constraint: | ||
sql += f" {self.constraint}" | ||
return sql | ||
|
||
def bind_table(self, table: Table) -> Column: | ||
"""Binds a table to this column.""" | ||
self.table = table | ||
return self | ||
|
||
def __str__(self) -> str: | ||
if self.table: | ||
return f"{self.table}.{self.name}" | ||
else: | ||
return self.name | ||
|
||
@property | ||
def avg(self) -> aggregates.Avg: | ||
"""Return the avg aggreate for this column.""" | ||
return aggregates.Avg(self) | ||
|
||
@property | ||
def count(self) -> aggregates.Count: | ||
"""Return the count aggreate for this column.""" | ||
return aggregates.Count(self) | ||
|
||
@property | ||
def max(self) -> aggregates.Max: | ||
"""Return the max aggreate for this column.""" | ||
return aggregates.Max(self) | ||
|
||
@property | ||
def min(self) -> aggregates.Min: | ||
"""Return the min aggreate for this column.""" | ||
return aggregates.Min(self) | ||
|
||
@property | ||
def sum(self) -> aggregates.Sum: | ||
"""Return the sum aggreate for this column.""" | ||
return aggregates.Sum(self) |
Oops, something went wrong.