co3/co3/components/__init__.py

128 lines
4.0 KiB
Python

'''
Dev note:
Any reason to have ComposeableComponents and Relations as separate types? The thought
is that there may be some possible Component types we want to be able to Compose that
wouldn't logically be Relations. But the gap here might be quite small
'''
from typing import Self
from abc import ABCMeta, abstractmethod
import sqlalchemy as sa
from co3.util.types import SQLTableLike
from co3.component import Component
class ComposableComponent[T](Component[T], metaclass=ABCMeta):
'''
Components that can be composed with others of the same type.
'''
@abstractmethod
def compose(self, component: Self, on, outer=False) -> Self:
'''
Abstract composition.
'''
raise NotImplementedError
# relational databases
class Relation[T](ComposableComponent[T]):
'''
Relation base for tabular components to be used in relation DB settings. Attempts to
adhere to the set-theoretic base outlined in the relational model [1]. Some
terminology:
Relation: table-like container
| -> Heading: set of attributes
| | -> Attribute: column name
| -> Body: set of tuples with domain matching the heading
| | -> Tuple: collection of values
[1]: https://en.wikipedia.org/wiki/Relational_model#Set-theoretic_formulation
Note: development tasks
As it stands, the Relation skeleton is incredibly lax compared to the properties and
operations that should be formally available, according its pure relational algebra
analog.
Relations are also generic up to a type T, which ultimately serves as the base object
for Relation instances. We aren't attempting to implement some generally useful
table-like class here; instead we're just exposing a lightweight interface that's
needed for a few CO3 contexts, and commonly off-loading most of the heavy-lifting to
true relation objects like SQLAlchemy tables.
'''
def compose(
self,
_with: Self,
on,
outer=False
):
return self
class SQLTable(Relation[SQLTableLike]):
@classmethod
def from_table(cls, table: sa.Table):
'''
Note that the sa.Table type is intentional here; not all matching types for
SQLTableLike have a defined ``name`` property
'''
return cls(table.name, table)
def get_attributes(self) -> tuple:
return tuple(self.obj.columns)
def get_column_defaults(self, include_all=True):
'''
Provide column:default pairs for a provided SQLAlchemy table.
Parameters:
include_all: whether to include all columns, even those without explicit defaults
'''
default_values = {}
for column in self.get_attributes():
if column.default is not None:
default_values[column.name] = column.default.arg
elif column.nullable:
default_values[column.name] = None
else:
# assume empty string if include_all and col has no explicit default
# and isn't nullable
if include_all and column.name != 'id':
default_values[column.name] = ''
return default_values
def prepare_insert_data(self, insert_data: dict) -> dict:
'''
Modifies insert dictionary with full table column defaults
'''
insert_dict = self.get_column_defaults()
insert_dict.update(
{ k:v for k,v in insert_data.items() if k in insert_dict }
)
return insert_dict
def compose(self, _with: Self, on, outer=False):
return self.__class__(
f'{self.name}+{_with.name}',
self.obj.join(_with.obj, on, isouter=outer)
)
# key-value stores
class Dictionary(Relation[dict]):
def get_attributes(self):
return tuple(self.obj.keys())
# document databases
class Document[T](Component[T]):
pass
# graph databases
class Node[T](Component[T]):
pass