flesh out general Collector/Mapper interaction
This commit is contained in:
parent
7baa746006
commit
8e5bbac46e
@ -30,6 +30,7 @@ co3/managers/__init__.py
|
|||||||
co3/managers/fts.py
|
co3/managers/fts.py
|
||||||
co3/managers/sql.py
|
co3/managers/sql.py
|
||||||
co3/managers/vss.py
|
co3/managers/vss.py
|
||||||
|
co3/mappers/__init__.py
|
||||||
co3/schemas/__init__.py
|
co3/schemas/__init__.py
|
||||||
co3/util/__init__.py
|
co3/util/__init__.py
|
||||||
co3/util/db.py
|
co3/util/db.py
|
||||||
|
41
co3/co3.py
41
co3/co3.py
@ -9,6 +9,7 @@ managing hierarchical document relations, format conversions, and syntactical co
|
|||||||
|
|
||||||
import inspect
|
import inspect
|
||||||
import logging
|
import logging
|
||||||
|
from collections import defaultdict
|
||||||
from functools import wraps, partial
|
from functools import wraps, partial
|
||||||
|
|
||||||
#from localsys.db.schema import tables
|
#from localsys.db.schema import tables
|
||||||
@ -40,6 +41,17 @@ def collate(action_key, action_groups=None):
|
|||||||
class FormatRegistryMeta(type):
|
class FormatRegistryMeta(type):
|
||||||
def __new__(cls, name, bases, attrs):
|
def __new__(cls, name, bases, attrs):
|
||||||
action_registry = {}
|
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
|
# add registered superclass methods; iterate over bases (usually just one), then
|
||||||
# that base's chain down (reversed), then methods from each subclass
|
# that base's chain down (reversed), then methods from each subclass
|
||||||
@ -47,18 +59,15 @@ class FormatRegistryMeta(type):
|
|||||||
for _class in reversed(base.mro()):
|
for _class in reversed(base.mro()):
|
||||||
methods = inspect.getmembers(_class, predicate=inspect.isfunction)
|
methods = inspect.getmembers(_class, predicate=inspect.isfunction)
|
||||||
for _, method in methods:
|
for _, method in methods:
|
||||||
if hasattr(method, '_action_data'):
|
register_action(method)
|
||||||
action_key, action_groups = method._action_data
|
|
||||||
action_registry[action_key] = (method, action_groups)
|
|
||||||
|
|
||||||
# add final registered formats for the current class, overwriting any found in
|
# add final registered formats for the current class, overwriting any found in
|
||||||
# superclass chain
|
# superclass chain
|
||||||
for attr_name, attr_value in attrs.items():
|
for attr_name, attr_value in attrs.items():
|
||||||
if hasattr(attr_value, '_action_data'):
|
register_action(attr_value)
|
||||||
action_key, action_groups = attr_value._action_data
|
|
||||||
action_registry[action_key] = (method, action_groups)
|
|
||||||
|
|
||||||
attrs['action_map'] = action_registry
|
attrs['action_registry'] = action_registry
|
||||||
|
attrs['group_registry'] = group_registry
|
||||||
|
|
||||||
return super().__new__(cls, name, bases, attrs)
|
return super().__new__(cls, name, bases, attrs)
|
||||||
|
|
||||||
@ -98,11 +107,25 @@ class CO3(metaclass=FormatRegistryMeta):
|
|||||||
'''
|
'''
|
||||||
return []
|
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):
|
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')
|
logger.debug(f'Collation for {action_key} not supported')
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
return self.action_map[action_key](self)
|
return self.action_registry[action_key][0](self)
|
||||||
|
|
||||||
|
|
||||||
|
@ -29,32 +29,42 @@ from uuid import uuid4
|
|||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
|
||||||
from co3 import util
|
from co3 import util
|
||||||
|
from co3.schema import Schema
|
||||||
from co3.component import Component
|
from co3.component import Component
|
||||||
#from localsys.db.schema import tables
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
class Collector[C: Component, M: 'Mapper[C]']:
|
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))
|
self._inserts = defaultdict(lambda: defaultdict(list))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def inserts(self):
|
def inserts(self):
|
||||||
return self._inserts_from_receipts()
|
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)
|
inserts = defaultdict(list)
|
||||||
|
|
||||||
if receipts is None:
|
if receipts is None:
|
||||||
receipts = list(self._inserts.keys())
|
receipts = list(self._inserts.keys())
|
||||||
|
|
||||||
for receipt in receipts:
|
for receipt in receipts:
|
||||||
if pop: insert_dict = self._inserts.pop(receipt, {})
|
if pop:
|
||||||
else: insert_dict = self._inserts[receipt]
|
receipt_tuple = self._inserts.pop(receipt, None)
|
||||||
|
else:
|
||||||
|
receipt_tuple = self._inserts.get(receipt, None)
|
||||||
|
|
||||||
for table, insert_list in insert_dict.items():
|
if receipt_tuple is not None:
|
||||||
inserts[table].extend(insert_list)
|
component, insert_data = receipt_tuple
|
||||||
|
inserts[component].append(insert_data)
|
||||||
|
|
||||||
return dict(inserts)
|
return dict(inserts)
|
||||||
|
|
||||||
@ -62,24 +72,33 @@ class Collector[C: Component, M: 'Mapper[C]']:
|
|||||||
self._inserts = defaultdict(lambda: defaultdict(list))
|
self._inserts = defaultdict(lambda: defaultdict(list))
|
||||||
|
|
||||||
def _generate_unique_receipt(self):
|
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
|
Parameters:
|
||||||
the table object itself to this method
|
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}')
|
#logger.debug(f'Inserts provided for non-existent table {table_name}')
|
||||||
return None
|
return None
|
||||||
|
|
||||||
receipt = self._generate_unique_receipt()
|
receipt = self._generate_unique_receipt()
|
||||||
|
self._inserts[receipt] = (
|
||||||
self._inserts[receipt][table_name].append(
|
component,
|
||||||
utils.db.prepare_insert(
|
component.prepare_insert_data(insert_data),
|
||||||
tables.table_map[table_name],
|
|
||||||
insert_dict
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if receipts is not None:
|
if receipts is not None:
|
||||||
|
@ -14,6 +14,12 @@ class Component[T]:
|
|||||||
self.schema = schema
|
self.schema = schema
|
||||||
schema.add_component(self)
|
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):
|
def get_attributes(self):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ from abc import ABCMeta, abstractmethod
|
|||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
|
||||||
from co3.util.types import TableLike
|
from co3.util.types import SQLTableLike
|
||||||
from co3.component import Component
|
from co3.component import Component
|
||||||
|
|
||||||
|
|
||||||
@ -54,14 +54,46 @@ class Relation[T](ComposableComponent[T]):
|
|||||||
):
|
):
|
||||||
return self
|
return self
|
||||||
|
|
||||||
class SQLTable(Relation[TableLike]):
|
class SQLTable(Relation[SQLTableLike]):
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_table(cls, table: sa.Table, schema: 'SQLSchema'):
|
def from_table(cls, table: sa.Table, schema: 'SQLSchema'):
|
||||||
return cls(table.name, table, schema)
|
return cls(table.name, table, schema)
|
||||||
|
|
||||||
def get_attributes(self):
|
def get_attributes(self) -> tuple:
|
||||||
return tuple(self.obj.columns)
|
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
|
# key-value stores
|
||||||
class Dictionary(Relation[dict]):
|
class Dictionary(Relation[dict]):
|
||||||
|
121
co3/mapper.py
121
co3/mapper.py
@ -11,12 +11,23 @@ mapper = Mapper[sa.Table]()
|
|||||||
|
|
||||||
mapper.attach(
|
mapper.attach(
|
||||||
Type,
|
Type,
|
||||||
attributes=TypeTable,
|
attr_comp=TypeTable,
|
||||||
collation=CollateTable,
|
coll_comp=CollateTable,
|
||||||
collation_groups={
|
coll_groups={
|
||||||
'name': NameConversions
|
'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 typing import Callable, Self
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
@ -32,15 +43,34 @@ class Mapper[C: Component]:
|
|||||||
'''
|
'''
|
||||||
Mapper base class for housing schema components and managing relationships between CO3
|
Mapper base class for housing schema components and managing relationships between CO3
|
||||||
types and storage components (of type C).
|
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]
|
_collector_cls: type[Collector[C, Self]] = Collector[C, Self]
|
||||||
_composer_cls: type[Composer[C, Self]] = Composer[C, Self]
|
_composer_cls: type[Composer[C, Self]] = Composer[C, Self]
|
||||||
|
|
||||||
def __init__(self, co3_root: type[CO3], schema: Schema):
|
def __init__(self, schema: Schema[C]):
|
||||||
self.co3_root = co3_root
|
'''
|
||||||
self.schema = schema
|
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.composer = self._composer_cls()
|
||||||
|
|
||||||
self.attribute_comps: dict[type[CO3], C] = {}
|
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
|
coll_groups: storage components for named collation groups; dict mapping group
|
||||||
names to components
|
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
|
# check attribute component in registered schema
|
||||||
attr_comp = self._check_component(attr_comp)
|
attr_comp = self._check_component(attr_comp)
|
||||||
self.attribute_comps[type_ref] = 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)
|
self.collation_groups[type_ref].update(coll_groups)
|
||||||
|
|
||||||
def attach_hierarchy(
|
def attach_many(
|
||||||
self,
|
self,
|
||||||
type_ref: type[CO3],
|
type_list: list[type[CO3]],
|
||||||
obj_name_map: Callable[[type[CO3]], str],
|
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,
|
self,
|
||||||
type_instance: CO3,
|
type_ref: type[CO3]
|
||||||
action_key,
|
) -> C | None:
|
||||||
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:
|
|
||||||
return self.attribute_comps.get(type_ref, None)
|
return self.attribute_comps.get(type_ref, None)
|
||||||
|
|
||||||
def get_collation_comp(self, type_ref: CO3, group=str | None) -> C | None:
|
def get_collation_comp(
|
||||||
return self.collation_group.get(type_ref, {}).get(group, None)
|
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.
|
Stages inserts up the inheritance chain, and down through components.
|
||||||
|
|
||||||
@ -166,7 +209,7 @@ class Mapper[C: Component]:
|
|||||||
if collation_data is None:
|
if collation_data is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
_, action_groups = obj.action_map[action_key]
|
_, action_groups = obj.action_registry[action_key]
|
||||||
for action_group in action_groups:
|
for action_group in action_groups:
|
||||||
collation_component = self.get_collation_comp(_cls, group=action_group)
|
collation_component = self.get_collation_comp(_cls, group=action_group)
|
||||||
|
|
||||||
@ -174,9 +217,9 @@ class Mapper[C: Component]:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# gather connective data for collation components
|
# 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,
|
collation_component,
|
||||||
{
|
{
|
||||||
**connective_data,
|
**connective_data,
|
||||||
@ -186,7 +229,7 @@ class Mapper[C: Component]:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# handle components
|
# 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))
|
receipts.extend(comp.collect(collector, formats=formats))
|
||||||
|
|
||||||
return receipts
|
return receipts
|
||||||
|
@ -3,14 +3,19 @@ Schema
|
|||||||
|
|
||||||
Collection of related storage components, often representing the data structure of an
|
Collection of related storage components, often representing the data structure of an
|
||||||
entire database. Some databases support multiple schemas, however. In general, a Schema
|
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
|
can wrap up an associated subset of components within a single database, so long as
|
||||||
`Manager.recreate()` supports creating components in separate calls.
|
`Manager.recreate()` supports creating components in separate calls (even if the
|
||||||
|
associated database doesn't explicitly support multiple schemas).
|
||||||
|
|
||||||
Schema objects are used to:
|
Schema objects are used to:
|
||||||
|
|
||||||
- Semantically group related storage components
|
- Semantically group related storage components
|
||||||
- Tell databases what components to create/remove together
|
- Tell databases what components to create/remove together
|
||||||
- Provide target contexts for connected CO3 type systems within Mappers
|
- 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
|
from co3.component import Component
|
||||||
|
@ -125,18 +125,6 @@ def sa_exec_explicit(engine, stmt, bind_params=None):
|
|||||||
trans.rollback() # rollback the transaction explicitly
|
trans.rollback() # rollback the transaction explicitly
|
||||||
raise
|
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):
|
def deferred_fkey(target, **kwargs):
|
||||||
return sa.ForeignKey(
|
return sa.ForeignKey(
|
||||||
target,
|
target,
|
||||||
@ -152,27 +140,6 @@ def deferred_cd_fkey(target, **kwargs):
|
|||||||
'''
|
'''
|
||||||
return deferred_fkey(target, ondelete='CASCADE', **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):
|
def get_column_names_str_table(engine, table: str):
|
||||||
col_sql = f'PRAGMA table_info({table});'
|
col_sql = f'PRAGMA table_info({table});'
|
||||||
|
@ -3,4 +3,4 @@ from typing import TypeVar
|
|||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
TableLike = TypeVar('TableLike', bound=sa.Table | sa.Subquery | sa.Join)
|
SQLTableLike = TypeVar('SQLTableLike', bound=sa.Table | sa.Subquery | sa.Join)
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
|
'''
|
||||||
|
just remembered tomatos aren't vegetables. whoops
|
||||||
|
'''
|
||||||
import random
|
import random
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
|
||||||
from co3.schemas import SQLSchema
|
from co3.schemas import SQLSchema
|
||||||
from co3 import CO3, collate
|
from co3 import CO3, collate, Mapper
|
||||||
from co3 import util
|
from co3 import util
|
||||||
|
|
||||||
|
|
||||||
@ -20,6 +23,12 @@ class Tomato(Vegetable):
|
|||||||
@property
|
@property
|
||||||
def attributes(self):
|
def attributes(self):
|
||||||
return vars(self)
|
return vars(self)
|
||||||
|
|
||||||
|
def collation_attributes(self, action_key, action_grounp):
|
||||||
|
return {
|
||||||
|
'name': self.name,
|
||||||
|
'state': action_key,
|
||||||
|
}
|
||||||
|
|
||||||
@collate('ripe', action_groups=['aging'])
|
@collate('ripe', action_groups=['aging'])
|
||||||
def ripen(self):
|
def ripen(self):
|
||||||
@ -39,42 +48,58 @@ class Tomato(Vegetable):
|
|||||||
'pieces': random.randint(2, 12)
|
'pieces': random.randint(2, 12)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type_list = [Vegetable, Tomato]
|
||||||
|
|
||||||
'''
|
'''
|
||||||
VEGETABLE
|
VEGETABLE
|
||||||
|
|
|
|
||||||
TOMATO -- AGING
|
TOMATO -- AGING
|
||||||
|
|
|
|
||||||
-- COOKING
|
-- 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()
|
metadata = sa.MetaData()
|
||||||
vegetable_table = sa.Table(
|
vegetable_table = sa.Table(
|
||||||
'vegetable',
|
'vegetable',
|
||||||
metadata,
|
metadata,
|
||||||
sa.Column('id', sa.Integer, primary_key=True),
|
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),
|
sa.Column('color', sa.String),
|
||||||
)
|
)
|
||||||
tomato_table = sa.Table(
|
tomato_table = sa.Table(
|
||||||
'tomato',
|
'tomato',
|
||||||
metadata,
|
metadata,
|
||||||
sa.Column('id', sa.Integer, primary_key=True),
|
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),
|
sa.Column('radius', sa.Integer),
|
||||||
)
|
)
|
||||||
tomato_aging_table = sa.Table(
|
tomato_aging_table = sa.Table(
|
||||||
'tomato_aging_states',
|
'tomato_aging_states',
|
||||||
metadata,
|
metadata,
|
||||||
sa.Column('id', sa.Integer, primary_key=True),
|
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('state', sa.String),
|
||||||
sa.Column('age', sa.Integer),
|
sa.Column('age', sa.Integer),
|
||||||
)
|
)
|
||||||
tomato_cooking_table = sa.Table(
|
tomato_cooking_table = sa.Table(
|
||||||
'tomato_cooking_states',
|
'tomato_cooking_states',
|
||||||
metadata,
|
metadata,
|
||||||
sa.Column('id', sa.Integer, primary_key=True),
|
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('state', sa.String),
|
||||||
sa.Column('pieces', sa.Integer),
|
sa.Column('pieces', sa.Integer),
|
||||||
)
|
)
|
||||||
vegetable_schema = SQLSchema.from_metadata(metadata)
|
vegetable_schema = SQLSchema.from_metadata(metadata)
|
||||||
|
vegetable_mapper = Mapper(vegetable_schema)
|
||||||
|
|
||||||
|
@ -16,8 +16,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"source": [
|
"source": [
|
||||||
"from co3 import Mapper\n",
|
|
||||||
"\n",
|
|
||||||
"import vegetables"
|
"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]`"
|
"- 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",
|
"cell_type": "code",
|
||||||
"execution_count": 3,
|
"execution_count": 2,
|
||||||
"id": "7d80f7b9-7458-4ad4-8c1a-3ea56e796b4e",
|
"id": "7d80f7b9-7458-4ad4-8c1a-3ea56e796b4e",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
|
"from co3 import Mapper\n",
|
||||||
|
"\n",
|
||||||
"vegetable_mapper = Mapper(\n",
|
"vegetable_mapper = Mapper(\n",
|
||||||
" vegetables.Vegetable,\n",
|
" vegetables.Vegetable,\n",
|
||||||
" vegetables.vegetable_schema\n",
|
" vegetables.vegetable_schema\n",
|
||||||
@ -46,21 +69,34 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": null,
|
"execution_count": 3,
|
||||||
"id": "d24d31b4-c4a6-4a1e-8bea-c44378aadfdd",
|
"id": "d24d31b4-c4a6-4a1e-8bea-c44378aadfdd",
|
||||||
"metadata": {},
|
"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": [
|
"source": [
|
||||||
"# not valid; tables need to be wrapped in CO3 Components\n",
|
"# not valid; tables need to be wrapped in CO3 Components\n",
|
||||||
|
"'''\n",
|
||||||
"vegetable_mapper.attach(\n",
|
"vegetable_mapper.attach(\n",
|
||||||
" vegetables.Vegetable,\n",
|
" vegetables.Vegetable,\n",
|
||||||
" vegetables.vegetable_table,\n",
|
" vegetables.vegetable_table,\n",
|
||||||
")"
|
")\n",
|
||||||
|
"'''"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 6,
|
"execution_count": 4,
|
||||||
"id": "f9408562-bf50-4522-909c-318557f85948",
|
"id": "f9408562-bf50-4522-909c-318557f85948",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [],
|
"outputs": [],
|
||||||
@ -78,7 +114,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 7,
|
"execution_count": 5,
|
||||||
"id": "05fdd404-87ee-4187-832f-2305272758ae",
|
"id": "05fdd404-87ee-4187-832f-2305272758ae",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [],
|
"outputs": [],
|
||||||
@ -96,21 +132,43 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": null,
|
"execution_count": 6,
|
||||||
"id": "e9b6af49-a69d-41cc-beae-1b6f171cd2f5",
|
"id": "e9b6af49-a69d-41cc-beae-1b6f171cd2f5",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"# attach entire type hierarchy w/ type->name map\n",
|
"# attach entire type hierarchy w/ type->name map\n",
|
||||||
"vegetable_mapper.attach_hierarchy(\n",
|
"vegetable_mapper.attach_hierarchy(\n",
|
||||||
"# this might make more sense during init\n",
|
" # this might make more sense during init\n",
|
||||||
" lambda x:x.__name__.lower())\n",
|
" vegetables.Vegetable,\n",
|
||||||
|
" lambda x:x.__name__.lower()\n",
|
||||||
")"
|
")"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 9,
|
"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",
|
"id": "2e4336ab-5b5f-484d-815d-164d4b6f40a0",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [
|
"outputs": [
|
||||||
@ -118,16 +176,16 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"text/plain": [
|
"text/plain": [
|
||||||
"{'co3_root': vegetables.Vegetable,\n",
|
"{'co3_root': vegetables.Vegetable,\n",
|
||||||
" 'schema': <co3.schemas.SQLSchema at 0x74ac03f5c8c0>,\n",
|
" 'schema': <co3.schemas.SQLSchema at 0x732074224aa0>,\n",
|
||||||
" 'collector': <co3.collector.Collector at 0x74ac0357ae70>,\n",
|
" 'collector': <co3.collector.Collector at 0x7320757da120>,\n",
|
||||||
" 'composer': <co3.composer.Composer at 0x74ac0357a4b0>,\n",
|
" 'composer': <co3.composer.Composer at 0x7320757da9c0>,\n",
|
||||||
" 'attribute_comps': {vegetables.Tomato: <co3.components.SQLTable at 0x74ac09d4a720>},\n",
|
" 'attribute_comps': {vegetables.Tomato: <co3.components.SQLTable at 0x732074224cb0>},\n",
|
||||||
" 'collation_groups': defaultdict(dict,\n",
|
" 'collation_groups': defaultdict(dict,\n",
|
||||||
" {vegetables.Tomato: {'aging': <co3.components.SQLTable at 0x74ac03f5cad0>,\n",
|
" {vegetables.Tomato: {'aging': <co3.components.SQLTable at 0x732074224ce0>,\n",
|
||||||
" 'cooking': <co3.components.SQLTable at 0x74ac03f5cb00>}})}"
|
" 'cooking': <co3.components.SQLTable at 0x732074224d10>}})}"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"execution_count": 9,
|
"execution_count": 7,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"output_type": "execute_result"
|
"output_type": "execute_result"
|
||||||
}
|
}
|
||||||
@ -137,40 +195,187 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "markdown",
|
||||||
"execution_count": 10,
|
"id": "47859e25-b803-4459-a581-f10bbcfac716",
|
||||||
"id": "c16786d4-0b71-42d9-97f7-7893c542104e",
|
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [],
|
|
||||||
"source": [
|
"source": [
|
||||||
"tomato = vegetables.Tomato('t1', 5)"
|
"## Holistic attachment"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 11,
|
"execution_count": 2,
|
||||||
"id": "884d6753-c763-4e71-824a-711436e203e1",
|
"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": {},
|
"metadata": {},
|
||||||
"outputs": [
|
"outputs": [
|
||||||
{
|
{
|
||||||
"data": {
|
"data": {
|
||||||
"text/plain": [
|
"text/plain": [
|
||||||
"<vegetables.Tomato at 0x74ac082bacc0>"
|
"{'age': 4}"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"execution_count": 11,
|
"execution_count": 3,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"output_type": "execute_result"
|
"output_type": "execute_result"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"source": [
|
"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",
|
"cell_type": "code",
|
||||||
"execution_count": null,
|
"execution_count": null,
|
||||||
"id": "137d0bf1-940d-448c-91e9-01e7fc4f31b4",
|
"id": "d166b9af-e3ba-4750-9dcb-d8d4e08fe4d3",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": []
|
"source": []
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
|
'''
|
||||||
|
just remembered tomatos aren't vegetables. whoops
|
||||||
|
'''
|
||||||
import random
|
import random
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
|
||||||
from co3.schemas import SQLSchema
|
from co3.schemas import SQLSchema
|
||||||
from co3 import CO3, collate
|
from co3 import CO3, collate, Mapper
|
||||||
from co3 import util
|
from co3 import util
|
||||||
|
|
||||||
|
|
||||||
@ -20,6 +23,12 @@ class Tomato(Vegetable):
|
|||||||
@property
|
@property
|
||||||
def attributes(self):
|
def attributes(self):
|
||||||
return vars(self)
|
return vars(self)
|
||||||
|
|
||||||
|
def collation_attributes(self, action_key, action_grounp):
|
||||||
|
return {
|
||||||
|
'name': self.name,
|
||||||
|
'state': action_key,
|
||||||
|
}
|
||||||
|
|
||||||
@collate('ripe', action_groups=['aging'])
|
@collate('ripe', action_groups=['aging'])
|
||||||
def ripen(self):
|
def ripen(self):
|
||||||
@ -39,42 +48,58 @@ class Tomato(Vegetable):
|
|||||||
'pieces': random.randint(2, 12)
|
'pieces': random.randint(2, 12)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type_list = [Vegetable, Tomato]
|
||||||
|
|
||||||
'''
|
'''
|
||||||
VEGETABLE
|
VEGETABLE
|
||||||
|
|
|
|
||||||
TOMATO -- AGING
|
TOMATO -- AGING
|
||||||
|
|
|
|
||||||
-- COOKING
|
-- 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()
|
metadata = sa.MetaData()
|
||||||
vegetable_table = sa.Table(
|
vegetable_table = sa.Table(
|
||||||
'vegetable',
|
'vegetable',
|
||||||
metadata,
|
metadata,
|
||||||
sa.Column('id', sa.Integer, primary_key=True),
|
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),
|
sa.Column('color', sa.String),
|
||||||
)
|
)
|
||||||
tomato_table = sa.Table(
|
tomato_table = sa.Table(
|
||||||
'tomato',
|
'tomato',
|
||||||
metadata,
|
metadata,
|
||||||
sa.Column('id', sa.Integer, primary_key=True),
|
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),
|
sa.Column('radius', sa.Integer),
|
||||||
)
|
)
|
||||||
tomato_aging_table = sa.Table(
|
tomato_aging_table = sa.Table(
|
||||||
'tomato_aging_states',
|
'tomato_aging_states',
|
||||||
metadata,
|
metadata,
|
||||||
sa.Column('id', sa.Integer, primary_key=True),
|
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('state', sa.String),
|
||||||
sa.Column('age', sa.Integer),
|
sa.Column('age', sa.Integer),
|
||||||
)
|
)
|
||||||
tomato_cooking_table = sa.Table(
|
tomato_cooking_table = sa.Table(
|
||||||
'tomato_cooking_states',
|
'tomato_cooking_states',
|
||||||
metadata,
|
metadata,
|
||||||
sa.Column('id', sa.Integer, primary_key=True),
|
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('state', sa.String),
|
||||||
sa.Column('pieces', sa.Integer),
|
sa.Column('pieces', sa.Integer),
|
||||||
)
|
)
|
||||||
vegetable_schema = SQLSchema.from_metadata(metadata)
|
vegetable_schema = SQLSchema.from_metadata(metadata)
|
||||||
|
vegetable_mapper = Mapper(vegetable_schema)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user