From a448e26b82616c55a9837b961de52947738c3104 Mon Sep 17 00:00:00 2001 From: "Sam G." Date: Sat, 6 Apr 2024 18:30:06 -0700 Subject: [PATCH] add and integrate Component, Schema objects --- README.md | 16 +- .../co3/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 5065 bytes .../co3/__pycache__/accessor.cpython-312.pyc | Bin 0 -> 1868 bytes .../co3/__pycache__/co3.cpython-312.pyc | Bin 0 -> 4625 bytes .../co3/__pycache__/collector.cpython-312.pyc | Bin 0 -> 6025 bytes .../co3/__pycache__/component.cpython-312.pyc | Bin 0 -> 1355 bytes .../co3/__pycache__/composer.cpython-312.pyc | Bin 0 -> 4557 bytes .../co3/__pycache__/database.cpython-312.pyc | Bin 0 -> 4423 bytes .../co3/__pycache__/indexer.cpython-312.pyc | Bin 0 -> 15132 bytes .../co3/__pycache__/manager.cpython-312.pyc | Bin 0 -> 2370 bytes .../co3/__pycache__/mapper.cpython-312.pyc | Bin 0 -> 8185 bytes .../co3/__pycache__/schema.cpython-312.pyc | Bin 0 -> 2268 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 1612 bytes .../accessors/__pycache__/fts.cpython-312.pyc | Bin 0 -> 6191 bytes .../accessors/__pycache__/sql.cpython-312.pyc | Bin 0 -> 6069 bytes .../accessors/__pycache__/vss.cpython-312.pyc | Bin 0 -> 4289 bytes .../co3/component.py | 1 + .../co3/components/__init__.py | 1 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 4779 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 353 bytes .../databases/__pycache__/fts.cpython-312.pyc | Bin 0 -> 579 bytes .../databases/__pycache__/sql.cpython-312.pyc | Bin 0 -> 2284 bytes .../databases/__pycache__/vss.cpython-312.pyc | Bin 0 -> 579 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 363 bytes .../managers/__pycache__/fts.cpython-312.pyc | Bin 0 -> 10172 bytes .../managers/__pycache__/sql.cpython-312.pyc | Bin 0 -> 17989 bytes .../managers/__pycache__/vss.cpython-312.pyc | Bin 0 -> 6493 bytes .../co3/relation.py | 1 - .../co3/relations/__init__.py | 1 - .../co3/schema.py | 1 + .../co3/schemas/__init__.py | 1 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 1543 bytes .../util/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 268 bytes .../co3/util/__pycache__/db.cpython-312.pyc | Bin 0 -> 14870 bytes .../util/__pycache__/regex.cpython-312.pyc | Bin 0 -> 541 bytes .../util/__pycache__/types.cpython-312.pyc | Bin 0 -> 442 bytes .../co3/util/types.py | 1 + co3/__init__.py | 8 +- co3/accessor.py | 5 +- co3/accessors/sql.py | 98 ++++++--- co3/co3.py | 2 + co3/collector.py | 3 +- co3/component.py | 19 ++ co3/components/__init__.py | 79 +++++++ co3/composer.py | 4 +- co3/database.py | 6 +- co3/databases/sql.py | 18 +- co3/manager.py | 5 +- co3/managers/sql.py | 23 +- co3/mapper.py | 159 +++++++------- co3/mappers/__init__.py | 74 +++++++ co3/relation.py | 37 ---- co3/relations/__init__.py | 16 -- .../__pycache__/__init__.cpython-312.pyc | Bin 1269 -> 0 bytes co3/schema.py | 32 +++ co3/schemas/__init__.py | 21 ++ co3/util/types.py | 6 + .../mapper-checkpoint.ipynb | 145 +++++++++++++ .../vegetables-checkpoint.py | 80 +++++++ .../__pycache__/vegetables.cpython-312.pyc | Bin 0 -> 4049 bytes examples/mapper.ipynb | 200 ++++++++++++++++++ examples/vegetables.py | 80 +++++++ tests/co4_example.py | 17 +- 63 files changed, 952 insertions(+), 208 deletions(-) create mode 100644 build/__editable__.co3-0.1.1-py3-none-any/co3/__pycache__/__init__.cpython-312.pyc create mode 100644 build/__editable__.co3-0.1.1-py3-none-any/co3/__pycache__/accessor.cpython-312.pyc create mode 100644 build/__editable__.co3-0.1.1-py3-none-any/co3/__pycache__/co3.cpython-312.pyc create mode 100644 build/__editable__.co3-0.1.1-py3-none-any/co3/__pycache__/collector.cpython-312.pyc create mode 100644 build/__editable__.co3-0.1.1-py3-none-any/co3/__pycache__/component.cpython-312.pyc create mode 100644 build/__editable__.co3-0.1.1-py3-none-any/co3/__pycache__/composer.cpython-312.pyc create mode 100644 build/__editable__.co3-0.1.1-py3-none-any/co3/__pycache__/database.cpython-312.pyc create mode 100644 build/__editable__.co3-0.1.1-py3-none-any/co3/__pycache__/indexer.cpython-312.pyc create mode 100644 build/__editable__.co3-0.1.1-py3-none-any/co3/__pycache__/manager.cpython-312.pyc create mode 100644 build/__editable__.co3-0.1.1-py3-none-any/co3/__pycache__/mapper.cpython-312.pyc create mode 100644 build/__editable__.co3-0.1.1-py3-none-any/co3/__pycache__/schema.cpython-312.pyc create mode 100644 build/__editable__.co3-0.1.1-py3-none-any/co3/accessors/__pycache__/__init__.cpython-312.pyc create mode 100644 build/__editable__.co3-0.1.1-py3-none-any/co3/accessors/__pycache__/fts.cpython-312.pyc create mode 100644 build/__editable__.co3-0.1.1-py3-none-any/co3/accessors/__pycache__/sql.cpython-312.pyc create mode 100644 build/__editable__.co3-0.1.1-py3-none-any/co3/accessors/__pycache__/vss.cpython-312.pyc create mode 120000 build/__editable__.co3-0.1.1-py3-none-any/co3/component.py create mode 120000 build/__editable__.co3-0.1.1-py3-none-any/co3/components/__init__.py create mode 100644 build/__editable__.co3-0.1.1-py3-none-any/co3/components/__pycache__/__init__.cpython-312.pyc create mode 100644 build/__editable__.co3-0.1.1-py3-none-any/co3/databases/__pycache__/__init__.cpython-312.pyc create mode 100644 build/__editable__.co3-0.1.1-py3-none-any/co3/databases/__pycache__/fts.cpython-312.pyc create mode 100644 build/__editable__.co3-0.1.1-py3-none-any/co3/databases/__pycache__/sql.cpython-312.pyc create mode 100644 build/__editable__.co3-0.1.1-py3-none-any/co3/databases/__pycache__/vss.cpython-312.pyc create mode 100644 build/__editable__.co3-0.1.1-py3-none-any/co3/managers/__pycache__/__init__.cpython-312.pyc create mode 100644 build/__editable__.co3-0.1.1-py3-none-any/co3/managers/__pycache__/fts.cpython-312.pyc create mode 100644 build/__editable__.co3-0.1.1-py3-none-any/co3/managers/__pycache__/sql.cpython-312.pyc create mode 100644 build/__editable__.co3-0.1.1-py3-none-any/co3/managers/__pycache__/vss.cpython-312.pyc delete mode 120000 build/__editable__.co3-0.1.1-py3-none-any/co3/relation.py delete mode 120000 build/__editable__.co3-0.1.1-py3-none-any/co3/relations/__init__.py create mode 120000 build/__editable__.co3-0.1.1-py3-none-any/co3/schema.py create mode 120000 build/__editable__.co3-0.1.1-py3-none-any/co3/schemas/__init__.py create mode 100644 build/__editable__.co3-0.1.1-py3-none-any/co3/schemas/__pycache__/__init__.cpython-312.pyc create mode 100644 build/__editable__.co3-0.1.1-py3-none-any/co3/util/__pycache__/__init__.cpython-312.pyc create mode 100644 build/__editable__.co3-0.1.1-py3-none-any/co3/util/__pycache__/db.cpython-312.pyc create mode 100644 build/__editable__.co3-0.1.1-py3-none-any/co3/util/__pycache__/regex.cpython-312.pyc create mode 100644 build/__editable__.co3-0.1.1-py3-none-any/co3/util/__pycache__/types.cpython-312.pyc create mode 120000 build/__editable__.co3-0.1.1-py3-none-any/co3/util/types.py create mode 100644 co3/component.py create mode 100644 co3/components/__init__.py create mode 100644 co3/mappers/__init__.py delete mode 100644 co3/relation.py delete mode 100644 co3/relations/__init__.py delete mode 100644 co3/relations/__pycache__/__init__.cpython-312.pyc create mode 100644 co3/schema.py create mode 100644 co3/schemas/__init__.py create mode 100644 co3/util/types.py create mode 100644 examples/.ipynb_checkpoints/mapper-checkpoint.ipynb create mode 100644 examples/.ipynb_checkpoints/vegetables-checkpoint.py create mode 100644 examples/__pycache__/vegetables.cpython-312.pyc create mode 100644 examples/mapper.ipynb create mode 100644 examples/vegetables.py diff --git a/README.md b/README.md index fff4fd0..45c7486 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,21 @@ a known schema. - **Collector** to collect data for updating storage state - **Database** to collect data for updating storage state - **Mapper** to collect data for updating storage state -- **Relation** to collect data for updating storage state +- **Component** to collect data for updating storage state **CO3** is an abstract base class that makes it easy to integrate this model with object hierarchies that mirror a storage schema. + +# Detailed structural breakdown +There are a few pillars of the CO3 model that meaningfully group up functionality: + +- Database: generic to a Component type, provides basic connection to a database at a + specific address/location. The explicit Component type makes it easy to hook into + appropriately typed functional objects: + * Manager: generic to a Component and Database type, provides a supported set of + state-modifying operations to a constituent database + * Accessor: generic to a Component and Database type, provides a supported set of + state inspection operations on a constituent database + * Indexer: +- Mapper: generic to a Component, serves as the fundamental connective component between + types in the data representation hierarchy (CO3 subclasses) and database Components. diff --git a/build/__editable__.co3-0.1.1-py3-none-any/co3/__pycache__/__init__.cpython-312.pyc b/build/__editable__.co3-0.1.1-py3-none-any/co3/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..09e92407cc1988402c45b74a84744428a1db1cb2 GIT binary patch literal 5065 zcmai2TW=h<6`qx3OIlf$FG+n#iZV42Tfu78KyOMBK$YRvKog^|ffkDbJKP!W?nIp7 zfSi%MLH|cz`Um=3+Fu|*?L*OrzO@MYlBa&>kaLj(xWa0|;gE;t@}2J-?q7CyhY|jM z@~6G|n|o384|-_+*23VszhdymD2*mjI!M>_AYYrT4QRZs*Yl0ZMt);*gP%9_&3tpR z$>SS(E8m`M^Z2G7<~x%e9&hSf`R-(w$6NY#zBk$9@wUE`-<{m$@lfB(?@#XYct?Mb ze>nM&$G7y4@&}U#Jl@6l;p8EYZ)5yu@`%TK7(bpo=J6ejKbm~RhCtm;N*C#qzs(f`oYg6IqL5ft( zvm&c9Q#jfrlSDaZtn+)%RgtM8Q6g8>(xgtzjTL@PRxCu?oUDqExSL&=1Sy34gd#hr%_gPN&{9g5$fspo6_%)!WiI zXht@-8Y3InnQkPslII-n#wW`xO;s@#FCbUFm3ax%hpb+k7>E2wQENQhP9z3emaFy| z_@hUDj_G9|mrGM4Wl6L66t7M~;)Nx^Q8yDDJQ7y9QY95TX~beFJqR%*C(YnP4&Jqh z;DAmPK#5#M2GrQ-V+5B{#A5O<{7qvP?eQ@MnsquAGbKtb6TrmOmCI7)!`&mDMqD_; zYh&z6Jj?RZSRjnWGJKq&oFmuk>U?toEES*c8r6Vz`EAdizSQ$}TH3t$e02FpzFCnm3=D@nhD#xOOTa#s`Q#cQ) z0GgusABdN<8ygb%OdxN7Cs%oXV2PsZ;rG?#87C7NGW4cNF`$G z07-H<0;+vJJJe(#7yiHo*UM6r7-CYJEtXY4ITe^JL9w;In8Ru(zNuY>*m;0+>CQYC zlKwf;t^hzu2WJFPx>w>1%uG-&763C^fecwk3zMZWj!XuQJcxn8%CZw2SWKq` z@to^n@Xbk@a!Wy0j6{I~D3<2pI(Pd5UaiW;SXEKM&$E=yy#UiR9S5I?EJCG6 z*+yaZ%}yxlF_nc_bOOe`Yao(@yM5{ea{sanbr-8kYsJ` zZpgMWWp3bV2F&$>!A%lezO&VXPj)gJbL)$6gyd)c5<7}e7djC(pW)LVWPzJPxr;H@;H+8LTe z?yfLiSUKY@s5n4AusNrKNvJ?39eCO$H=#aKhl4fX#WvHgb$zTT=O=SF z2oqg3NR>ON>s7LCzhCuQZqCns6UxMv7<4I)X^RgyAsSt1YP)?!zsBrf`VOT|fDdu4 zK-qM@p+TG}@=L@Zbk*oXS*%lV8!#TXMGqA{$Fb*%QXwX?M9&3X3NWyX!AcZ}(6f%f zqi7qOj<#9Fl4%|J)8mQ-;r7G#!Lc~SJWq6eIWu9wR_`m5s~+_7E;8d##?QmE>DCBj)= zEXksaj`DS_3l#+OMNKeSvToE-bnRWJ{oJkBTxY2xpvKTk{-G~Ip_kNedl2@`#Sytv z8o=R3!`q2l8DHik?z@F=GkZ`6#adWJ>4jP=y>gUxp~;2#P%Ov8f6?zI`-97^lW;ZJ zU%OmCeRXuXnHa5c2;Oa-W16(=jLxcL>imqrj0 zfK1b``F9DLZgE5p%})&u4_ghQFdlXRz|*F`qSK;{8gc)6|Hqe`(`jmw>GbkeVvgF% zVAo+CnQ}D7zumx-y+KpEekzz{Z_*s^!yIXM(bpexV|D zkH$G$Zg*%R92Xp5Z?%iOC3cr4)=90sj1&{hv=uiL6qDfSioqrKo%bz&T3p}P)ZegHTv-dS87Zj{A~RB`16Bu zb#zdGf(~S{;?=#5;QOtgds_L@KEas-{JSsl_3!n;VDO){-wy^G|NX;SG?aWMKSn5#%m4rY literal 0 HcmV?d00001 diff --git a/build/__editable__.co3-0.1.1-py3-none-any/co3/__pycache__/accessor.cpython-312.pyc b/build/__editable__.co3-0.1.1-py3-none-any/co3/__pycache__/accessor.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ad10a815d363a9f2d81088dc3859e85c79f7611f GIT binary patch literal 1868 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 tYhhh literal 0 HcmV?d00001 diff --git a/build/__editable__.co3-0.1.1-py3-none-any/co3/__pycache__/collector.cpython-312.pyc b/build/__editable__.co3-0.1.1-py3-none-any/co3/__pycache__/collector.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..407a99145e1e2308eba80e0ab808f9fee45307f7 GIT binary patch literal 6025 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 Q9~yD?eDedGzhT6`0j+@3_5c6? literal 0 HcmV?d00001 diff --git a/build/__editable__.co3-0.1.1-py3-none-any/co3/__pycache__/database.cpython-312.pyc b/build/__editable__.co3-0.1.1-py3-none-any/co3/__pycache__/database.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c089b45d006faf66d680783dcb9dd9412d5674e7 GIT binary patch literal 4423 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 T<9{C{>4V>s$-89Y?qcOXy{{qX literal 0 HcmV?d00001 diff --git a/build/__editable__.co3-0.1.1-py3-none-any/co3/accessors/__pycache__/fts.cpython-312.pyc b/build/__editable__.co3-0.1.1-py3-none-any/co3/accessors/__pycache__/fts.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4b6455e326222be2dabf3bcc5f5e055af7db950c GIT binary patch literal 6191 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 literal 0 HcmV?d00001 diff --git a/build/__editable__.co3-0.1.1-py3-none-any/co3/accessors/__pycache__/sql.cpython-312.pyc b/build/__editable__.co3-0.1.1-py3-none-any/co3/accessors/__pycache__/sql.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5be8d248a331eba33d956fb1d6f09531a9e35fc0 GIT binary patch literal 6069 zcmb6dZA=`;b$9O$jy>QoXABtE9)HDL3`d-#X-u%?)Yy*OggC)%tZSoVZ+Grk*tzHqDs*2|Fbo7X`3W{gdcuutPIwF0)qv~kdl%i8;T|8l%yi# zIA8^G!DLX3gH|XRg1L~@kZVjfR_4OVCYWom)LbMP5p#`JbFL-XBF15>H5W}rb8X2s zSyCjUX->LPL7LpANM|HNos*0R-A3Dt=8Vz}Yc~QOK(f6QjJIfWAxTn)DL1ouQebw$ z=9)D~E^5rw(iSCYO`oKBgRAO`c}fZzb4w^@Q9eY#z=4A`k<%{=W>Gd6=Q+|lwn?E`FPK1;^< zl%-kvIL(#D2(06`>CiDmY0wPp6Et!yM@f!4(xrI`k?iw+z{G!>YDROyz+IZ0bZ zT3|p|f}C{@alsD9hNIw4$SL3fUPTZ@bd6I^rpMv3ByBt6aCt-qT*zr9lBOy!BvZ8d zNE&b<@p*fin1Gfec5WITZ3ewaExRCi!*#nr6J&JUySYPPjS-*^dVC47-lvY+)40$_ zrU93k9S8F5JRZn^yIioEj8(WlMl_Z!iqq|Ug@2BzIi{WOBa@UCkc|^X4p$qeYTX+U zdI_Euq=0F#g7`Jy7u?r|x8~Hr>sD~|^8(dP;E)gzKB%f=V`GJq>M<41^q$Cn-rkp~ zB`DA=e=0sm_6xpg76%sR0VA*~!|WX3G^kn;)7xm z7vM{+5dm`HW80d5FLT)caZ*+MkCE?HNjg*$*h7Ot%J?%-n5KdaNOQS}UOFC&<@@B~SsY%}RZ!;5iMf5quLZ zZC?a41cILhi!)6R8J?!(DTpEfg5^NN0)mRzrkPg2aj}q4KS8S;m)+nwP#*XVp9K|0 z6FdSRAzcHDg{wu!wBCVLUz*~I8}@a>4a2q0;~w2J@m`N!C;)%!7odAhc`bmR$dKox zUn`g7s{unX0^WE=$|zSA^cwTZd?2G3!MU&zx}y9_osk{yEa#wo^Z2C^7L6Tx->495cr5sYwErcI|jg}?lj zhbc(ZmcNQy!_du2cU$)^hxZC*jksY}aR9o{D}a%{h)~uAK=DSl6Mon(=-i-gTigu^ zAM8d9lRIW!cT#rV4ciRdVJcm6LzbB{9gc|5kV5lr(NMWT_ZBzsmY#+!9OJ=R=|Q^` z-RINqhI5|r;BGVEvx){yAt5@-pWpD-LSYtRuppJC3B2B>R+h_3IZzIkAyRx-xl!3a z(@+kv-41HGXypbYAY#4B2}WRY7&y@4&wH!zB_mWu8I|hy-q^S+2fG8<9TY$e;@ORB{>``d{_Mro*p4@* zu06H5_uA1#eQEFY%yR6&a`ST`@Q^a{6?< z$qlDcXu(q{S52jIwoya_uYdJ3%_JAyQ!9pypj2sv>=(E9nKLlrR06M#wJz#wqVkcZXfIC}ziiYp;U z{yi)GqUX<#oLKDnOxh!FU&ro`y|<3s(r!KZhs3A&`LN9lz(#EAPeE6+n4T3hLSQZB zWWcwHm03^=VM&8v4?`t6-sna!5~%oy5gkpm-%zy>$NKxw)%&lBdQ(|}o<~HvkpLnUErtL!ka>5H__5k!)_9bXxrH2{g`|8aU~SI)mW|zFY6Il@XA? zNZQOB6;Tx$8V|c6j54^Z7-_eKYbkn(>M4k)9qzXIpH+JnHY!fgZJ>EfyIh59ZqT8Z zoOpw0c+nlOuVGg+11gw-R3*Pc9-x)2*jb?|mv_`XOr3vt1z`DE=)l6ZN;`TME?m2C zr{nPLj>GfnUm~%)-MioFy54oC`|;b|kI#SOUbK7hz+3&-`|s>b+}@e^o$~v}-!$F~ z{I>bl;bo)nespR%JS8N{4T)Gn$C$)#C=5aX{s9aGdmNbE?o?PQNn1UppLx#&oT@lX zZ1KQbJ?u^yYR^A`5z3}k4*0?`qn4HLKlTM@DL;}XFtFLw zz66fo_qUbR3G{NCS~*xpNk~AU)!Wp{p*kq=Uc#&4iJhAO;j`ROg9U|Z0)@h8U{for z7BB;*{_8*GLt<>;yBPn|qZrizr6h?do`upnZ7h*B>?CJpgD zhNJzWGGV7Na3zpk(dtoS5W?$|DFM|RAB#h#>0=p&8UhvhDr9&LloKHJuuQBLR6!7Q z}<9wR%i~oF9kcfv0DX-1q>3l8tBkH^P*snw)d2lW3KTfy#q9 zXXK=v!!)09D0u)6sAfP8glM=|gn&ARe=qzsz^890U-;&>?*8Yh{-TzOH=O5FL2w@eLa_~=zlPKcKV;7|x3J?NzhGS%NFfHB>?n4F*gb>Ye(W&fawCf$ivS{WI|ie8#Iw-P;%qHiBluZ^tXge$0u~^~cXAGoz_3QV7aQ;r7g~uubtg7-J2rIlTPv}X^TQuT zb}UEst;RYRj$J#p)Uy)n$GO;YWRE}BwGw+2=i1&la`h-q4qh8vq${xl^TVDOIeh)_ zl5?}~{pgwH@EI>Y3}8&ySPc^4qk$cUTe3dv`mqyHAKGp2X+)M-OAs;JVc2~e{y4tn zoU|5}I=WZ4?O5&DxgLpxn7l?dMR=>75b*7~A4`OulRu1hJ_zB!3*GRn%N_6l)zlQm z=g}^`8jrb+sgz;s5Rf(&pvYjEX@+*sje?Y7Eer~LJfWp^hTh9<1Oo>}I>&9w6d^@| zkd-rZiLP2JNO|HIfc%@GpL3gGlt9J7k}Sjc#cjnY9Zg0a>op?lXM!PU-^JxOrOls* zhrGc0I6DU*=yLff=+*+VEZ{pbJy literal 0 HcmV?d00001 diff --git a/build/__editable__.co3-0.1.1-py3-none-any/co3/accessors/__pycache__/vss.cpython-312.pyc b/build/__editable__.co3-0.1.1-py3-none-any/co3/accessors/__pycache__/vss.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6a4221e4e67ca8ca7a9fdce87aa7e31fa7a923bb GIT binary patch literal 4289 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 G3RwUppJMs| literal 0 HcmV?d00001 diff --git a/build/__editable__.co3-0.1.1-py3-none-any/co3/databases/__pycache__/fts.cpython-312.pyc b/build/__editable__.co3-0.1.1-py3-none-any/co3/databases/__pycache__/fts.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6428624dbd045977e6c905624a51ae5e53d1ddb3 GIT binary patch literal 579 zcmXv~Jx?Ps5FPJF5`~0-?i$k3u|;-4vFAn^m}_zj3( zg08y?twe>cxDF}MP%&QbPBDIO?0MrifBO9{VEo-9=g)Y)S<*yUgT*d_2jIX-4w9sV zSnTDV^i#jWejdm$4GDN~1U$F{9t!g0yZ*E_Z$<6@mPDlN9Iz=nV@4oeIy^nu&oW_* z(nz~Vk6FRSLT9ekWm1#HPXrGjASE19PxxsdLR2S_S4lO>zoT~9p|oI9P+E2=m5R@D zi+hw_&sgqmbbxYYht(g@)^6FB?gNjouv5P-k_#n8V&qsS2P&IMQA|xT(dtTM2vjju zxf&;#+D^`9mpM-;75ow%+IcaSN1O3hyfvE4w?~C4#E2F1>NT&|V3PBxi6?X2#&K0@ zAuv5mcknV8-u!wQtlb{z<>uPD>!EPmC}_Lqa@Cd^FD6D&=WoT;M1& literal 0 HcmV?d00001 diff --git a/build/__editable__.co3-0.1.1-py3-none-any/co3/databases/__pycache__/sql.cpython-312.pyc b/build/__editable__.co3-0.1.1-py3-none-any/co3/databases/__pycache__/sql.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7063d4b45c62d3d5cba88a7b5b43c706af9ceb94 GIT binary patch literal 2284 zcmaJ?&2JM&6rWjtZ5+n|Qoh@=s32;?3gy)IW_J@CR7Tpj^YPxiuity~ zaAc%NVC0^b8((Dz`2!c}$S0HDGED9go7mJP9@Qzux#B8bM$gE3#?5*;J?G{1yj*A9 zf~V@LH>3~Ab#{ZBQ(^QK@GCE9V3xn}4DzUiK|EG9(2qv6>L=hFqSY~Ny; zHhr_ncvJ+AWDX2!$`Glrf61J8nT0CTfnh;|dLIJ0NpC7~XqL#&$`|yO(xA5}y0A=_ zm4;#~ORAmun*LP0LBjL~a-H1E#*0}}$wu_zfC36fWAOFLK<@U5&5~6*Q=uGFwn}hLg2Bw@S40n8CZ)R^FvW@@!&IN!6fj*zzv#pD>zl3Uvyl%ZWzpVAYd*tj2diL zPt?Y1>^o0_r3ghKax^bjbJTzT3~Y!*sxb#5dC8 zaB>IY!-!68qAQoleWGVR0U+sFmScHX(}sd6)(cD>+`(NoWGfT+FAney1Mk32!kaJy&r@*>`U_7o}ygH{jLJJzP%p6KyN%6FW$xZI7bMm%#P2bkqOdQu@3AjbH znB_Da2HF?Q5D3#gH@Sm(Lk&(SK%q9zz$7$H3j@uJl?Gfh<*k+rjvd#y3Bnw&@qs1r z55)|}7oq7}%&18INKGRX6_U`*R7#O*7{2KNTB4$1c!9l$$cajZac$9blao=yKv#{H z$xTndaltU`z%mSuIN=#2JAp)5WQ%~^^!*@|r3&m^MTT8Kp#Dm=zN;kQgaRj>4&Q-X zZ=^)R@B*x!-vlK7t>kj*lM*@gF_3TQsJ67wK>%V5F z*H3=5Hu>P-+En-C`OXw_99p@wI=*tbJ67$MYmc($C8A?R2<1v98mTpz&$we5*l76M zXam-RK+m)@XQXK_j!q<8nv4Sr8JAet0zeAJ(_8~V=>cN*-wyd<{{~U%H0??HzYyU!%Z4fdWJ1bUAmdk=J>r1L1yd1!yHU&Ij|4!pJQ&IVD5aD}} zpeuYIk^&M07#~6+kwj&h1gc7)a|q9nNc+%QfX@2pzRtPz(Y;?z#h&0f_=hm7WMpXg z0hq^^$#iQ;y5ISqbobeNnjZ#PsSZ7T-XY%}+aL>haU87^KEq!Ng3 zg-8aa3`(4kZN(+?a$H`@%@Ong$$t1m4#*N&&#JM{xIa_b7UiMxpfkvNjY{x1)a>LH z;3yGo$E~zUh#EW$aeZU>qiuMY^o{sTRy8IhD&v_IcrAEg;C+%WSPUUq&N4C5!a=KF zs1tF+InAqZ4W?3@0P-Y5DgBM?{*xShOg{LXOgtvljXajm#x4lu(;}rqx38@1>FzxG JjKExa`5)Vu9CH8w literal 0 HcmV?d00001 diff --git a/build/__editable__.co3-0.1.1-py3-none-any/co3/databases/__pycache__/vss.cpython-312.pyc b/build/__editable__.co3-0.1.1-py3-none-any/co3/databases/__pycache__/vss.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..825111d455a76fe5c101212a93a44bc38b1c00d8 GIT binary patch literal 579 zcmXv~u};G<5IrYNTa}iAV4)iu8Jd7t5fTy-EU+LkWU)+a3Mh_?-6C}Zi4S1o8xVg2 zKY$`JAqF<2!otKkPHs5AclN#WyVqW?0~maVoWJ1tVM!f91?Ia59)SZV8Ay^4VzHNb z(og&n`&mNJJmDt|5uiE=y;7=HJ`P(&o6?*~L21#UR4P8rEbdZz zJ!P4@(G8R<+N}D3wswn_bRT$vxt;oV5nm`NVk1X7-dE{VihN??u~t_iMWFJD%G4-M z)pmR~z07z_soH6OCuOh2zR(f$3tp zhqwOV_VBI0c6Xo`>uc+-i^5T@pzWTEWm~Gfm8AB8Duw%q{@Ock jQM-BtsolC6VEXn6A@8vM3E?|z{4_l>c-Z&_Jj&jG&T^C4 literal 0 HcmV?d00001 diff --git a/build/__editable__.co3-0.1.1-py3-none-any/co3/managers/__pycache__/__init__.cpython-312.pyc b/build/__editable__.co3-0.1.1-py3-none-any/co3/managers/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f27bd710a5b1338744c2f8bd71f63748821cb669 GIT binary patch literal 363 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 F{|D6)n(Y7p literal 0 HcmV?d00001 diff --git a/build/__editable__.co3-0.1.1-py3-none-any/co3/managers/__pycache__/vss.cpython-312.pyc b/build/__editable__.co3-0.1.1-py3-none-any/co3/managers/__pycache__/vss.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a8d2a6395a058427bb4cce15c0d07880cf55c5ba GIT binary patch literal 6493 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 Q5rloF@t@#p34Ia%3$+M}9RL6T literal 0 HcmV?d00001 diff --git a/build/__editable__.co3-0.1.1-py3-none-any/co3/relation.py b/build/__editable__.co3-0.1.1-py3-none-any/co3/relation.py deleted file mode 120000 index 0b07dfb..0000000 --- a/build/__editable__.co3-0.1.1-py3-none-any/co3/relation.py +++ /dev/null @@ -1 +0,0 @@ -/home/smgr/Documents/projects/ontolog/co3/co3/relation.py \ No newline at end of file diff --git a/build/__editable__.co3-0.1.1-py3-none-any/co3/relations/__init__.py b/build/__editable__.co3-0.1.1-py3-none-any/co3/relations/__init__.py deleted file mode 120000 index 25617d3..0000000 --- a/build/__editable__.co3-0.1.1-py3-none-any/co3/relations/__init__.py +++ /dev/null @@ -1 +0,0 @@ -/home/smgr/Documents/projects/ontolog/co3/co3/relations/__init__.py \ No newline at end of file diff --git a/build/__editable__.co3-0.1.1-py3-none-any/co3/schema.py b/build/__editable__.co3-0.1.1-py3-none-any/co3/schema.py new file mode 120000 index 0000000..4b513f9 --- /dev/null +++ b/build/__editable__.co3-0.1.1-py3-none-any/co3/schema.py @@ -0,0 +1 @@ +/home/smgr/Documents/projects/ontolog/co3/co3/schema.py \ No newline at end of file diff --git a/build/__editable__.co3-0.1.1-py3-none-any/co3/schemas/__init__.py b/build/__editable__.co3-0.1.1-py3-none-any/co3/schemas/__init__.py new file mode 120000 index 0000000..3381267 --- /dev/null +++ b/build/__editable__.co3-0.1.1-py3-none-any/co3/schemas/__init__.py @@ -0,0 +1 @@ +/home/smgr/Documents/projects/ontolog/co3/co3/schemas/__init__.py \ No newline at end of file diff --git a/build/__editable__.co3-0.1.1-py3-none-any/co3/schemas/__pycache__/__init__.cpython-312.pyc b/build/__editable__.co3-0.1.1-py3-none-any/co3/schemas/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..387aa6ef790f0e4dd9d75555eee0adbdf643a976 GIT binary patch literal 1543 zcmZWp&u<$=6n-;nuh))~y0j*yQWXnvh+EliRKY2#I0O(9L?T8)+6pb3-Em^GKiru$ zs;Pr=u+)G!fDrh?A(fCIia`7kNF2BjA=GMB1gPS`E!0G}UP&!B%_OD4@+k5-Joq)5xDMbudYx=RlYB&F?j71BE*%)I z_96W?w?(2P7O@YKO`q(i5?2E%shF&tNP*(O8GJ@_2t#-j+<-l@S|MC=*n<%w9KT!N z41HD?{synFgihCIK_u!O9=4f-Mi@k)7jD#@@N#{<+w$DHWihuE$#Yp&4c*mCwWZop zwbQ#?4Z?s`?VzVt3#Ev_^{t>4;o44*XECD!X$VPU8@^op)PIU=9Fbcy$-WF zHn)9YSy;(@?J(wJ^ zKB=9|C&%aogh$FT%_S_=R!yYlr_iK$*;3Q%ulF<*GK<$Yku^4a7TK;H*$$n?0Ho2D zsYV|l46%p$(+=9AebRnx>BMYLs7)OBo}^uYw@Uz9S|s;Xqdn>Bi38t#txxyIEC_sj zE(Lws06v3L$XQqSQpEauw%28XXV4wznmqI^HG@ku#}iyC8XK)ZM0Vh?IGu#aI+k&$ zLdCNxY;yAV=cjViPE5#~E0z(q;ms)nnT3_pz<@uwun299SO&lUnXY=&-3g&iBO zViFBT96a3@_jqxNaEBpfPAe&4qBA^n4{08&)VE!*S|2tG4=yXz{V_}d?z)B z16MWWRnV>8KjqnB+F&V$Xl%$x2os8Tc1rSjCwJ(y>|Gn=BQ}H6KK8N;_^KNM^bsw# zl0K02V62i=rIm@>+Ulwjv@TX+E!IUhmW4;Ji>)6ob~;D0@g|bkjpymTLp50a5N323 TsQWp^PX+)-w)|wb;hN@ODQiUZ literal 0 HcmV?d00001 diff --git a/build/__editable__.co3-0.1.1-py3-none-any/co3/util/__pycache__/db.cpython-312.pyc b/build/__editable__.co3-0.1.1-py3-none-any/co3/util/__pycache__/db.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5d41991200625d471e5fa330d911df3b35984763 GIT binary patch literal 14870 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 QR;P>yZH_$N{58gZ11oN39{>OV literal 0 HcmV?d00001 diff --git a/build/__editable__.co3-0.1.1-py3-none-any/co3/util/__pycache__/regex.cpython-312.pyc b/build/__editable__.co3-0.1.1-py3-none-any/co3/util/__pycache__/regex.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..be0da3384e98d9a7c52b3270eb2c14f9f92d5667 GIT binary patch literal 541 zcmYjOze~eF6n>Y)pFu^`O|)(XNn@-y`2)qNgNs{5Lz_Hm>m^q%30iS*a2A~kPOj2F z#ii0gh;B}9LI=UgyO^pE?t9;R_wK##?jFa+MnTx!^(S5n|0aXJsTt|53DN=&JVK!a z3Lw;#e$k)zls5T^N{XjmK}l^ALwlF0z4M91t@VW?V{vQaVB0Jm8g5pa9?%5JVZEY& z7DQ=MLcK5HHv>Y%1+=sA^?`pI8Et2N9}HzLA&#LYlWy5Zx)KKE6LdIM@hnlpxRMM| zc8c5}i5B5m6iP|CFSL%Ck5=ecxwXsO*vB9Wtp;axbOm8S#3-v;E-P4-xJEt8anP$p zT4t)ANmHGM;L3BIcyBxcr7evBEh4m+-33 zKI^$BJ$FBK?{)R1XWf3%?XF(vXoa4r07FX@j>(YA{6>u;moJevc>u0UJ|f_2c-?1S tOmUM>h)`ZIToCY~5<*^JvXjODI6ZJK4lPG--T44o(d2&Md=ohtXaJLy+xDO{@NKeM6=Q>Dp6 zd}y)#wAi$4g7>^*Ea<#VlfbR2X(8sJ7qpoy>}e_L5sNq-?DJHLtTaDqN!5eI?j@M< zG`B;&faQeu7L2kDV~je~$0+jK3~f*+@o^AGC}gyY9RX+%Q$pLwp$%jw9&wRrV%Lka zQ%utkw7z20jk+H4h91xW>sG)g?j-gI635}l2>DoD^{JZ>yw2vuRw!PC#P=mb2tUBm r5G;NdfVObHd&#c*SN(_Or`ls}P+5DHUX>SRP}zJhZ=DsHCVGAVhO=*n literal 0 HcmV?d00001 diff --git a/build/__editable__.co3-0.1.1-py3-none-any/co3/util/types.py b/build/__editable__.co3-0.1.1-py3-none-any/co3/util/types.py new file mode 120000 index 0000000..df0d193 --- /dev/null +++ b/build/__editable__.co3-0.1.1-py3-none-any/co3/util/types.py @@ -0,0 +1 @@ +/home/smgr/Documents/projects/ontolog/co3/co3/util/types.py \ No newline at end of file diff --git a/co3/__init__.py b/co3/__init__.py index 0b6fc93..4e9c770 100644 --- a/co3/__init__.py +++ b/co3/__init__.py @@ -93,17 +93,19 @@ Note: Organization for inheritance over composition ''' from co3.accessor import Accessor -from co3.co3 import CO3 +from co3.co3 import CO3, collate from co3.collector import Collector from co3.composer import Composer from co3.database import Database from co3.indexer import Indexer from co3.manager import Manager from co3.mapper import Mapper -from co3.relation import Relation +from co3.component import Component +from co3.schema import Schema from co3 import accessors from co3 import databases from co3 import managers -from co3 import relations +from co3 import components +from co3 import schemas from co3 import util diff --git a/co3/accessor.py b/co3/accessor.py index ef69d42..c4b98ad 100644 --- a/co3/accessor.py +++ b/co3/accessor.py @@ -8,13 +8,14 @@ schema-specific queries. import inspect from pathlib import Path from collections import defaultdict +from abc import ABCMeta, abstractmethod import sqlalchemy as sa -#from co3.database import Database +from co3.component import Component -class Accessor[D: 'Database']: +class Accessor[C: Component, D: 'Database[C]'](metaclass=ABCMeta): ''' Access wrapper class for complex queries and easy integration with Composer tables. Implements high-level access to things like common constrained SELECT queries. diff --git a/co3/accessors/sql.py b/co3/accessors/sql.py index 1ee0fea..41ce839 100644 --- a/co3/accessors/sql.py +++ b/co3/accessors/sql.py @@ -1,3 +1,42 @@ +''' +Design proposal: variable backends + +One particular feature not supported by the current type hierarchy is the possible use of +different backends to implement a general interface like SQLAccessor. One could imagine, +for instance, using `sqlalchemy` or `sqlite` to define the same methods laid out in a +parent class blueprint. It's not too difficult to imagine cases where both of these may be +useful, but for now it is outside the development scope. Should it ever enter the scope, +however, we might consider a simple `backend` argument on instantiation, keeping just the +SQLAccessor exposed rather than a whole set of backend-specific types: + +```py +class SQLAlchemyAccessor(RelationalAccessor): # may also inherit from a dedicated interface parent + def select(...): + ... + +class SQLiteAccessor(RelationalAccessor): + def select(...): + ... + +class SQLAccessor(RelationalAccessor): + backends = { + 'sqlalchemy': SQLAlchemyAccessor, + 'sqlite': SQLteAccessor, + } + + def __init__(self, backend: str): + self.backend = self.backends.get(backend) + + def select(...): + return self.backend.select(...) + +``` + +For now, we can look at SQLAccessor (and equivalents in other type hierarchies, like +SQLManagers) as being SQLAlchemyAccessors and not supporting any backend swapping. But in +theory, to make the above change, we'd just need to rename it and wrap it up. +''' + from pathlib import Path from collections.abc import Iterable import inspect @@ -7,21 +46,44 @@ import sqlalchemy as sa from co3 import util from co3.accessor import Accessor -from co3.relation import Relation - -#from co3.databases.sql import RelationalDatabase, TabularDatabase, SQLDatabase -from co3.relations import TabularRelation, SQLTable +from co3.components import Relation, SQLTable -class RelationalAccessor[D: 'RelationalDatabase', R: Relation](Accessor[D]): - pass +class RelationalAccessor[R: Relation, D: 'RelationalDatabase[R]'](Accessor[R, D]): + def raw_select(self, sql: str): + raise NotImplementedError + + def select( + self, + relation: R, + cols = None, + where = None, + distinct_on = None, + order_by = None, + limit = 0, + ): + raise NotImplementedError + + def select_one( + self, + relation : R, + cols = None, + where = None, + mappings : bool = False, + include_cols : bool = False, + ): + res = self.select(relation, cols, where, mappings, include_cols, limit=1) + + if include_cols and len(res[0]) > 0: + return res[0][0], res[1] + + if len(res) > 0: + return res[0] + + return None -class TabularAccessor[D: 'TabularDatabase', R: TabularRelation](RelationalAccessor[D, R]): - pass - - -class SQLAccessor(TabularAccessor['SQLDatabase', SQLTable]): +class SQLAccessor(RelationalAccessor[SQLTable, 'SQLDatabase[SQLTable]']): def raw_select( self, sql, @@ -37,7 +99,7 @@ class SQLAccessor(TabularAccessor['SQLDatabase', SQLTable]): def select( self, - table: sa.Table | sa.Subquery | sa.Join, + table: SQLTable, cols = None, where = None, distinct_on = None, @@ -82,15 +144,3 @@ class SQLAccessor(TabularAccessor['SQLDatabase', SQLTable]): stmt = stmt.limit(limit) return res_method(self.engine, stmt, include_cols=include_cols) - - def select_one(self, table, cols=None, where=None, mappings=False, include_cols=False): - res = self.select(table, cols, where, mappings, include_cols, limit=1) - - if include_cols and len(res[0]) > 0: - return res[0][0], res[1] - - if len(res) > 0: - return res[0] - - return None - diff --git a/co3/co3.py b/co3/co3.py index 0018d87..14e8098 100644 --- a/co3/co3.py +++ b/co3/co3.py @@ -29,6 +29,8 @@ logger = logging.getLogger(__name__) def collate(action_key, action_groups=None): def decorator(func): + nonlocal action_groups + if action_groups is None: action_groups = [None] func._action_data = (action_key, action_groups) diff --git a/co3/collector.py b/co3/collector.py index 3880504..305266d 100644 --- a/co3/collector.py +++ b/co3/collector.py @@ -29,12 +29,13 @@ from uuid import uuid4 import sqlalchemy as sa from co3 import util +from co3.component import Component #from localsys.db.schema import tables logger = logging.getLogger(__name__) -class Collector: +class Collector[C: Component, M: 'Mapper[C]']: def __init__(self): self._inserts = defaultdict(lambda: defaultdict(list)) diff --git a/co3/component.py b/co3/component.py new file mode 100644 index 0000000..a648434 --- /dev/null +++ b/co3/component.py @@ -0,0 +1,19 @@ +''' +Component + +General wrapper for storage components to be used in various database contexts. Relations +can be thought of generally as named data containers/entities serving as a fundamental +abstractions within particular storage protocols. +''' + +class Component[T]: + def __init__(self, name, obj: T, schema: 'Schema'): + self.name = name + self.obj = obj + + self.schema = schema + schema.add_component(self) + + def get_attributes(self): + raise NotImplementedError + diff --git a/co3/components/__init__.py b/co3/components/__init__.py new file mode 100644 index 0000000..413f37b --- /dev/null +++ b/co3/components/__init__.py @@ -0,0 +1,79 @@ +from typing import Self +from abc import ABCMeta, abstractmethod + +import sqlalchemy as sa + +from co3.util.types import TableLike +from co3.component import Component + + +class ComposableComponent[T](Component[T], metaclass=ABCMeta): + ''' + Components that can be composed with others of the same type. + ''' + @abstractmethod + def compose(self, component: Self, on) -> Self: + ''' + Abstract composition. + ''' + raise NotImplementedError + + +# relational databases +class Relation[T](ComposableComponent[T]): + ''' + Relation base for tabular components to be used in relation DB settings. Attempts to + adhere to the set-theoretic base outlined in the relational model [1]. Some + terminology: + + Relation: table-like container + | -> Heading: set of attributes + | | -> Attribute: column name + | -> Body: set of tuples with domain matching the heading + | | -> Tuple: collection of values + + + [1]: https://en.wikipedia.org/wiki/Relational_model#Set-theoretic_formulation + + Note: development tasks + As it stands, the Relation skeleton is incredibly lax compared to the properties and + operations that should be formally available, according its pure relational algebra + analog. + + Relations are also generic up to a type T, which ultimately serves as the base object + for Relation instances. We aren't attempting to implement some generally useful + table-like class here; instead we're just exposing a lightweight interface that's + needed for a few CO3 contexts, and commonly off-loading most of the heavy-lifting to + true relation objects like SQLAlchemy tables. + ''' + def compose( + self, + _with: Self, + on, + outer=False + ): + return self + +class SQLTable(Relation[TableLike]): + @classmethod + def from_table(cls, table: sa.Table, schema: 'SQLSchema'): + return cls(table.name, table, schema) + + def get_attributes(self): + return tuple(self.obj.columns) + + +# key-value stores +class Dictionary(Relation[dict]): + def get_attributes(self): + return tuple(self.obj.keys()) + + +# document databases +class Document[T](Component[T]): + pass + + +# graph databases +class Node[T](Component[T]): + pass diff --git a/co3/composer.py b/co3/composer.py index 6161518..4456cb5 100644 --- a/co3/composer.py +++ b/co3/composer.py @@ -26,7 +26,7 @@ class ExampleComposer(Composer): ''' from pathlib import Path -from co3.mapper import Mapper +from co3.component import Component def register_table(table_name=None): @@ -43,7 +43,7 @@ def register_table(table_name=None): return func return decorator -class Composer[M: Mapper]: +class Composer[C: Component, M: 'Mapper[C]']: ''' Base composer wrapper for table groupings. diff --git a/co3/database.py b/co3/database.py index 2b46052..7952296 100644 --- a/co3/database.py +++ b/co3/database.py @@ -21,9 +21,9 @@ from co3.indexer import Indexer logger = logging.getLogger(__name__) -class Database: - accessor: type[Accessor[Self]] = Accessor - manager: type[Manager[Self]] = Manager +class Database[C: Component]: + accessor: type[Accessor[C, Self]] = Accessor[C, Self] + manager: type[Manager[C, Self]] = Manager[C, Self] def __init__(self, resource): ''' diff --git a/co3/databases/sql.py b/co3/databases/sql.py index 9c4b08d..56a3f77 100644 --- a/co3/databases/sql.py +++ b/co3/databases/sql.py @@ -2,30 +2,24 @@ from typing import Self from co3.database import Database -from co3.accessors.sql import RelationalAccessor, TabularAccessor, SQLAccessor -from co3.managers.sql import RelationalManager, TabularManager, SQLManager +from co3.accessors.sql import RelationalAccessor, SQLAccessor +from co3.managers.sql import RelationalManager, SQLManager -from co3.relation import Relation -from co3.relations import TabularRelation, SQLTable +from co3.components import Relation, SQLTable class RelationalDatabase[R: Relation](Database): - accessor: type[RelationalAccessor[Self, R]] = RelationalAccessor[Self, R] - manager: type[RelationalManager[Self, R]] = RelationalManager[Self, R] - - -class TabularDatabase[R: TabularRelation](RelationalDatabase[R]): ''' accessor/manager assignments satisfy supertype's type settings; `TabluarAccessor[Self, R]` is of type `type[RelationalAccessor[Self, R]]` (and yes, `type[]` specifies that the variable is itself being set to a type or a class, rather than a satisfying _instance_) ''' - accessor: type[TabularAccessor[Self, R]] = TabularAccessor[Self, R] - manager: type[TabularManager[Self, R]] = TabularManager[Self, R] + accessor: type[RelationalAccessor[Self, R]] = RelationalAccessor[Self, R] + manager: type[RelationalManager[Self, R]] = RelationalManager[Self, R] -class SQLDatabase[R: SQLTable](TabularDatabase[R]): +class SQLDatabase[R: SQLTable](RelationalDatabase[R]): accessor = SQLAccessor manager = SQLManager diff --git a/co3/manager.py b/co3/manager.py index f8ced6d..e69270b 100644 --- a/co3/manager.py +++ b/co3/manager.py @@ -7,10 +7,11 @@ interacting with an underlying database, like inserts and schema recreation. from pathlib import Path from abc import ABCMeta, abstractmethod +from co3.schema import Schema #from co3.database import Database -class Manager[D: 'Database'](metaclass=ABCMeta): +class Manager[C: Component, D: 'Database[C]'](metaclass=ABCMeta): ''' Management wrapper class for table groupings. @@ -22,7 +23,7 @@ class Manager[D: 'Database'](metaclass=ABCMeta): self.database = database @abstractmethod - def recreate(self): + def recreate(self, schema: Schema[C]): raise NotImplementedError @abstractmethod diff --git a/co3/managers/sql.py b/co3/managers/sql.py index 43b676a..ceafa1a 100644 --- a/co3/managers/sql.py +++ b/co3/managers/sql.py @@ -48,26 +48,21 @@ import sqlalchemy as sa from tqdm.auto import tqdm from co3 import util - +from co3.schema import Schema from co3.manager import Manager -from co3.relation import Relation -#from co3.databases import SQLDatabase +from co3.components import Relation, SQLTable + #from localsys.reloader.router._base import ChainRouter, Event -from co3.relations import TabularRelation, SQLTable logger = logging.getLogger(__name__) -class RelationalManager[D: 'RelationalDatabase', R: Relation](Manager[D]): +class RelationalManager[R: Relation, D: 'RelationalDatabase[R]'](Manager[R, D]): pass -class TabularManager[D: 'TabularDatabase', R: TabularRelation](RelationalManager[D, R]): - pass - - -class SQLManager(TabularManager['SQLDatabase', SQLTable]): +class SQLManager(RelationalManager[SQLTable, 'SQLDatabase[SQLTable]']): ''' Core schema table manager. Exposes common operations and facilitates joint operations needed for highly connected schemas. @@ -80,8 +75,6 @@ class SQLManager(TabularManager['SQLDatabase', SQLTable]): saturates a router with events (dynamically) and sweeps up inserts on session basis from an attached collector. ''' - conversion_formats = ['src', 'html5'] - def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -99,9 +92,9 @@ class SQLManager(TabularManager['SQLDatabase', SQLTable]): def add_router(self, router): self.routers.append(router) - def recreate(self): - tables.metadata.drop_all(self.engine) - tables.metadata.create_all(self.engine, checkfirst=True) + def recreate(self, schema: Schema[SQLTable]): + schema.metadata.drop_all(self.engine) + schema.metadata.create_all(self.engine, checkfirst=True) def update(self): pass diff --git a/co3/mapper.py b/co3/mapper.py index 813a70e..eb1a587 100644 --- a/co3/mapper.py +++ b/co3/mapper.py @@ -18,27 +18,56 @@ mapper.attach( } ) ''' +from typing import Callable, Self from collections import defaultdict -from co3.co3 import CO3 -from co3.relation import Relation +from co3.co3 import CO3 +from co3.collector import Collector +from co3.composer import Composer +from co3.component import Component +from co3.schema import Schema -class Mapper[R: Relation]: +class Mapper[C: Component]: ''' Mapper base class for housing schema components and managing relationships between CO3 - types and storage targets (of type R). + types and storage components (of type C). ''' - def __init__(self): - self.attribute_comps: dict[CO3, R] = {} - self.collation_groups: dict[CO3, dict[str|None, R]] = defaultdict(dict) + _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 + + self.collector = self._collector_cls() + self.composer = self._composer_cls() + + self.attribute_comps: dict[type[CO3], C] = {} + self.collation_groups: dict[type[CO3], dict[str|None, C]] = defaultdict(dict) + + def _check_component(self, comp: C | str): + if type(comp) is str: + comp_key = comp + comp = self.schema.get_component(comp_key) + if comp is None: + raise ValueError( + f'Component key {comp_key} not available in attached schema' + ) + else: + if comp not in self.schema: + raise TypeError( + f'Component {comp} not registered to Mapper schema {self.schema}' + ) + + return comp def attach( self, - type_ref : CO3, - attr_comp : R, - coll_comp : R | None = None, - coll_groups : dict[str | None, R] = None + type_ref : type[CO3], + attr_comp : C | str, + coll_comp : C | str | None = None, + coll_groups : dict[str | None, C | str] = None ) -> None: ''' Parameters: @@ -49,24 +78,33 @@ class Mapper[R: Relation]: 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 + # check default component in registered schema if coll_comp is not None: - self.collation_groups[type_ref][None] = attr_comp + coll_comp = self._check_component(coll_comp) + self.collation_groups[type_ref][None] = coll_comp + # check if any component in group dict not in registered schema if coll_groups is not None: - self.collation_groups[type_ref].update(attr_comp) + for coll_key in coll_groups: + coll_groups[coll_key] = self._check_component(coll_groups[coll_key]) - def join_attribute_relations(self, r1: R, r2: R) -> R: - ''' - Specific mechanism for joining attribute-based relations. - ''' - pass + self.collation_groups[type_ref].update(coll_groups) - def join_collation_relations(self, r1: R, r2: R) -> R: - ''' - Specific mechanism for joining collation-based relations. - ''' + def attach_hierarchy( + self, + type_ref: type[CO3], + obj_name_map: Callable[[type[CO3]], str], + ): pass def get_connective_data( @@ -84,13 +122,13 @@ class Mapper[R: Relation]: ''' return {} - def get_attribute_comp(self, type_ref: CO3) -> R | None: + def get_attribute_comp(self, type_ref: CO3) -> C | None: return self.attribute_comps.get(type_ref, None) - def get_collation_comp(self, type_ref: CO3, group=str | None) -> R | 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 collect(self, collector, mapper, action_keys=None) -> dict: + def collect(self, obj, action_keys=None) -> dict: ''' Stages inserts up the inheritance chain, and down through components. @@ -105,38 +143,38 @@ class Mapper[R: Relation]: Returns: dict with keys and values relevant for associated SQLite tables ''' if action_keys is None: - action_keys = list(self.action_map.keys()) + action_keys = list(obj.action_map.keys()) receipts = [] - for _cls in reversed(self.__class__.__mro__[:-2]): - attribute_component = mapper.get_attribute_comp(_cls) + for _cls in reversed(obj.__class__.__mro__[:-2]): + attribute_component = self.get_attribute_comp(_cls) # require an attribute component for type consideration if attribute_component is None: continue - collector.add_insert( + self.collector.add_insert( attribute_component, - self.attributes, + obj.attributes, receipts=receipts, ) for action_key in action_keys: - collation_data = self.collate(action_key) + collation_data = obj.collate(action_key) # if method either returned no data or isn't registered, ignore if collation_data is None: continue - _, action_groups = self.action_map[action_key] + _, action_groups = obj.action_map[action_key] for action_group in action_groups: - collation_component = mapper.get_collation_comp(_cls, group=action_group) + collation_component = self.get_collation_comp(_cls, group=action_group) if collation_component is None: continue # gather connective data for collation components - connective_data = mapper.get_connective_data(self, action_key, action_group) + connective_data = self.get_connective_data(_cls, action_key, action_group) collector.add_insert( collation_component, @@ -153,56 +191,3 @@ class Mapper[R: Relation]: return receipts - @classmethod - def compose(cls, outer=False, conversion=False, full=False): - ''' - Note: - Comparing to ORM, this method would likely also still be needed, since it may - not be explicitly clear how some JOINs should be handled up the inheritance - 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 - ''' - def join_builder(outer=False, conversion=False): - head_table = None - last_table = None - join_table = None - - for _cls in reversed(cls.__mro__[:-2]): - table_str = None - table_prefix = _cls.table_prefix - - if conversion: table_str = f'{table_prefix}_conversions' - else: table_str = f'{table_prefix}s' - - if table_str not in tables.table_map: - continue - - table = tables.table_map[table_str] - - if join_table is None: - head_table = table - join_table = table - else: - if conversion: - join_condition = last_table.c.name_fmt == table.c.name_fmt - else: - join_condition = last_table.c.name == table.c.name - - join_table = join_table.join(table, join_condition, isouter=outer) - - last_table = table - - return join_table, head_table - - if full: - # note how the join isn't an OUTER join b/w the two - core, core_h = join_builder(outer=outer, conversion=False) - conv, conv_h = join_builder(outer=outer, conversion=True) - return core.join(conv, core_h.c.name == conv_h.c.name) - - join_table, _ = join_builder(outer=outer, conversion=conversion) - return join_table diff --git a/co3/mappers/__init__.py b/co3/mappers/__init__.py new file mode 100644 index 0000000..36a60bf --- /dev/null +++ b/co3/mappers/__init__.py @@ -0,0 +1,74 @@ +from typing import Self + +import sqlalchemy as sa + +from co3.mapper import Mapper +from co3.component import ComposableComponent + + +class ComposableMapper[C: ComposableComponent](Mapper[C]): + def join_attribute_relations(self, r1: C, r2: C) -> C: + ''' + Specific mechanism for joining attribute-based relations. + ''' + pass + + def join_collation_relations(self, r1: C, r2: C) -> C: + ''' + Specific mechanism for joining collation-based relations. + ''' + pass + + @classmethod + def compose(cls, outer=False, conversion=False, full=False): + ''' + Note: + Comparing to ORM, this method would likely also still be needed, since it may + not be explicitly clear how some JOINs should be handled up the inheritance + 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 + ''' + def join_builder(outer=False, conversion=False): + head_table = None + last_table = None + join_table = None + + for _cls in reversed(cls.__mro__[:-2]): + table_str = None + table_prefix = _cls.table_prefix + + if conversion: table_str = f'{table_prefix}_conversions' + else: table_str = f'{table_prefix}s' + + if table_str not in tables.table_map: + continue + + table = tables.table_map[table_str] + + if join_table is None: + head_table = table + join_table = table + else: + if conversion: + join_condition = last_table.c.name_fmt == table.c.name_fmt + else: + join_condition = last_table.c.name == table.c.name + + join_table = join_table.join(table, join_condition, isouter=outer) + + last_table = table + + return join_table, head_table + + if full: + # note how the join isn't an OUTER join b/w the two + core, core_h = join_builder(outer=outer, conversion=False) + conv, conv_h = join_builder(outer=outer, conversion=True) + return core.join(conv, core_h.c.name == conv_h.c.name) + + join_table, _ = join_builder(outer=outer, conversion=conversion) + return join_table diff --git a/co3/relation.py b/co3/relation.py deleted file mode 100644 index b2049ee..0000000 --- a/co3/relation.py +++ /dev/null @@ -1,37 +0,0 @@ -''' -Relation - -Loose wrapper for table-like objects to be used in various database contexts. Relations -can be thought of generally as named data containers that contain tuples of attributes -(adhering to relational algebra terms). Relation subtypes are referred to commonly across -CO3 generics, serving as a fundamental abstraction within particular storage protocols. - -Note: development tasks - As it stands, the Relation skeleton is incredibly lax compared to the properties and - operations that should be formally available, according its pure relational algebra - analog. - - Relations are also generic up to a type T, which ultimately serves as the base object - for Relation instances. We aren't attempting to implement some generally useful - table-like class here; instead we're just exposing a lightweight interface that's - needed for a few CO3 contexts, and commonly off-loading most of the heavy-lifting to - true relation objects like SQLAlchemy tables. -''' -from typing import Self - - -class Relation[T]: - def __init__(self, name, obj: T): - self.name = name - self.obj = obj - - def get_attributes(self): - raise NotImplementedError - - def join( - self, - corelation: Self, - on, - outer=False - ): - raise NotImplementedError diff --git a/co3/relations/__init__.py b/co3/relations/__init__.py deleted file mode 100644 index 5bc5ba3..0000000 --- a/co3/relations/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -import sqlalchemy as sa - -from co3.relation import Relation - - -class DictRelation(Relation[dict]): - def get_attributes(self): - return tuple(self.obj.keys()) - -class TabularRelation(Relation[sa.Table]): - def get_attributes(self): - return tuple(self.obj.columns) - - -class SQLTable(Relation[sa.Table]): - pass diff --git a/co3/relations/__pycache__/__init__.cpython-312.pyc b/co3/relations/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 3916fdebffff0cbb828e71f816187a26645d08ea..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1269 zcmb6YJx|*}^v-sOlLpccKGasKU`S{dqN+=W3RNno9jZtz3@8#h8T)_)JBB-3BB(=$ z3>`tO6quMJeo7Y*L#u1IszbL*Nf)NRX9oxt13l^9d-p!>eSaAq&Js8qPw47ACFCbQ z&d*Sv=za#GO&sD-msBZW&DA`;s#BtoSH#g<#4)(ejr*W?q{mj(R3uA*YzSnup6mZM zfUfALBmFe!n>W%oo3xbqjlWe&RGII%YXvpGVGsOIAfxDX?*eKQ9z|c(9NHpPy{VOS zm7A;C!G-O)J{lQtF_-RZ0BsUn$ljB>2CT%XrxFZ!9`#vAecJ`np%rkN3c^j7E8SnI zD`SHzOdCs>(k2eBY-V}bF+d%k@z<2$%vh6>OuIQ&O z%Z$|;wScji&88Yz;{mgRK-5;kfXkhSgPG1;6{f+hoB;5F{K${)F1DBUmfFjE%b(b3 zzHn?7x|m3%V7NuMchVjqX;gmIF3&QxUd6NoQ`!+a(xwX z@h)c{wnkIt!rM0xsLNp*#T}3fTo7>&LBj0=J4eMxch8>Zf~|nfb5wXo!(vF7l+#i+b83XkvZMVQkwa=y{{db2j)qB>MwzJ6!t%l CaQb=x diff --git a/co3/schema.py b/co3/schema.py new file mode 100644 index 0000000..317165a --- /dev/null +++ b/co3/schema.py @@ -0,0 +1,32 @@ +''' +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. + +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 +''' + +from co3.component import Component + + +class Schema[C: Component]: + def __init__(self): + self._component_set = set() + self._component_map = {} + + def __contains__(self, component: C): + return component in self._component_set + + def add_component(self, component: C): + self._component_set.add(component) + self._component_map[component.name] = component + + def get_component(self, name: str): + return self._component_map.get(name) diff --git a/co3/schemas/__init__.py b/co3/schemas/__init__.py new file mode 100644 index 0000000..be666a5 --- /dev/null +++ b/co3/schemas/__init__.py @@ -0,0 +1,21 @@ +from typing import Self + +import sqlalchemy as sa + +from co3.schema import Schema +from co3.components import Relation, SQLTable + + +class RelationalSchema[R: Relation](Schema[R]): + pass + +class SQLSchema(RelationalSchema[SQLTable]): + @classmethod + def from_metadata(cls, metadata: sa.MetaData): + instance = cls() + + for table in metadata.tables.values(): + SQLTable.from_table(table, instance) + + return instance + diff --git a/co3/util/types.py b/co3/util/types.py new file mode 100644 index 0000000..822d9bc --- /dev/null +++ b/co3/util/types.py @@ -0,0 +1,6 @@ +from typing import TypeVar + +import sqlalchemy as sa + + +TableLike = TypeVar('TableLike', bound=sa.Table | sa.Subquery | sa.Join) diff --git a/examples/.ipynb_checkpoints/mapper-checkpoint.ipynb b/examples/.ipynb_checkpoints/mapper-checkpoint.ipynb new file mode 100644 index 0000000..a519865 --- /dev/null +++ b/examples/.ipynb_checkpoints/mapper-checkpoint.ipynb @@ -0,0 +1,145 @@ +{ + "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 new file mode 100644 index 0000000..488864f --- /dev/null +++ b/examples/.ipynb_checkpoints/vegetables-checkpoint.py @@ -0,0 +1,80 @@ +import random + +import sqlalchemy as sa + +from co3.schemas import SQLSchema +from co3 import CO3, collate +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) + + @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) + } + +''' +VEGETABLE +| +TOMATO -- AGING + | + -- COOKING +''' +metadata = sa.MetaData() +vegetable_table = sa.Table( + 'vegetable', + metadata, + sa.Column('id', sa.Integer, primary_key=True), + sa.Column('name', sa.String), + 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('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('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('vegetable_id', sa.Integer, util.db.deferred_cd_fkey('vegetables.id')), + sa.Column('state', sa.String), + sa.Column('pieces', sa.Integer), +) +vegetable_schema = SQLSchema.from_metadata(metadata) \ No newline at end of file diff --git a/examples/__pycache__/vegetables.cpython-312.pyc b/examples/__pycache__/vegetables.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..02df0c3935547d2aa77b39de8b35850562c4c9eb GIT binary patch literal 4049 zcmd@WO>Y}j@a>1!>$Tmuv6CikffS0U+maMo@gbl>5UHwwqLNbDy{xw07uR9!4R6;d zwsJ61BwPU^mWo_X7;zv`D)ARMaN|OiNJI}1hr%H@SIz-(VrJKCJEZ}H-~xM?H*em& z`F=C|buy_D_)dK`H2YPQklVNkzeE@5$Pq%C#3Ghp6Fpc3U4*@8OGR0i1*A!Kq^Rgh zF{(%Xy$pL*R|O)HGsKE45lf-UniR;?V?H$sRF%ehR4oSFRGb?PP3W&{T~tr_9Q*qx zA!fyBaCR`H^jAv*srUEyA!fyICPMAL+Hk0!GJjkPyVlUi zpF~dyvY?Ob8okl~arE~6#psPf^yIn8-`6=!gmtcuRfO!=7P6en3OqJ*{^gn6Ra!K& zB9~r#^(2qx9NRWM3WsvpE7-X~Y2C*{I>SJ15=wNzBG5vGC8A3eF)Q)dMVhCcdBvu= zZjbcuV~@doX97TzEDDRn04(?xMc9R+NrkKm=fE1dWELrpfH@8W!Lm#3Sr&&I3>&(O z(k3P*etmxGs#By>ZZXfMrkz~5NK2kOHP4(mnghfsc_1J^m2*x`(FLb?a)Sd za=yYr34FQfV7bX^2A{|kT90+*uct_x6AmG=z4&dE^(AUYV z0O414iOQC^M0)cj0=1x6k|pPbn~|_fS;_!M)aQua#i0&xs6I!`(ssy*4{*flWI@oi z8InzK<$_Z*J!ePW(QZHQrvUj#Xc7uxTqT{q!vURQ&1fWOzdSB6XK2o z_@zd^SCiMGx74v4nH%K|_2JFYgOKL9pO0LxIDr-2@m(4N1RUy}Et2q&%@Brp#V7Q( z$=p@Jx6(z<3Evv7nV!cASIQoBt9!df^+_3Ebg{~7nNyy3ci4$z*|+m?K;XAW=vzA13osmYKTEWvFbxF(5-LOB%{<*g ztGBQ}uZkGZse_C)(pE@v`1%vg(~Z-uL#^}c z=||deN8K$<%@*eV-bTu3MK#{F5;y3Y+Yj%^_1R{rQEHuE5!XkL z-4)(!ob5%?*uGgB0X_?^Lc4^gjL;?iEKklyRWShWzQW^i0to3fVC03qYx!ozt}p{dU_-B=p9jtXm~i; zM(mD;hnHIU*5!@JRPXS_z~LLM%dM9-A}4x>k8R^kKU2OHJl1RSTr5}@;H|{xS)pjM zit#3`^alx7Jil%6MDK!8u&T-4&Ydh+%-T(qw6ltpKun^McjS;j1c zgeSVz2Ry?&oDFE1yZj5N$yw$Ujh;2BzP7+GhV3lK49>n^)n|h#d752-Ew&dI1`TOT zf*^cHlDEmepUI&g$-y5;W?PYj1IyAjfn{487qt52Z30XC2}O|Gdq`qm{qYZ^o=c)-r2W8`TwWLq64~&H}a4sI1F}`m}tH z)`=@=u{H!MQe$=Y;li~AU*ry{SDJH;xfN?eKG~O0YKfm~pK2@9Uz}Mz^X>8JTgRur zG}e#5zAj(vGhMwQexiM>t;-LAnv_^efSS?!mtSjMYFyfs$L})!Z>#Bh(^pMFi{8-J z<%v!*Ix2ochTADJHUWMwG{D6J|EBH%x0BH^(47I@nPbaOf15sfD}8k3srB@!TO{RI F%5MSoBc}iW literal 0 HcmV?d00001 diff --git a/examples/mapper.ipynb b/examples/mapper.ipynb new file mode 100644 index 0000000..94d1df3 --- /dev/null +++ b/examples/mapper.ipynb @@ -0,0 +1,200 @@ +{ + "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": "markdown", + "id": "c0914069-7f3c-4213-8d34-f7566033e054", + "metadata": {}, + "source": [ + "## Development notes\n", + "- No registry actually needs to take place if there's a default type2component map or one supplied on creation. Can just collect right out of the gate\n", + "- 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": "code", + "execution_count": 3, + "id": "7d80f7b9-7458-4ad4-8c1a-3ea56e796b4e", + "metadata": {}, + "outputs": [], + "source": [ + "vegetable_mapper = Mapper(\n", + " vegetables.Vegetable,\n", + " vegetables.vegetable_schema\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d24d31b4-c4a6-4a1e-8bea-c44378aadfdd", + "metadata": {}, + "outputs": [], + "source": [ + "# not valid; tables need to be wrapped in CO3 Components\n", + "vegetable_mapper.attach(\n", + " vegetables.Vegetable,\n", + " vegetables.vegetable_table,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "f9408562-bf50-4522-909c-318557f85948", + "metadata": {}, + "outputs": [], + "source": [ + "# manually attach component\n", + "vegetable_mapper.attach(\n", + " vegetables.Tomato,\n", + " vegetables.vegetable_schema.get_component('tomato'),\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": 7, + "id": "05fdd404-87ee-4187-832f-2305272758ae", + "metadata": {}, + "outputs": [], + "source": [ + "# attach by name in schema\n", + "vegetable_mapper.attach(\n", + " vegetables.Tomato,\n", + " 'tomato',\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": [ + "{'co3_root': vegetables.Vegetable,\n", + " 'schema': ,\n", + " 'collector': ,\n", + " 'composer': ,\n", + " 'attribute_comps': {vegetables.Tomato: },\n", + " 'collation_groups': defaultdict(dict,\n", + " {vegetables.Tomato: {'aging': ,\n", + " 'cooking': }})}" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "vars(vegetable_mapper)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "c16786d4-0b71-42d9-97f7-7893c542104e", + "metadata": {}, + "outputs": [], + "source": [ + "tomato = vegetables.Tomato('t1', 5)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "884d6753-c763-4e71-824a-711436e203e1", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "tomato" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "137d0bf1-940d-448c-91e9-01e7fc4f31b4", + "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/vegetables.py b/examples/vegetables.py new file mode 100644 index 0000000..488864f --- /dev/null +++ b/examples/vegetables.py @@ -0,0 +1,80 @@ +import random + +import sqlalchemy as sa + +from co3.schemas import SQLSchema +from co3 import CO3, collate +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) + + @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) + } + +''' +VEGETABLE +| +TOMATO -- AGING + | + -- COOKING +''' +metadata = sa.MetaData() +vegetable_table = sa.Table( + 'vegetable', + metadata, + sa.Column('id', sa.Integer, primary_key=True), + sa.Column('name', sa.String), + 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('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('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('vegetable_id', sa.Integer, util.db.deferred_cd_fkey('vegetables.id')), + sa.Column('state', sa.String), + sa.Column('pieces', sa.Integer), +) +vegetable_schema = SQLSchema.from_metadata(metadata) \ No newline at end of file diff --git a/tests/co4_example.py b/tests/co4_example.py index 1758a6b..cee0459 100644 --- a/tests/co4_example.py +++ b/tests/co4_example.py @@ -26,7 +26,13 @@ class Tomato(CO3): return self.size / 2 -tomato_table = sa.Table() +metadata = sa.MetaData() +tomato_table = sa.Table( + 'tomato', + metadata, + sa.Column('id', sa.Integer, primary_key=True), +) +tomato_schema = Schema.from_metadata(metadata) mapper = Mapper() mapper.attach( @@ -39,6 +45,15 @@ tomato = Tomato(5, False) mapper.collect(tomato, for='diced') db = SQLiteDatabse('resource.sqlite') +db.recreate(tomato_schema) + +# for non-raw DB ops, consider requiring a verify step first. Keeps up integrity between +# runs +db.verify(tomato_schema) +# then +# not too straightforward, but can also verify mapper compositions if they use a schema +# that's been verified by the database + db.sync(mapper) dict_results = db.select(