flesh out general Collector/Mapper interaction

This commit is contained in:
Sam G. 2024-04-12 03:16:03 -07:00
parent 7baa746006
commit 8e5bbac46e
12 changed files with 501 additions and 150 deletions

View File

@ -30,6 +30,7 @@ co3/managers/__init__.py
co3/managers/fts.py
co3/managers/sql.py
co3/managers/vss.py
co3/mappers/__init__.py
co3/schemas/__init__.py
co3/util/__init__.py
co3/util/db.py

View File

@ -9,6 +9,7 @@ managing hierarchical document relations, format conversions, and syntactical co
import inspect
import logging
from collections import defaultdict
from functools import wraps, partial
#from localsys.db.schema import tables
@ -40,6 +41,17 @@ def collate(action_key, action_groups=None):
class FormatRegistryMeta(type):
def __new__(cls, name, bases, attrs):
action_registry = {}
group_registry = defaultdict(list)
def register_action(method):
nonlocal action_registry, group_registry
if hasattr(method, '_action_data'):
action_key, action_groups = method._action_data
action_registry[action_key] = (method, action_groups)
for action_group in action_groups:
group_registry[action_group].append(action_key)
# add registered superclass methods; iterate over bases (usually just one), then
# that base's chain down (reversed), then methods from each subclass
@ -47,18 +59,15 @@ class FormatRegistryMeta(type):
for _class in reversed(base.mro()):
methods = inspect.getmembers(_class, predicate=inspect.isfunction)
for _, method in methods:
if hasattr(method, '_action_data'):
action_key, action_groups = method._action_data
action_registry[action_key] = (method, action_groups)
register_action(method)
# add final registered formats for the current class, overwriting any found in
# superclass chain
for attr_name, attr_value in attrs.items():
if hasattr(attr_value, '_action_data'):
action_key, action_groups = attr_value._action_data
action_registry[action_key] = (method, action_groups)
register_action(attr_value)
attrs['action_map'] = action_registry
attrs['action_registry'] = action_registry
attrs['group_registry'] = group_registry
return super().__new__(cls, name, bases, attrs)
@ -98,11 +107,25 @@ class CO3(metaclass=FormatRegistryMeta):
'''
return []
def collation_attributes(self, action_key, action_group):
'''
Return "connective" collation component data, possibly dependent on
instance-specific attributes and the action arguments. This is typically the
auxiliary structure that may be needed to attach to responses from registered
`collate` calls to complete inserts.
Note: this method is primarily used by `Mapper.collect()`, and is called just
prior to collector send-off for collation inserts and injected alongside collation
data. Common structure in collation components can make this function easy to
define, independent of action group for instance.
'''
return {}
def collate(self, action_key, *action_args, **action_kwargs):
if action_key not in self.action_map:
if action_key not in self.action_registry:
logger.debug(f'Collation for {action_key} not supported')
return None
else:
return self.action_map[action_key](self)
return self.action_registry[action_key][0](self)

View File

@ -29,32 +29,42 @@ from uuid import uuid4
import sqlalchemy as sa
from co3 import util
from co3.schema import Schema
from co3.component import Component
#from localsys.db.schema import tables
logger = logging.getLogger(__name__)
class Collector[C: Component, M: 'Mapper[C]']:
def __init__(self):
def __init__(self, schema: Schema[C]):
self.schema = schema
self._inserts = defaultdict(lambda: defaultdict(list))
@property
def inserts(self):
return self._inserts_from_receipts()
def _inserts_from_receipts(self, receipts=None, pop=False):
def _inserts_from_receipts(self, receipts: list=None, pop=False):
'''
Group up added inserts by Component, often to be used directly for bulk insertion.
Optionally provide a list of `receipts` to group up only the corresponding subset of
inserts, and `pop` to remove encountered receipts from the internal store.
'''
inserts = defaultdict(list)
if receipts is None:
receipts = list(self._inserts.keys())
for receipt in receipts:
if pop: insert_dict = self._inserts.pop(receipt, {})
else: insert_dict = self._inserts[receipt]
if pop:
receipt_tuple = self._inserts.pop(receipt, None)
else:
receipt_tuple = self._inserts.get(receipt, None)
for table, insert_list in insert_dict.items():
inserts[table].extend(insert_list)
if receipt_tuple is not None:
component, insert_data = receipt_tuple
inserts[component].append(insert_data)
return dict(inserts)
@ -62,24 +72,33 @@ class Collector[C: Component, M: 'Mapper[C]']:
self._inserts = defaultdict(lambda: defaultdict(list))
def _generate_unique_receipt(self):
return str(uuid4())
receipt = str(uuid4())
while receipt in self._inserts:
receipt = str(uuid4())
def add_insert(self, table_name, insert_dict, receipts=None):
return receipt
def add_insert(
self,
component : C,
insert_data : dict,
receipts : list | None = None,
):
'''
TODO: formalize table_name mapping; at class level provide a `table_map`, or provide
the table object itself to this method
Parameters:
component: Component from registered schema
insert_data: dict with (possibly raw/incomplete) insert data
receipts: optional list to which generated receipt should be appended.
Accommodates the common receipt list aggregation pattern.
'''
if table_name not in tables.table_map:
if component not in self.schema:
#logger.debug(f'Inserts provided for non-existent table {table_name}')
return None
receipt = self._generate_unique_receipt()
self._inserts[receipt][table_name].append(
utils.db.prepare_insert(
tables.table_map[table_name],
insert_dict
)
self._inserts[receipt] = (
component,
component.prepare_insert_data(insert_data),
)
if receipts is not None:

View File

@ -14,6 +14,12 @@ class Component[T]:
self.schema = schema
schema.add_component(self)
def __str__(self):
return f'<Component ({self.__class__.__name__})> {self.name}'
def __repr__(self):
return f'<Component ({self.__class__.__name__})> {self.name}'
def get_attributes(self):
raise NotImplementedError

View File

@ -3,7 +3,7 @@ from abc import ABCMeta, abstractmethod
import sqlalchemy as sa
from co3.util.types import TableLike
from co3.util.types import SQLTableLike
from co3.component import Component
@ -54,14 +54,46 @@ class Relation[T](ComposableComponent[T]):
):
return self
class SQLTable(Relation[TableLike]):
class SQLTable(Relation[SQLTableLike]):
@classmethod
def from_table(cls, table: sa.Table, schema: 'SQLSchema'):
return cls(table.name, table, schema)
def get_attributes(self):
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:
table: SQLAlchemy table
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
# key-value stores
class Dictionary(Relation[dict]):

View File

@ -11,12 +11,23 @@ mapper = Mapper[sa.Table]()
mapper.attach(
Type,
attributes=TypeTable,
collation=CollateTable,
collation_groups={
attr_comp=TypeTable,
coll_comp=CollateTable,
coll_groups={
'name': NameConversions
}
)
Development log:
- Overruled design decision: Mappers were previously designed to map from a specific
CO3 hierarchy to a specific Schema. The intention was to allow only related types to
be attached to a single schema, at least under a particular Mapper. The type
restriction has since been removed, however, as it isn't particularly well-founded.
During `collect()`, a particular instance collects data from both its attributes and
its collation actions. It then repeats the same upward for parent types (part of the
same type hierarchy), and down to components (often not part of the same type
hierarchy). As such, to fully collect from a type, the Mapper needs to leave
registration open to various types, not just those part of the same hierarchy.
'''
from typing import Callable, Self
from collections import defaultdict
@ -32,15 +43,34 @@ class Mapper[C: Component]:
'''
Mapper base class for housing schema components and managing relationships between CO3
types and storage components (of type C).
Mappers are responsible for two primary tasks:
1. Attaching CO3 types to database Components from within a single schema
2. Facilitating collection of Component-related insertion data from instances of
attached CO3 types
Additionally, the Mapper manages its own Collector and Composer instances. The
Collector receives the inserts from `.collect()` calls, and will subsequently be
"dropped off" at an appropriate Database's Manager to actually perform the requested
inserts (hence why we tie Mappers to Schemas one-to-one).
Dev note: the Composer needs reconsideration, or at least its positioning directly in
this class. It may be more appropriate to have at the Schema level, or even just
dissolved altogether if arbitrary named Components can be attached to schemas.
'''
_collector_cls: type[Collector[C, Self]] = Collector[C, Self]
_composer_cls: type[Composer[C, Self]] = Composer[C, Self]
def __init__(self, co3_root: type[CO3], schema: Schema):
self.co3_root = co3_root
self.schema = schema
def __init__(self, schema: Schema[C]):
'''
Parameters:
schema: Schema object holding the set of components eligible as attachment
targets for registered CO3 types
'''
self.schema = schema
self.collector = self._collector_cls()
self.collector = self._collector_cls(schema)
self.composer = self._composer_cls()
self.attribute_comps: dict[type[CO3], C] = {}
@ -78,12 +108,6 @@ class Mapper[C: Component]:
coll_groups: storage components for named collation groups; dict mapping group
names to components
'''
# check for type compatibility with CO3 root
if not issubclass(type_ref, self.co3_root):
raise TypeError(
f'Type ref {type_ref} not a subclass of Mapper CO3 root {self.co3_root}'
)
# check attribute component in registered schema
attr_comp = self._check_component(attr_comp)
self.attribute_comps[type_ref] = attr_comp
@ -100,35 +124,54 @@ class Mapper[C: Component]:
self.collation_groups[type_ref].update(coll_groups)
def attach_hierarchy(
def attach_many(
self,
type_ref: type[CO3],
obj_name_map: Callable[[type[CO3]], str],
type_list: list[type[CO3]],
attr_name_map: Callable[[type[CO3]], str],
coll_name_map: Callable[[type[CO3], str|None], str] | None = None,
):
pass
'''
Auto-register a set of types to the Mapper's attached Schema. Associations are
made from types to both attribute and collation component names, through
`attr_name_map` and `coll_name_map`, respectively. Collation targets are inferred
through the registered groups in each type.
def get_connective_data(
Parameters:
type_ref: reference to CO3 type
attr_name_map: function mapping from types/classes to attribute component names
in the attached Mapper Schema
coll_name_map: function mapping from types/classes & action groups to
collation component names in the attached Mapper Schema. `None`
is passed as the action group to retrieve the default
collection target.
'''
for _type in type_list:
attr_comp = attr_name_map(_type)
coll_groups = {}
for action_group in _type.group_registry:
coll_groups[action_group] = coll_name_map(_type, action_group)
self.attach(_type, attr_comp, coll_groups=coll_groups)
def get_attribute_comp(
self,
type_instance: CO3,
action_key,
action_group=None,
) -> dict:
'''
Return data relevant for connecting collation entries to primary storage
containers. This is typically some combination of the action key and a unique
identifier from the target CO3 instance to form a unique key for inserts from
actions. This is called just prior to collector send-off for collation inserts and
injected alongside collation data.
'''
return {}
def get_attribute_comp(self, type_ref: CO3) -> C | None:
type_ref: type[CO3]
) -> C | None:
return self.attribute_comps.get(type_ref, None)
def get_collation_comp(self, type_ref: CO3, group=str | None) -> C | None:
return self.collation_group.get(type_ref, {}).get(group, None)
def get_collation_comp(
self,
type_ref: type[CO3],
group=str | None
) -> C | None:
return self.collation_groups.get(type_ref, {}).get(group, None)
def collect(self, obj, action_keys=None) -> dict:
def collect(
self,
obj: CO3,
action_keys: list[str]=None,
action_groups: list[str]=None,
) -> list:
'''
Stages inserts up the inheritance chain, and down through components.
@ -166,7 +209,7 @@ class Mapper[C: Component]:
if collation_data is None:
continue
_, action_groups = obj.action_map[action_key]
_, action_groups = obj.action_registry[action_key]
for action_group in action_groups:
collation_component = self.get_collation_comp(_cls, group=action_group)
@ -174,9 +217,9 @@ class Mapper[C: Component]:
continue
# gather connective data for collation components
connective_data = self.get_connective_data(_cls, action_key, action_group)
connective_data = obj.collation_attributes(action_key, action_group)
collector.add_insert(
self.collector.add_insert(
collation_component,
{
**connective_data,
@ -186,7 +229,7 @@ class Mapper[C: Component]:
)
# handle components
for comp in self.components:
for comp in [c for c in obj.components if isinstance(c, CO3)]:
receipts.extend(comp.collect(collector, formats=formats))
return receipts

View File

@ -3,14 +3,19 @@ Schema
Collection of related storage components, often representing the data structure of an
entire database. Some databases support multiple schemas, however. In general, a Schema
can wrap up a relevant subset of tables within a single database, so long as
`Manager.recreate()` supports creating components in separate calls.
can wrap up an associated subset of components within a single database, so long as
`Manager.recreate()` supports creating components in separate calls (even if the
associated database doesn't explicitly support multiple schemas).
Schema objects are used to:
- Semantically group related storage components
- Tell databases what components to create/remove together
- Provide target contexts for connected CO3 type systems within Mappers
with particular emphasis on the latter. Mappers associate exactly one CO3 type hierarchy
with exactly one Schema. This is an intentional point of simplification in the CO3
operational model.
'''
from co3.component import Component

View File

@ -125,18 +125,6 @@ def sa_exec_explicit(engine, stmt, bind_params=None):
trans.rollback() # rollback the transaction explicitly
raise
def prepare_insert(table, value_dict):
'''
Modifies insert dictionary with full table column defaults
'''
insert_dict = get_column_defaults(table)
#insert_dict.update(value_dict)
insert_dict.update(
{ k:v for k,v in value_dict.items() if k in insert_dict }
)
return insert_dict
def deferred_fkey(target, **kwargs):
return sa.ForeignKey(
target,
@ -152,27 +140,6 @@ def deferred_cd_fkey(target, **kwargs):
'''
return deferred_fkey(target, ondelete='CASCADE', **kwargs)
def get_column_defaults(table, include_all=True):
'''
Provide column:default pairs for a provided SQLAlchemy table.
Parameters:
table: SQLAlchemy table
include_all: whether to include all columns, even those without explicit defaults
'''
default_values = {}
for column in table.columns:
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 get_column_names_str_table(engine, table: str):
col_sql = f'PRAGMA table_info({table});'

View File

@ -3,4 +3,4 @@ from typing import TypeVar
import sqlalchemy as sa
TableLike = TypeVar('TableLike', bound=sa.Table | sa.Subquery | sa.Join)
SQLTableLike = TypeVar('SQLTableLike', bound=sa.Table | sa.Subquery | sa.Join)

View File

@ -1,9 +1,12 @@
'''
just remembered tomatos aren't vegetables. whoops
'''
import random
import sqlalchemy as sa
from co3.schemas import SQLSchema
from co3 import CO3, collate
from co3 import CO3, collate, Mapper
from co3 import util
@ -20,6 +23,12 @@ class Tomato(Vegetable):
@property
def attributes(self):
return vars(self)
def collation_attributes(self, action_key, action_grounp):
return {
'name': self.name,
'state': action_key,
}
@collate('ripe', action_groups=['aging'])
def ripen(self):
@ -39,42 +48,58 @@ class Tomato(Vegetable):
'pieces': random.randint(2, 12)
}
type_list = [Vegetable, Tomato]
'''
VEGETABLE
|
TOMATO -- AGING
|
-- COOKING
Note: foreign keys need to represent values that could be known by objects _without_ first interacting
with a DB. This is slightly non-standard, given how common it is to depend on another table's integer ID
(typically a value assigned by the DB using an autoincrement, for example, and not specified explicitly
within the insertion body). As a result, SQLTable components need to be able to operate by another unique
key when expected to connect to other tables in the hierarchy. Below we use `name` with a UNIQUE constraint
for this purpose. Note that having an integer `id` is still perfectly okay so that a table can manage
uniqueness of its own rows by default.
'''
metadata = sa.MetaData()
vegetable_table = sa.Table(
'vegetable',
metadata,
sa.Column('id', sa.Integer, primary_key=True),
sa.Column('name', sa.String),
sa.Column('name', sa.String, unique=True),
sa.Column('color', sa.String),
)
tomato_table = sa.Table(
'tomato',
metadata,
sa.Column('id', sa.Integer, primary_key=True),
sa.Column('vegetable_id', sa.Integer, util.db.deferred_cd_fkey('vegetables.id')),
sa.Column('name', sa.String, util.db.deferred_cd_fkey('vegetable.name'), unique=True),
sa.Column('radius', sa.Integer),
)
tomato_aging_table = sa.Table(
'tomato_aging_states',
metadata,
sa.Column('id', sa.Integer, primary_key=True),
sa.Column('vegetable_id', sa.Integer, util.db.deferred_cd_fkey('vegetables.id')),
sa.Column('name', sa.String, util.db.deferred_cd_fkey('tomato.name'), unique=True),
sa.Column('state', sa.String),
sa.Column('age', sa.Integer),
sa.Column('age', sa.Integer),
)
tomato_cooking_table = sa.Table(
'tomato_cooking_states',
metadata,
sa.Column('id', sa.Integer, primary_key=True),
sa.Column('vegetable_id', sa.Integer, util.db.deferred_cd_fkey('vegetables.id')),
sa.Column('state', sa.String),
sa.Column('name', sa.String, util.db.deferred_cd_fkey('tomato.name'), unique=True),
sa.Column('state', sa.String),
sa.Column('pieces', sa.Integer),
)
vegetable_schema = SQLSchema.from_metadata(metadata)
vegetable_schema = SQLSchema.from_metadata(metadata)
vegetable_mapper = Mapper(vegetable_schema)

View File

@ -16,8 +16,6 @@
}
],
"source": [
"from co3 import Mapper\n",
"\n",
"import vegetables"
]
},
@ -31,13 +29,38 @@
"- Need connective function (type to collation) and attribute map. Do we need to this with a subclass? If a func is passed in on init, I can type it appropriately I guess `Callable[[type[CO3],str,str|None],dict]`"
]
},
{
"cell_type": "markdown",
"id": "ef733715-bb75-4263-b216-45e778a06b21",
"metadata": {},
"source": [
"## Usage\n",
"The Mapper's primary job is to associate class hierarchies with database components. This can be done in a few ways:\n",
"\n",
"1. Manually attaching a type reference to a Component\n",
"2. Attaching a type reference to a Component's name as registered in a schema\n",
"3. Automatically register the CO3 heirarchy to matching schema component names (through transformation)"
]
},
{
"cell_type": "markdown",
"id": "d2672422-3596-4eab-ac44-5da617f74b80",
"metadata": {
"jp-MarkdownHeadingCollapsed": true
},
"source": [
"## Explicit example steps"
]
},
{
"cell_type": "code",
"execution_count": 3,
"execution_count": 2,
"id": "7d80f7b9-7458-4ad4-8c1a-3ea56e796b4e",
"metadata": {},
"outputs": [],
"source": [
"from co3 import Mapper\n",
"\n",
"vegetable_mapper = Mapper(\n",
" vegetables.Vegetable,\n",
" vegetables.vegetable_schema\n",
@ -46,21 +69,34 @@
},
{
"cell_type": "code",
"execution_count": null,
"execution_count": 3,
"id": "d24d31b4-c4a6-4a1e-8bea-c44378aadfdd",
"metadata": {},
"outputs": [],
"outputs": [
{
"data": {
"text/plain": [
"'\\nvegetable_mapper.attach(\\n vegetables.Vegetable,\\n vegetables.vegetable_table,\\n)\\n'"
]
},
"execution_count": 3,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# not valid; tables need to be wrapped in CO3 Components\n",
"'''\n",
"vegetable_mapper.attach(\n",
" vegetables.Vegetable,\n",
" vegetables.vegetable_table,\n",
")"
")\n",
"'''"
]
},
{
"cell_type": "code",
"execution_count": 6,
"execution_count": 4,
"id": "f9408562-bf50-4522-909c-318557f85948",
"metadata": {},
"outputs": [],
@ -78,7 +114,7 @@
},
{
"cell_type": "code",
"execution_count": 7,
"execution_count": 5,
"id": "05fdd404-87ee-4187-832f-2305272758ae",
"metadata": {},
"outputs": [],
@ -96,21 +132,43 @@
},
{
"cell_type": "code",
"execution_count": null,
"execution_count": 6,
"id": "e9b6af49-a69d-41cc-beae-1b6f171cd2f5",
"metadata": {},
"outputs": [],
"source": [
"# attach entire type hierarchy w/ type->name map\n",
"vegetable_mapper.attach_hierarchy(\n",
"# this might make more sense during init\n",
" lambda x:x.__name__.lower())\n",
" # this might make more sense during init\n",
" vegetables.Vegetable,\n",
" lambda x:x.__name__.lower()\n",
")"
]
},
{
"cell_type": "code",
"execution_count": 9,
"id": "0fb45a86-5c9b-41b1-a3ab-5691444f175e",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"<co3.components.SQLTable at 0x7f2012b23f80>"
]
},
"execution_count": 9,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"vegetable_mapper.get_collation_comp(vegetables.Tomato, 'cooking')"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "2e4336ab-5b5f-484d-815d-164d4b6f40a0",
"metadata": {},
"outputs": [
@ -118,16 +176,16 @@
"data": {
"text/plain": [
"{'co3_root': vegetables.Vegetable,\n",
" 'schema': <co3.schemas.SQLSchema at 0x74ac03f5c8c0>,\n",
" 'collector': <co3.collector.Collector at 0x74ac0357ae70>,\n",
" 'composer': <co3.composer.Composer at 0x74ac0357a4b0>,\n",
" 'attribute_comps': {vegetables.Tomato: <co3.components.SQLTable at 0x74ac09d4a720>},\n",
" 'schema': <co3.schemas.SQLSchema at 0x732074224aa0>,\n",
" 'collector': <co3.collector.Collector at 0x7320757da120>,\n",
" 'composer': <co3.composer.Composer at 0x7320757da9c0>,\n",
" 'attribute_comps': {vegetables.Tomato: <co3.components.SQLTable at 0x732074224cb0>},\n",
" 'collation_groups': defaultdict(dict,\n",
" {vegetables.Tomato: {'aging': <co3.components.SQLTable at 0x74ac03f5cad0>,\n",
" 'cooking': <co3.components.SQLTable at 0x74ac03f5cb00>}})}"
" {vegetables.Tomato: {'aging': <co3.components.SQLTable at 0x732074224ce0>,\n",
" 'cooking': <co3.components.SQLTable at 0x732074224d10>}})}"
]
},
"execution_count": 9,
"execution_count": 7,
"metadata": {},
"output_type": "execute_result"
}
@ -137,40 +195,187 @@
]
},
{
"cell_type": "code",
"execution_count": 10,
"id": "c16786d4-0b71-42d9-97f7-7893c542104e",
"cell_type": "markdown",
"id": "47859e25-b803-4459-a581-f10bbcfac716",
"metadata": {},
"outputs": [],
"source": [
"tomato = vegetables.Tomato('t1', 5)"
"## Holistic attachment"
]
},
{
"cell_type": "code",
"execution_count": 11,
"id": "884d6753-c763-4e71-824a-711436e203e1",
"execution_count": 2,
"id": "70c9baed-b870-4021-8949-9b713d863de6",
"metadata": {},
"outputs": [],
"source": [
"def attr_name_map(cls):\n",
" return f'{cls.__name__.lower()}'\n",
"\n",
"def coll_name_map(cls, action_group):\n",
" return f'{cls.__name__.lower()}_{action_group}_states'\n",
"\n",
"vegetables.vegetable_mapper.attach_many(\n",
" vegetables.type_list,\n",
" attr_name_map,\n",
" coll_name_map,\n",
")"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "c16786d4-0b71-42d9-97f7-7893c542104e",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"<vegetables.Tomato at 0x74ac082bacc0>"
"{'age': 4}"
]
},
"execution_count": 11,
"execution_count": 3,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"tomato"
"# create new CO3 descendant\n",
"tomato = vegetables.Tomato('t1', 5)\n",
"\n",
"# test a register collation action\n",
"tomato.collate('ripe')"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "d7fa94ca-3ecd-4ee3-b0dc-f3b2b65ee47c",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"<Component (SQLTable)> tomato"
]
},
"execution_count": 4,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"vegetables.vegetable_mapper.get_attribute_comp(vegetables.Tomato)"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "1adc3bc5-957f-4b5a-bc2c-2d172675826d",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'schema': <co3.schemas.SQLSchema at 0x7ab568224e60>,\n",
" 'collector': <co3.collector.Collector at 0x7ab568225190>,\n",
" 'composer': <co3.composer.Composer at 0x7ab5682251c0>,\n",
" 'attribute_comps': {vegetables.Vegetable: <Component (SQLTable)> vegetable,\n",
" vegetables.Tomato: <Component (SQLTable)> tomato},\n",
" 'collation_groups': defaultdict(dict,\n",
" {vegetables.Vegetable: {},\n",
" vegetables.Tomato: {'aging': <Component (SQLTable)> tomato_aging_states,\n",
" 'cooking': <Component (SQLTable)> tomato_cooking_states}})}"
]
},
"execution_count": 5,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"vars(vegetables.vegetable_mapper)"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "f32d1f65-9b1d-4600-b396-8551fbd1fcf7",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"['3bf42abc-8a12-452f-baf6-38a05fc5d420',\n",
" '271b7b84-846e-4d1d-87f6-bcabc90a7b55',\n",
" 'f9fc5d16-c5cb-47a7-9eca-7df8a3ba5d10']"
]
},
"execution_count": 6,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"vegetables.vegetable_mapper.collect(tomato, ['ripe'])"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "380dfbea-90cc-49fc-aef1-ebb342872632",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"defaultdict(<function co3.collector.Collector.__init__.<locals>.<lambda>()>,\n",
" {'3bf42abc-8a12-452f-baf6-38a05fc5d420': (<Component (SQLTable)> vegetable,\n",
" {'name': 't1', 'color': 'red'}),\n",
" '271b7b84-846e-4d1d-87f6-bcabc90a7b55': (<Component (SQLTable)> tomato,\n",
" {'name': 't1', 'radius': 5}),\n",
" 'f9fc5d16-c5cb-47a7-9eca-7df8a3ba5d10': (<Component (SQLTable)> tomato_aging_states,\n",
" {'name': 't1', 'state': 'ripe', 'age': 1})})"
]
},
"execution_count": 7,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"vegetables.vegetable_mapper.collector._inserts"
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "905bb2a9-9c22-4187-be15-3dd32d206e26",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{<Component (SQLTable)> vegetable: [{'name': 't1', 'color': 'red'}],\n",
" <Component (SQLTable)> tomato: [{'name': 't1', 'radius': 5}],\n",
" <Component (SQLTable)> tomato_aging_states: [{'name': 't1',\n",
" 'state': 'ripe',\n",
" 'age': 1}]}"
]
},
"execution_count": 8,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"vegetables.vegetable_mapper.collector.inserts"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "137d0bf1-940d-448c-91e9-01e7fc4f31b4",
"id": "d166b9af-e3ba-4750-9dcb-d8d4e08fe4d3",
"metadata": {},
"outputs": [],
"source": []

