Skip to content

Commit

Permalink
Add package files, basic SQL objects.
Browse files Browse the repository at this point in the history
Table is in an incomplete state, will need to complete later.
  • Loading branch information
scragly committed Mar 25, 2021
1 parent daf1659 commit 0d1211e
Show file tree
Hide file tree
Showing 15 changed files with 1,345 additions and 0 deletions.
20 changes: 20 additions & 0 deletions .flake8
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
3 changes: 3 additions & 0 deletions everstone/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .database import Database

db = Database("__default__")
14 changes: 14 additions & 0 deletions everstone/bases.py
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
114 changes: 114 additions & 0 deletions everstone/database.py
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)
10 changes: 10 additions & 0 deletions everstone/exceptions.py
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 added everstone/sql/__init__.py
Empty file.
109 changes: 109 additions & 0 deletions everstone/sql/aggregates.py
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"
64 changes: 64 additions & 0 deletions everstone/sql/column.py
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)
Loading

0 comments on commit 0d1211e

Please sign in to comment.