From 6caf2631b858ee624465585eb87f878aa05845fa Mon Sep 17 00:00:00 2001 From: "Sam G." Date: Thu, 18 Apr 2024 02:06:42 -0700 Subject: [PATCH] add inital test suite, fix minor Mapping group bug --- Makefile | 3 + co3/co3.py | 13 -- co3/managers/sql.py | 30 ++- co3/mapper.py | 20 +- .../database-checkpoint.ipynb | 204 ------------------ .../mapper-checkpoint.ipynb | 145 ------------- .../vegetables-checkpoint.py | 135 ------------ {examples => notebooks}/database.ipynb | 2 +- {examples => notebooks}/mapper.ipynb | 4 +- tests/{co4_example.py => co4_api_demo.py} | 0 tests/imports.py | 19 -- {examples => tests/setups}/vegetables.py | 0 tests/test_co3.py | 57 +++++ tests/test_database.py | 36 ++++ tests/test_imports.py | 25 +++ tests/test_mapper.py | 56 +++++ tests/test_schema.py | 26 +++ 17 files changed, 238 insertions(+), 537 deletions(-) delete mode 100644 examples/.ipynb_checkpoints/database-checkpoint.ipynb delete mode 100644 examples/.ipynb_checkpoints/mapper-checkpoint.ipynb delete mode 100644 examples/.ipynb_checkpoints/vegetables-checkpoint.py rename {examples => notebooks}/database.ipynb (99%) rename {examples => notebooks}/mapper.ipynb (99%) rename tests/{co4_example.py => co4_api_demo.py} (100%) delete mode 100644 tests/imports.py rename {examples => tests/setups}/vegetables.py (100%) create mode 100644 tests/test_co3.py create mode 100644 tests/test_database.py create mode 100644 tests/test_imports.py create mode 100644 tests/test_mapper.py create mode 100644 tests/test_schema.py diff --git a/Makefile b/Makefile index 8d88d76..72d0667 100644 --- a/Makefile +++ b/Makefile @@ -14,3 +14,6 @@ docs-clean: make -C docs/ clean ## ------------------------------------------ ## +## ----------------- tests ------------------ ## +test: + pytest --pyargs tests -v diff --git a/co3/co3.py b/co3/co3.py index 96b181f..cc0ff4f 100644 --- a/co3/co3.py +++ b/co3/co3.py @@ -12,22 +12,9 @@ import logging from collections import defaultdict from functools import wraps, partial -#from localsys.db.schema import tables logger = logging.getLogger(__name__) - -#def register_format(_format): -# def decorator(func): -# self.collate.format_map[_format] = func -# -# @wraps(func) -# def register(*args, **kwargs): -# return func(*args, **kwargs) -# -# return register -# return decorator - def collate(action_key, action_groups=None): def decorator(func): nonlocal action_groups diff --git a/co3/managers/sql.py b/co3/managers/sql.py index cc12fed..1f9954d 100644 --- a/co3/managers/sql.py +++ b/co3/managers/sql.py @@ -35,13 +35,10 @@ Note: Options for insert/update model build, then single thread bulk INSERT. (**Note**: this is what the method does). ''' -from pathlib import Path -from collections import defaultdict +import time import logging import threading -import math -import time -import pprint +from pathlib import Path from concurrent.futures import wait, as_completed import sqlalchemy as sa @@ -98,7 +95,11 @@ class SQLManager(RelationalManager[SQLTable]): def add_router(self, router): self.routers.append(router) - def recreate(self, schema: Schema[SQLTable], engine: SQLEngine): + def recreate( + self, + schema: Schema[SQLTable], + engine: SQLEngine + ) -> None: ''' Ideally this remains open, as we can't necessarily rely on a SQLAlchemy metadata object for all kinds of SQLDatabases (would depend on the backend, for instance). @@ -118,16 +119,22 @@ class SQLManager(RelationalManager[SQLTable]): connection, component, inserts: list[dict], + commit=True ): ''' Parameters: ''' with self._insert_lock: - connection.execute( + res = connection.execute( sa.insert(component.obj), inserts ) + if commit: + connection.commit() + + return res + def insert_many(self, connection, inserts: dict): ''' Perform provided table inserts. @@ -139,9 +146,10 @@ class SQLManager(RelationalManager[SQLTable]): if total_inserts < 1: return logger.info(f'Total of {total_inserts} sync inserts to perform') + start = time.time() # TODO: add some exception handling? may be fine w default propagation - start = time.time() + res_list = [] with self._insert_lock: for component in inserts: comp_inserts = inserts[component] @@ -151,10 +159,14 @@ class SQLManager(RelationalManager[SQLTable]): f'Inserting {len(comp_inserts)} out-of-date entries into component "{component}"' ) - self.insert(connection, component, comp_inserts) + res = self.insert(connection, component, comp_inserts, commit=False) + res_list.append(res) + connection.commit() logger.info(f'Insert transaction completed successfully in {time.time()-start:.2f}s') + return res_list + def _file_sync_bools(self): synced_bools = [] fpaths = utils.paths.iter_nested_paths(self.collector.basepath, no_dir=True) diff --git a/co3/mapper.py b/co3/mapper.py index 83bfc34..f8d06d4 100644 --- a/co3/mapper.py +++ b/co3/mapper.py @@ -131,7 +131,7 @@ class Mapper[C: Component]: type_list: list[type[CO3]], attr_name_map: Callable[[type[CO3]], str | C], coll_name_map: Callable[[type[CO3], str], str | C] | None = None, - ): + ) -> None: ''' 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 @@ -194,8 +194,10 @@ class Mapper[C: Component]: Returns: dict with keys and values relevant for associated SQLite tables ''' + # default is to have no actions if action_keys is None: - action_keys = list(obj.action_map.keys()) + action_keys = [] + #action_keys = list(obj.action_registry.keys()) receipts = [] for _cls in reversed(obj.__class__.__mro__[:-2]): @@ -218,7 +220,7 @@ class Mapper[C: Component]: if collation_data is None: continue - _, action_groups = obj.action_registry[action_key] + _, action_groups = obj.action_registry.get(action_key, (None, [])) for action_group in action_groups: collation_component = self.get_collation_comp(_cls, group=action_group) @@ -329,7 +331,7 @@ class ComposableMapper[C: ComposableComponent](Mapper[C]): def compose( self, - obj: CO3, + obj: CO3 | type[CO3], action_groups: list[str] | None = None, *compose_args, **compose_kwargs, @@ -344,12 +346,14 @@ class ComposableMapper[C: ComposableComponent](Mapper[C]): chain (for components / sa.Relationships, it's a little easier). Parameters: - outer: whether to use outer joins down the chain - conversion: whether to return conversion joins or base primitives - full: whether to return fully connected primitive and conversion table + obj: either a CO3 instance or a type reference ''' + class_ref = obj + if isinstance(obj, CO3): + class_ref = obj.__class__ + attr_comp_agg = None - for _cls in reversed(obj.__class__.__mro__[:-2]): + for _cls in reversed(class_ref.__mro__[:-2]): attr_comp = self.get_attribute_comp(_cls) # require an attribute component for type consideration diff --git a/examples/.ipynb_checkpoints/database-checkpoint.ipynb b/examples/.ipynb_checkpoints/database-checkpoint.ipynb deleted file mode 100644 index 025b7ed..0000000 --- a/examples/.ipynb_checkpoints/database-checkpoint.ipynb +++ /dev/null @@ -1,204 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "id": "6f6fbc7e-4fb9-4353-b2ee-9ea819a3c896", - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/smgr/.pyenv/versions/co4/lib/python3.12/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", - " from .autonotebook import tqdm as notebook_tqdm\n" - ] - } - ], - "source": [ - "import vegetables" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "88fd0ea8-9c94-4569-a51b-823a04f32f55", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'age': 3}" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "tomato = vegetables.Tomato('t1', 5)\n", - "\n", - "# test a register collation action\n", - "tomato.collate('ripe')" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "348926d9-7137-4eff-a919-508788553dd2", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "['afff61ec-e0b0-44bb-9f0d-06008a82f6a5',\n", - " '5edfa13c-0eb1-4bbc-b55e-1550ff7df3d2',\n", - " '4568a2d4-eb41-4b15-9a29-e3e22906c661']" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "vegetables.vegetable_mapper.collect(tomato, ['ripe'])" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "4e5e7319-11bf-4051-951b-08c84e9f3874", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - " vegetable+tomato+tomato_aging_states+tomato_cooking_states" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "vegetables.vegetable_mapper.compose(tomato, action_groups=['aging', 'cooking'])" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "aa290686-8074-4038-a3cc-ce6817844653", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[Column('id', Integer(), table=, primary_key=True, nullable=False),\n", - " Column('name', String(), table=),\n", - " Column('color', String(), table=),\n", - " Column('id', Integer(), table=, primary_key=True, nullable=False),\n", - " Column('name', String(), ForeignKey('vegetable.name'), table=),\n", - " Column('radius', Integer(), table=),\n", - " Column('id', Integer(), table=, primary_key=True, nullable=False),\n", - " Column('name', String(), ForeignKey('tomato.name'), table=),\n", - " Column('state', String(), table=),\n", - " Column('age', Integer(), table=)]" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "list(vegetables.vegetable_mapper.compose(tomato, action_groups=['aging']).obj.columns)" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "f3c7e37d-ba9e-4bae-ae44-adc922bf5f4c", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "(vegetables.Tomato, vegetables.Vegetable, co3.co3.CO3, object)" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "tomato.__class__.__mro__" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "c21d2c54-39e2-4de3-93bc-763896ed348e", - "metadata": {}, - "outputs": [], - "source": [ - "from co3.databases import SQLDatabase\n", - "\n", - "db = SQLDatabase('sqlite://', echo=True)" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "a785d202-99d3-4ae7-859e-ee22b481f8df", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "db.recreate(vegetable_schema) " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "cda01cb0-1666-4cb1-aa64-bcdca871aff5", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "co3", - "language": "python", - "name": "co3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.12.2" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/examples/.ipynb_checkpoints/mapper-checkpoint.ipynb b/examples/.ipynb_checkpoints/mapper-checkpoint.ipynb deleted file mode 100644 index a519865..0000000 --- a/examples/.ipynb_checkpoints/mapper-checkpoint.ipynb +++ /dev/null @@ -1,145 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "id": "e02ccafe-e04d-4312-acba-e41cf7b1c021", - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/smgr/.pyenv/versions/co4/lib/python3.12/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", - " from .autonotebook import tqdm as notebook_tqdm\n" - ] - } - ], - "source": [ - "from co3 import Mapper\n", - "\n", - "import vegetables" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "7d80f7b9-7458-4ad4-8c1a-3ea56e796b4e", - "metadata": {}, - "outputs": [], - "source": [ - "vegetable_mapper = Mapper(\n", - " vegetables.Vegetable,\n", - " vegetables.vegetable_schema\n", - ")\n", - "\n", - "vegetable_mapper.attach(\n", - " vegetables.Vegetable,\n", - " vegetables.vegetable_table,\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "f9408562-bf50-4522-909c-318557f85948", - "metadata": {}, - "outputs": [], - "source": [ - "# manually attach component\n", - "vegetable_mapper.attach(\n", - " vegetables.Tomato,\n", - " vegetables.tomato_table,\n", - " coll_groups={\n", - " 'aging': vegetables.vegetable_schema.get_component('tomato_aging_states'),\n", - " 'cooking': vegetables.vegetable_schema.get_component('tomato_cooking_states'),\n", - " },\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "05fdd404-87ee-4187-832f-2305272758ae", - "metadata": {}, - "outputs": [], - "source": [ - "# attach by name in schema\n", - "vegetable_mapper.attach(\n", - " vegetables.Tomato,\n", - " vegetables.tomato_table,\n", - " coll_groups={\n", - " 'aging': 'tomato_aging_states',\n", - " 'cooking': 'tomato_cooking_states',\n", - " },\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "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", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "2e4336ab-5b5f-484d-815d-164d4b6f40a0", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "defaultdict(dict,\n", - " {vegetables.Tomato: {'aging': ,\n", - " 'cooking': }})" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "vegetable_mapper.collation_groups" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d416f9cd-2cb6-4a6e-bab7-86ac21216b8c", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "co3", - "language": "python", - "name": "co3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.12.2" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/examples/.ipynb_checkpoints/vegetables-checkpoint.py b/examples/.ipynb_checkpoints/vegetables-checkpoint.py deleted file mode 100644 index ed09698..0000000 --- a/examples/.ipynb_checkpoints/vegetables-checkpoint.py +++ /dev/null @@ -1,135 +0,0 @@ -''' -just remembered tomatos aren't vegetables. whoops -''' -import random - -import sqlalchemy as sa - -from co3.schemas import SQLSchema -from co3 import CO3, collate, Mapper, ComposableMapper -from co3 import util - - -class Vegetable(CO3): - def __init__(self, name, color): - self.name = name - self.color = color - -class Tomato(Vegetable): - def __init__(self, name, radius): - super().__init__(name, 'red') - self.radius = radius - - @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): - return { - 'age': random.randint(1, 6) - } - - @collate('rotten', action_groups=['aging']) - def rot(self): - return { - 'age': random.randint(4, 9) - } - - @collate('diced', action_groups=['cooking']) - def dice(self): - return { - '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, unique=True), - - sa.Column('color', sa.String), -) -tomato_table = sa.Table( - 'tomato', - metadata, - sa.Column('id', sa.Integer, primary_key=True), - 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('name', sa.String, util.db.deferred_cd_fkey('tomato.name'), unique=True), - - sa.Column('state', sa.String), - sa.Column('age', sa.Integer), -) -tomato_cooking_table = sa.Table( - 'tomato_cooking_states', - metadata, - sa.Column('id', sa.Integer, primary_key=True), - 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) - -def general_compose_map(c1, c2): - return c1.obj.c.name == c2.obj.c.name - -vegetable_mapper = ComposableMapper( - vegetable_schema, - attr_compose_map=general_compose_map, - coll_compose_map=general_compose_map, -) - -def attr_name_map(cls): - return f'{cls.__name__.lower()}' - -def coll_name_map(cls, action_group): - return f'{cls.__name__.lower()}_{action_group}_states' - -vegetable_mapper.attach_many( - type_list, - attr_name_map, - coll_name_map, -) - -''' -new mapping type for Mapper attachment: - -Callable[ [type[CO3], str|None], tuple[str, tuple[str], tuple[str]]] - -tail tuples to associate column names from central table to collation - -this should complete the auto-compose horizontally -''' - diff --git a/examples/database.ipynb b/notebooks/database.ipynb similarity index 99% rename from examples/database.ipynb rename to notebooks/database.ipynb index 1bc7e69..8ed8b56 100644 --- a/examples/database.ipynb +++ b/notebooks/database.ipynb @@ -196,7 +196,7 @@ "source": [ "with db.engine.connect() as connection:\n", " db.manager.insert_many(\n", - " connection, \n", + " connection,\n", " vegetables.vegetable_mapper.collector.inserts,\n", " )" ] diff --git a/examples/mapper.ipynb b/notebooks/mapper.ipynb similarity index 99% rename from examples/mapper.ipynb rename to notebooks/mapper.ipynb index 657939f..3aac3d4 100644 --- a/examples/mapper.ipynb +++ b/notebooks/mapper.ipynb @@ -45,9 +45,7 @@ { "cell_type": "markdown", "id": "d2672422-3596-4eab-ac44-5da617f74b80", - "metadata": { - "jp-MarkdownHeadingCollapsed": true - }, + "metadata": {}, "source": [ "## Explicit example steps" ] diff --git a/tests/co4_example.py b/tests/co4_api_demo.py similarity index 100% rename from tests/co4_example.py rename to tests/co4_api_demo.py diff --git a/tests/imports.py b/tests/imports.py deleted file mode 100644 index db8a53c..0000000 --- a/tests/imports.py +++ /dev/null @@ -1,19 +0,0 @@ -from co3 import Accessor -from co3 import CO3 -from co3 import Collector -from co3 import Database -from co3 import Indexer -from co3 import Manager -from co3 import Mapper -from co3 import Relation - - -from co3.accessors import SQLAccessor - -from co3.databases import SQLDatabase -from co3.databases import SQLiteDatabase - -from co3.managers import SQLManager - -from co3.relations import TabularRelation -from co3.relations import SQLTable diff --git a/examples/vegetables.py b/tests/setups/vegetables.py similarity index 100% rename from examples/vegetables.py rename to tests/setups/vegetables.py diff --git a/tests/test_co3.py b/tests/test_co3.py new file mode 100644 index 0000000..4043b40 --- /dev/null +++ b/tests/test_co3.py @@ -0,0 +1,57 @@ +from co3.components import Relation + +from setups import vegetables as veg + + +def test_mapper_getters(): + veg_comp = veg.vegetable_schema.get_component('vegetable') + tom_comp = veg.vegetable_schema.get_component('tomato') + + assert veg.vegetable_mapper.get_attribute_comp(veg.Vegetable) is veg_comp + assert veg.vegetable_mapper.get_attribute_comp(veg.Tomato) is tom_comp + + tom_aging = veg.vegetable_schema.get_component('tomato_aging_states') + tom_cooking = veg.vegetable_schema.get_component('tomato_cooking_states') + + assert veg.vegetable_mapper.get_collation_comp(veg.Tomato, 'aging') is tom_aging + assert veg.vegetable_mapper.get_collation_comp(veg.Tomato, 'cooking') is tom_cooking + +def test_mapper_attach(): + assert veg.vegetable_mapper.attach( + veg.Tomato, + 'tomato', + coll_groups={ + 'aging': 'tomato_aging_states', + 'cooking': 'tomato_cooking_states', + }, + ) is None + +def test_mapper_attach_many(): + assert veg.vegetable_mapper.attach_many( + [veg.Vegetable, veg.Tomato], + lambda t: f'{t.__name__.lower()}' + ) is None + +def test_mapper_collect(): + tomato = veg.Tomato('t1', 10) + receipts = veg.vegetable_mapper.collect(tomato) + + assert len(receipts) == 2 + + # attempt to retrieve receipts one at a time + res1 = veg.vegetable_mapper.collector.collect_inserts([receipts[0]]) + + assert len(res1) == 1 # should be just one match + assert len(res1[next(iter(res1.keys()))]) == 1 # and one dict for matching comp + + # try again, check no persistent match + res1 = veg.vegetable_mapper.collector.collect_inserts([receipts[0]]) + + assert len(res1) == 0 # should be no matches for the same receipt + + res2 = veg.vegetable_mapper.collector.collect_inserts([receipts[1]]) + + assert len(res2) == 1 + assert len(res2[next(iter(res2.keys()))]) == 1 + + diff --git a/tests/test_database.py b/tests/test_database.py new file mode 100644 index 0000000..c6c33a5 --- /dev/null +++ b/tests/test_database.py @@ -0,0 +1,36 @@ +from co3.components import Relation +from co3.databases import SQLDatabase + +from setups import vegetables as veg + + +db = None + +def test_database_init(): + global db + + db = SQLDatabase('sqlite://') + assert True + +def test_database_recreate(): + db.recreate(veg.vegetable_schema) + assert True + +def test_database_insert(): + tomato = veg.Tomato('t1', 5) + veg.vegetable_mapper.collect(tomato) + + with db.engine.connect() as connection: + assert db.manager.insert_many( + connection, + veg.vegetable_mapper.collector.inserts, + ) is not None + +def test_database_access(): + agg_table = veg.vegetable_mapper.compose(veg.Tomato) + + with db.engine.connect() as connection: + assert db.accessor.select( + connection, + agg_table, + ) is not None diff --git a/tests/test_imports.py b/tests/test_imports.py new file mode 100644 index 0000000..aa6465f --- /dev/null +++ b/tests/test_imports.py @@ -0,0 +1,25 @@ +def test_import(): + from co3 import Accessor + from co3 import CO3 + from co3 import Collector + from co3 import Database + from co3 import Indexer + from co3 import Manager + from co3 import Mapper + from co3 import Component + + + from co3.accessors import SQLAccessor + from co3.accessors import FTSAccessor + from co3.accessors import VSSAccessor + + from co3.databases import SQLDatabase + from co3.databases import SQLiteDatabase + + from co3.managers import SQLManager + + from co3.components import ComposableComponent + from co3.components import Relation + from co3.components import SQLTable + + assert True diff --git a/tests/test_mapper.py b/tests/test_mapper.py new file mode 100644 index 0000000..feb120b --- /dev/null +++ b/tests/test_mapper.py @@ -0,0 +1,56 @@ +from co3.components import Relation + +from setups import vegetables as veg + + +def test_mapper_getters(): + veg_comp = veg.vegetable_schema.get_component('vegetable') + tom_comp = veg.vegetable_schema.get_component('tomato') + + assert veg.vegetable_mapper.get_attribute_comp(veg.Vegetable) is veg_comp + assert veg.vegetable_mapper.get_attribute_comp(veg.Tomato) is tom_comp + + tom_aging = veg.vegetable_schema.get_component('tomato_aging_states') + tom_cooking = veg.vegetable_schema.get_component('tomato_cooking_states') + + assert veg.vegetable_mapper.get_collation_comp(veg.Tomato, 'aging') is tom_aging + assert veg.vegetable_mapper.get_collation_comp(veg.Tomato, 'cooking') is tom_cooking + +def test_mapper_attach(): + assert veg.vegetable_mapper.attach( + veg.Tomato, + 'tomato', + coll_groups={ + 'aging': 'tomato_aging_states', + 'cooking': 'tomato_cooking_states', + }, + ) is None + +def test_mapper_attach_many(): + assert veg.vegetable_mapper.attach_many( + [veg.Vegetable, veg.Tomato], + lambda t: f'{t.__name__.lower()}' + ) is None + +def test_mapper_collect(): + tomato = veg.Tomato('t1', 10) + receipts = veg.vegetable_mapper.collect(tomato) + + assert len(receipts) == 2 + + # attempt to retrieve receipts one at a time + res1 = veg.vegetable_mapper.collector.collect_inserts([receipts[0]]) + + assert len(res1) == 1 # should be just one match + assert len(res1[next(iter(res1.keys()))]) == 1 # and one dict for matching comp + + # try again, check no persistent match + res1 = veg.vegetable_mapper.collector.collect_inserts([receipts[0]]) + + assert len(res1) == 0 # should be no matches for the same receipt + + res2 = veg.vegetable_mapper.collector.collect_inserts([receipts[1]]) + + assert len(res2) == 1 + assert len(res2[next(iter(res2.keys()))]) == 1 + diff --git a/tests/test_schema.py b/tests/test_schema.py new file mode 100644 index 0000000..b116af9 --- /dev/null +++ b/tests/test_schema.py @@ -0,0 +1,26 @@ +from co3.components import Relation + +from setups import vegetables as veg + + +def test_schema_get(): + veg_comp_raw = veg.vegetable_schema._component_map.get('vegetable') + veg_comp = veg.vegetable_schema.get_component('vegetable') + + assert veg_comp_raw is veg_comp + +def test_schema_contains(): + vegetable_comp = veg.vegetable_schema.get_component('vegetable') + tomato_comp = veg.vegetable_schema.get_component('tomato') + tomato_aging_comp = veg.vegetable_schema.get_component('tomato_aging_states') + + assert vegetable_comp in veg.vegetable_schema + assert tomato_comp in veg.vegetable_schema + assert tomato_aging_comp in veg.vegetable_schema + +def test_schema_add(): + veg.vegetable_schema.add_component(Relation[int]('a', 1)) + veg.vegetable_schema.add_component(Relation[int]('b', 2)) + + assert veg.vegetable_schema.get_component('a') is not None + assert veg.vegetable_schema.get_component('b') is not None