View File

@ -1,9 +1,12 @@
'''
just remembered tomatos aren't vegetables. whoops
'''
import random
import sqlalchemy as sa
from co3.schemas import SQLSchema
from co3 import CO3, collate
from co3 import CO3, collate, Mapper
from co3 import util
@ -20,6 +23,12 @@ class Tomato(Vegetable):
@property
def attributes(self):
return vars(self)
def collation_attributes(self, action_key, action_grounp):
return {
'name': self.name,
'state': action_key,
}
@collate('ripe', action_groups=['aging'])
def ripen(self):
@ -39,42 +48,58 @@ class Tomato(Vegetable):
'pieces': random.randint(2, 12)
}
type_list = [Vegetable, Tomato]
'''
VEGETABLE
|
TOMATO -- AGING
|
-- COOKING
Note: foreign keys need to represent values that could be known by objects _without_ first interacting
with a DB. This is slightly non-standard, given how common it is to depend on another table's integer ID
(typically a value assigned by the DB using an autoincrement, for example, and not specified explicitly
within the insertion body). As a result, SQLTable components need to be able to operate by another unique
key when expected to connect to other tables in the hierarchy. Below we use `name` with a UNIQUE constraint
for this purpose. Note that having an integer `id` is still perfectly okay so that a table can manage
uniqueness of its own rows by default.
'''
metadata = sa.MetaData()
vegetable_table = sa.Table(
'vegetable',
metadata,
sa.Column('id', sa.Integer, primary_key=True),
sa.Column('name', sa.String),
sa.Column('name', sa.String, unique=True),
sa.Column('color', sa.String),
)
tomato_table = sa.Table(
'tomato',
metadata,
sa.Column('id', sa.Integer, primary_key=True),
sa.Column('vegetable_id', sa.Integer, util.db.deferred_cd_fkey('vegetables.id')),
sa.Column('name', sa.String, util.db.deferred_cd_fkey('vegetable.name'), unique=True),
sa.Column('radius', sa.Integer),
)
tomato_aging_table = sa.Table(
'tomato_aging_states',
metadata,
sa.Column('id', sa.Integer, primary_key=True),
sa.Column('vegetable_id', sa.Integer, util.db.deferred_cd_fkey('vegetables.id')),
sa.Column('name', sa.String, util.db.deferred_cd_fkey('tomato.name'), unique=True),
sa.Column('state', sa.String),
sa.Column('age', sa.Integer),
sa.Column('age', sa.Integer),
)
tomato_cooking_table = sa.Table(
'tomato_cooking_states',
metadata,
sa.Column('id', sa.Integer, primary_key=True),
sa.Column('vegetable_id', sa.Integer, util.db.deferred_cd_fkey('vegetables.id')),
sa.Column('state', sa.String),
sa.Column('name', sa.String, util.db.deferred_cd_fkey('tomato.name'), unique=True),
sa.Column('state', sa.String),
sa.Column('pieces', sa.Integer),
)
vegetable_schema = SQLSchema.from_metadata(metadata)
vegetable_schema = SQLSchema.from_metadata(metadata)
vegetable_mapper = Mapper(vegetable_schema)