From 8528f9ce807ed2ee4db26a06113221ac465e7e5e Mon Sep 17 00:00:00 2001 From: tmsincomb Date: Tue, 2 Apr 2024 14:47:54 -0700 Subject: [PATCH 01/18] core package requirements --- .gitignore | 164 +++++++ .pre-commit-config.yaml | 20 + README.md | 0 README.pdf | Bin 0 -> 11284 bytes poetry.lock | 969 ++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 27 ++ 6 files changed, 1180 insertions(+) create mode 100644 .gitignore create mode 100644 .pre-commit-config.yaml create mode 100644 README.md create mode 100644 README.pdf create mode 100644 poetry.lock create mode 100644 pyproject.toml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..74f9128 --- /dev/null +++ b/.gitignore @@ -0,0 +1,164 @@ +.vscode/ + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +nb/ \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..d1dd0c1 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,20 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: check-yaml + args: ["--unsafe"] + - id: end-of-file-fixer + - id: trailing-whitespace + exclude: docs/img/ + - repo: https://github.com/psf/black + rev: 23.3.0 + hooks: + - id: black + args: ["-l", "120"] + - repo: https://github.com/PyCQA/isort + rev: 5.12.0 + hooks: + - id: isort + name: isort (python) + args: ["-m", "3", "--tc"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/README.pdf b/README.pdf new file mode 100644 index 0000000000000000000000000000000000000000..eff2baa79326409fbcb2e138d0d691504f2854bd GIT binary patch literal 11284 zcmaKybyV9;x3++B?LhJXQ|OAB*@ zxp~37U~X;*1i~xC4hFLz-^g*w&X)i85OHw;%*pa$3p{^s#lj2-0dPT;fm|xiu8w97 ze-GmOdyuTH0|L1-E?EcU9;9Iw&XzC$*Grg_HNpnS4Hgs^2f`7^lpFz`nIUoGc2Edj z=;1Zl{kbVbVG&LpXN1RJmE1*d`qIn={rvVz{dQNWU}DrW+YSjw`2x2`t(_A&^-mc{ z>jDp1X`!vNT9lBwwcn&2#?_bJj8sc~oG!_N&ulvM%?M&ztMz1lsTeudV-rt5YuqPy zs(`On9P)99lj6DUL2v!gK(eKUdTP7{DrN&5yZGkM#7vBaNt7^H2_?i2v;`?gqoS_Um_T3w!gjt`Tx1p zKXZHd`A>)bog4gNIydA_kTy}WwS)r=A4;HM^tVw2{xeL<3}NQrZ2ho|HL@cAv5Mf| z!~bC+2oS=}2Npyo{D0U?2qGZxUwzi@jq6D>oqSI>P@5&F@{1^(G(oQ~_}TDNCR<(} z5-*{eo3ZW$2iAB?falZb!gyZl5zQ0;?--Qfh1nw1kow9r58Wop9L>%10|;aUSbRK~ zGBGj18Qa+E2a0vV?eUQ%h)wx*zjt|mw-WGecxgp!dkH=&cKNN{qfn9(xe2UcS2^V6bwo%lyh-I zW$*L&1cTH>c7Sp*+q{-_4sU!(PWD`<+dS1;KhT{}LmSyoHocwhE^@?T$7-pnn>*vk zt%LRp)Ea3#=g2X#hwkn&p9}y6_8}OiXB`a)8Ynx!>J~#i!zcz^;?p}F7n@pyXFU(o ze^Iih{FRhZz;OS@v>y({^7+bnLQUaKd5f7x|F!bUA%ygg?!^S1DPX~R-Y{9qG4G9M z@Hg-2sVX+#^UHhJ<5=RW&umA347Fv-{4E%cS%MoygW?+Szg<`F#o-T%n+l;YR~o;p z*;bpXB{}(k9lYZw*HBqvV}lywj-WkJ!ue<;H%GxZMm)ncA857jV&=SOxWxf)$QNMvTuDqkPV^znzqn=AKc34fB zQcYm6jJu3m86xl^07Kdl8)Fy!kH(L1heB=d*}E?n=GqPt?kGM}wf)|T zhGC#K7go=Coa67PWT^MrM`ywav7RHME4HyuW12*NnCEg~pJ1Jv+V)j$C5{tT9KN1w zl_6n|I|#+hrG^;pBy))-#yDEgjBxJ!(Uzk+BC_9hTrqj$?tS*Io?h)H{w4HF!Zhy8 zfIq0AzdYgm&3PsNlza8Q(4xv>e@wui-?+GcLiQ*+J{_%8)J&n)tOV-;38!jsmGdbT z^Ei51rl_Cf$av&+?>z50us!4OCTI_dv7pJ#;%LOm=%b#Ku!uE{wFqd`;}A>nFM-!> zTBLBH(A9}Lc`dZew0v(F_*tLr-2UYXV=u2!b)lAj{(^OJ?c#o?>4)c*=S9=&o33?2 z4s;6f_M_wwi`5YnPm7CP!4oNa7Ij*0a2R!k-V<;?$#1wo3)YVD(NF&~6mMGJYi4iq zzNHMJFy;!uGs&m4X%vew`{)v3kSAOHfqLjsQuj}*pZ1x_{9^oN{;Ob4GB=}|OHw2T zg|7FYza#7x*bO=gp#jN}mt}OPr$^&c-7e6%kmF&|v#(ol8ynrqSG_*VI)d)HGI9@MYFSPH= zUW4{MJd%Vv7LGa6gD3;=7tX#j1^rB+;RukSFzj0uy5@X|606^r$HLil(C744ei^Vn zgMY@8%K&5<)5uj~2(^CHE=DnL_b1Tmhk*`z8Y>5JI%`~+-6Jast+m1C6#k3sKlTu; zQvys2j5-3p)$(X-f^3NC;Yzjt@~-{tS%vuily-6XuYLWRHXr@j3Fa)i{YL(e7*ai7 zS9jjEd(FJ8+b{oedChIgW=QBF(*CHNwRED&o65_o;y_PrmV(o0sa}mE) zqAm#LUVj9a3mc~L$3=aF&$tMqcFH$9$cH|8`k_xAzMtz{*laa6o1iZc<1%lB;1>EJ z;%5qM2(dBme*XnQihp2R`B{}rU}#>_jSa`mnE`z=05M(r%k-k`4?|Kmg!uA{<>X*d zdJ0utjB#7D_t@;KWzU+!fSn?&lD>oSmA2=F*n+?y^nry7G9`j%2L#N~W-FkmxNZ=7T*-^92X9=wn zS)n2ZO1#}3#|mE+H6T%@@2ie2-l_>1+KP9+RoAO2r^qDyp}gfnl_Z^13E&dTf|V{+I=S-Jm(7h}?u)XB zBS-{G!!*yc3t9t1#cRV*L?;d=$|6@*d6seQS1b_b%ZiQjF2`cr@|02|zQL5F8VAU;Vs`HN0&Kk%-Ofw8qOa9WF>grDQvu zyR%WM4{%vUBtitif+$W!zG}&jLT4A3MPf7Wx3OEl)0hF;+!`2v-35@DT|J2apAyQX ziFpN!t@7B*&RySgb#1q?bb&$R_b5egW7@@1B^K!Yj|0gUO}f2B4F3dtf9=G6`{&d! zhBn9RG;vvglNs2$k=49{W)G?LqrQmM(Mt`B)dP1{_|Y#4)Dtwg{jRVntr&4`Fqq-A zJv_3oQQl6tr=-mK*?oNUagpWw0{wxP-?j9p;o^AZzO&<@7rB!P*0jjjHEI*4L*ETe zKm*3T_h{RBv9nV!_2OfHCk98165DOHC$$Dl?4ZZP=8ilb(Qkzm;bj?IJo{rnf zQyW;6xA-P+mTM2Q4b^^{$QsQtyH#pg|MNwA)ML~lxmk1K<5o>nn0ITcKFBevctKbs z<-EeUea1YipZ1c&aHieLZROD%ho=j>JH7C@ijApf@MXKDaII^D$B%}U_K5b-brr0H zQK{mrh1Q?-)5Qd1B9N9@znN94eY!K*!GyjKWzJ&8glCQhG$N#lDf1eQ%+A5jzBw6w z%FBA+=>L7qV5P3I0S9d2ew9-Hsq;{|2hFgnh>kAG88e{&@aHd4oK8BB2+j)N=AcMP zbIr~eHT|w9Kkm#`W-hf%B6bA@NDS^;TyOmKaUP*!0oC zAFmTq`vQJfdr~lAdZfdY|!;AuP(=ZuM01R86 z(BW=b<=~5TQH|EPW7!cfIZ^jhzkdhJ-_0n#AhC)1sh~UC(?3!}YNck6f z3^S2QOk`hYSA(nJ+<6~92B*KRgO-S9Vn9bewG_TAQL6&5Ac%KZgUd+9SjTjUnD7J3 zHgPJXZOEaC!v~XOJ2J^*yihJ;B4|(;Ju_ZAbV^XrEB_gB9frBiAnBMGzLB26?(Ov2 z$06sX>zS^GQZ9eg?Ypf=AT0k> z@#kF5+3GrVtT*rkc<$Ad9kv;Y-+g5sdbbI_{b$6s-C^+j@9CV0aa3zZG+T zvD+0t7Goj~7EAK{)ixrbEJ`WTd+)sB?cl!76eAH}U3Ihf(^08bH^A>u&c+=#;B1~^ zLaZvF;4oW)TJ<}Tf8h0<8n&R2Sjdp@U)=BXt2vD+)-kTVuhW!U5OEf@ zTVyv%Z97X^mneN)rOy*ctb21*tLk?)Lw*?};lG!6p?hALnzM3Mar3!$NbbJ(dMWBu zuDc+>b7O+eG5(r+*ZQtjVqbI_#q;=N&LjPL!l zNcL6L&T*{!O7ne=KV{o){jGrC*4wZHvraLD@qVwvA)yJ?%H<+YE9WQ2jMhCV0h5!S zp(_6i*M6ZYt;4(3Co%rp<5it!#Y)-ta~tQa{@&#s0sF-Rkp3#CJc+|SiVr>}`j@jK z*8wN1sDBg*fV+dR89IZ@`q!h865@MzV-3oeAPS1n z-_v(*S#nzZhL&k`vO5WB?s~XVGL3GACSrYW%JLff#XI%|D86?VpH5i_fR+VMiB2&b zW>ZtMT#wj!oK?lQxHO*Qajhw&Nz2Y*W@*aOWyh^zUsGp?tD2w3Ri&J3 zvsx_eEk~)Q6nT@)mWVZgB~nuNnBEm?MSYJu7^m^!75b?*8KEad#ll)p2|+NG!TN<1 ztS@baEDHO6P-hPa*$P9p%xJFdh`$^v&RV>#>hhklDNKpBB~X36up2AVHm#O@-eVX2 z`S*dWASG{Ygnp*Z?|HHUAD74xto~YZs!OF7y|UUG1vbTT@23#;{Givu>+k_P)`{HS z_)x5z=?{K&+^R5_82hID@t1tG%tn}p&zpO(_0OA^P#Jb{2<6&ki_rU)==CZEL7&5c_q(nM+K&v#(_sXgpt;l{LUo(1)`n}6RHw{Uz zrZxkZR=4hkmr3A^k!viSgN8$#o;(#?|SwY(4S0gv_!3KVpjI{x9_4G$@k|Q7+eUG`Rm17N{|Lt*Syqw zxxFD6%c7Vq9Z{H!PUC{nvl|CxlX1ppY4>vyO_+SC6Gul;+e9qw32_Bgv1@ zXA%$6A9p?NE_l$GK#bNp_>D9(p<8>D08AP6=;u^$g<6e~)G~-uuGgQ{Nsn z>ATnx28*seH*H>bj@@V-D&*6W7qOwdHfE93tDlWgn?)t}5V+JV`* zP@Rw6*IGqi6bv|*)$)se23IONaDt#F0Kqew5t(4qo`EKET~H>EqQS7HE)A+EO;y}0 zh!h5S{5ds_(X#!pXQD!(RxbYKL`749sWk4V`0A6BIC9U862Gn%Qm~ZH zJJ@J+(fDMw5GPKGMi-L^IS-bUt`*ARZj`lrJ7iB}UDzGJwDB%d;m*uR|CY=vB+d@Q zSKNH>Eg2c(DbK8FyMsG*QpzqQjLLUn{jz$o1&{E|LW;>F+2pIpJeMQD5DbF3j3Z` zIMIQ!>%2p)bd~wO2Rzazqi_l7W`H#fO3M>EX)VhVa7_;6GekJVzvQ9L6b4}=s=G3>YAC+WDX&pc<@-sp)~p@oP7zqMNBbrI?I!(I}!9v@QQ!gfaVjOygn}{F4eX`l~Nc~>cc;(DZzxsQ#m@M&=t9^c2;tcFW$89Z_+mz8%E| zJ?35MmjKqf+Cf>d@gCmcC3=bjHeo%LD&5jF_Dno(;_nJFh=y$DC;%y7Qn8lds9Yy1 zjy&E>kB^eMAp(%bs}NM85Y~bR5a0R@gF8h4;W$j8~#6@eT&Z6TA8h zfj)G3YYJ{H^jfdyg@$D8{zR-P=vY5P1UZJQ_i0zUDzsC-{_3qoF$a7n#aUwgr>qZ9 zYA27&qt5x`oYF1?loBbd8eyz%JunzNOX&gLP3xA49a5pq;aq3jJp1Z{|NboDZK%O8 zYhn`PwN@5xr0+Wq*gWg2Cpm1#_!adOTtZ%6fGvgh*#t2^ma5r;&s&HADoJGe8k34- zTlJLhfBaMl%HcO{?}d{uP$j@*sa3t)7>r)Vw{^karf;}L$+mr{>C?*W#V45|PCswP z=PT6-BpX(TCVZomT&9EzEkko@%lZd+>GOgVN@>}s9hz!XndT!HyWb^0YWa{5k^ZCd zD`P+Se7Gr*Ah?@ZY+HvV5Mya1Su3<+{pow{q_oM>9;5FzJr!2@+I(QzKCcgX80_%| zFG|bC^?KGRXtO!A%UpzvV&haKFN$1Pcg$Tv#^vM{2%e#BRv#R%9+%}}+S7}baJS4? z?FNgAZG8;EtI=~){_I<-r6)EZdxVqq6iy%Do4-@(spLJpIW9CPGdu4pty=00p)SU# zNa!-#elarBY(w7ol7x!V!{=SxmpF*3&`4eK?yK}4X#;`peB=Wh47Aoe6sMUJ#^sxK zwr!1+l>YFRlFz-O8-<)F&{vMVt$8V#uQAQmo$ezV|E@?oWCB1faZrl6%*-u}>XWR$ zFcNLA$XvWWV8Adag#IZmEg6pa-c6Z1@2emwehY3Pz4b?n#Lf1N@RX5ez^lrG^Px;^ z+$Q7A=WJ?(NjE~tT9k3IuQ58mjgvO3*BX$K*J`j&Kwsg|bn})z1t8je^ z76g6310Cd1(~{mSX#uo zu&RK3xAwa}G`Ez55F&xT53k{GjgP>1*eh)Ee>1-?%w6Ikxc(szPtoIkLeNtpk?WXNpw@r3& z(f)Pw$m4Z~bXQ%tQ`$!(OGP{yvmN44c-PT6**vDM>vmr|YN`@gT7%(uIbp(OVi!Qb zldQ(?ONvm;d|o*3ryVq)To(_ngsV6Urf{M&o#!7=n@K4W>Gn-%P~T9*s`A?Mf^E zOtX8zNXyVm-ggP@UGpzZg`-b_tU`_#LNR@V(!-x{04?q%@q}(r`JmQ7k zxuat*JHWpElbQ9)#Cyf~h`xU0#rU$W&uMTKtxUq!bJA5Y>bF%pF@3QYRknMt%a(lU zk7-S`l{b8`8*b^AQ`p&H#(WdP*XxPz>kHR?tI_syhiC08ZN@7o+g41&mMxzdP2v}Q zuuhbeQ#=-S3w=D-V+_igQDCm<7A7Tmot6XTuG};g#W;)XkJXe#m^7`b2r{Q^+jz-cePDxS(P zn~=^_-?Kr%L=I)hCimgcTFVO`L>Z&ZD?aTX3Dxd0Kg$IbTh|@pV_?1E@qJRw%Pobi zWqSIMkyGVpLW5O4JZeDPB`3JrwOS zMO4(p8A=UfCIQ&^X2E6H&6|a3lk?9@HR8&;WZCqzTm#c0bbk2OPOXxzqY3e|UCk#@ zm(dt=ib<-(eo1`w(ib`)^t)lo{wZ z<6{QpQ3^%9*B<%BK6!mpylx*uLD%`Z%?rx;Hd6|)3O2=vh+8*+eFr%Yt|X_ZqpLmU zW!qscB%9T8YC4vCjcUx)_)>vU_jRoyoj%K*W8-E z#$0ekr9=1D$vroRWAFXekJH@FPf~i=L?t7aO$HnqGBjc*=}FiUbgE7+mMz?`7nw6J zng${jU`C{ckylO;q!Vb)Pm2d8X;~^P-U@V{d*4H1Gk=R-p@`R;4_vO# zMG7|VVP({C_0;4oFVE3{-_cgIw!0nLE)!q$sih9IR6c60q`O`}2=Bddyd`|R!-E($ z_rD+Od8|e{)}5kKa?km~1GP4nOK*5c`bi_OX}F%$2=}+a=W7tZyolL5rO)wim{8@= zRA^dIT-8g~0#kaiH--(EDl#9rwgR_pHswpAgK6gn&kcpQtH0%4S5wsBIQ+~ezgp{w-dLIPGNIGY?3F3M#SaI#0&X-JEuS0$C&JWG zdOkHAcYy4>As*{+E?G&+tTpRIVI zghJn3c}+Ttdf3pKWqE(#^()z@#6Q#YYP*Jg`v>~dC-0+H<~y&oev#BS;|D$7m&*WI zc{7|eObBn;6xF2_FjtMlXXK>`e>y#l`wAB^dE=5aHj`2 z?XODvWO{kgDXDVa+2PiKFIeT6br`&nnz0S?_;xNK2j#ABxi8-Mb1N}VwG)8O;mdvp zz3;Ycz8BBeUUgr2^uY~*4L21aa64a7^sl{td@^{K;nCP*QfP4D6#gk;PvFt%_dE74 z?^@iD|9H<q>EV!bv zPk}^DTXC$uIFus{1Z06DejpJWo@!q?^`)1eV?rb|ltsXn?^70%qwJpQq4MRtPqX6! z))vVc7~7PJnU&FXn4g9p9&L?)$QN}GGD@3(MIB8Hw2qzTN0CnC(|&jbg8ZpW(<4&_MHU*u7ITpJ z^%|ToTiSPwEJtKVsdH1JtSUtIH<*3hDMYvw51I z`@C}r>QPH@t#a>@jjGM(PziJ2;v3lzWubav{R$*nh;R=?P^hK;iDr-w8+ZD={UsZa zcRKzD*p5{(y;MvwVyOQl=7X? z9XRPLsI_@d3gq6NDxKlE7|V8|-Pj{1@Hx#uoM8cJu9(y)yfJQZ%&&#L$&Yg0I?V0v zbmmFTTI{J&$ePliIQKT9xSKvTrPxCM^>!1-Y}?fMGgCDtlLHJu{wsL^AJ3tl|hLFl=={?cQQ zfYgx2u{Tt>kJ1{d--taXG_C5z{Y^V0^yZU(BkFgjc>>Qc7}jc5O4ge&j@Hi$B^cf- zSFj;UyqU#vBKRJ|9>Zr3J!h;N^1ZA#op!fut3rFTC@c2uG)IILCRjH~=L(-C>h7?Q zoXbb^I2TjPJ42%HMjA8XM{X+VzoeW8pd5RK9{*3^O6YG~=pRth0~RF>gIl=Tx*!?4 ze_&E7W{yY{OUu>~1_x<4JDNEmWd}2BBzpwll7w3zu_vGaACh}|hm$n60%90?5w~;pFFrfRUmC5@xcskaV(kfB}(F56eUU;;%s5 zU>;5+x&#FAatm^Db93|a10e!nFegNaj|a_V@&L0+B*AnPFrNjJf`fe-Z41Kssc zS5?V0{%`C_)r8S#|1Tm!ycahN2;1`=7_&oG${q?hsO_f;znkpAg$#r zry#9t=7Pk{ERlKJBE0^RHND`-+!dUxoFDiy4Vbkp5_0nbK9@w&fiNUWsOoA7bG3D{ z20s5+7*f=9b8&HiIX;vR`~afqJ`iYz0^Ho35Pkun2M7%OP*)=$L=ee{2?%fs@*oR@ z6#2klBV-Mvb^nU{SK|K#hy1^|FK!WWi^qOr z^-osk6zyyr3IwBW{9x8;Pz-+BLZ9Y@wzJW851b7V3?9?K0WYPd&}h7_@Ve7{r_aKP zPFntTJI|j+h50Ero%sXu_fR&lJZ7auGP4~hk0rkf)XIMwb-8N68}5F*PRO}Kss&}O z7Oa_!imCFJjFWLc09_g_jF$h&_`Spg)oc=dPP_x)u^4P?8t zoVA^7A4pFib7e-!C9PA0H1N&J=o@mH;o*9gBL LfSFlFRrdb@>jkC0 literal 0 HcmV?d00001 diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..1d5fa74 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,969 @@ +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. + +[[package]] +name = "annotated-types" +version = "0.6.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"}, + {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, +] + +[[package]] +name = "anyio" +version = "4.3.0" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +optional = false +python-versions = ">=3.8" +files = [ + {file = "anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8"}, + {file = "anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6"}, +] + +[package.dependencies] +idna = ">=2.8" +sniffio = ">=1.1" + +[package.extras] +doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] +trio = ["trio (>=0.23)"] + +[[package]] +name = "cfgv" +version = "3.4.0" +description = "Validate configuration and produce human readable error messages." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, + {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, +] + +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "distlib" +version = "0.3.8" +description = "Distribution utilities" +optional = false +python-versions = "*" +files = [ + {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, + {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, +] + +[[package]] +name = "fastapi" +version = "0.110.1" +description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +optional = false +python-versions = ">=3.8" +files = [ + {file = "fastapi-0.110.1-py3-none-any.whl", hash = "sha256:5df913203c482f820d31f48e635e022f8cbfe7350e4830ef05a3163925b1addc"}, + {file = "fastapi-0.110.1.tar.gz", hash = "sha256:6feac43ec359dfe4f45b2c18ec8c94edb8dc2dfc461d417d9e626590c071baad"}, +] + +[package.dependencies] +pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" +starlette = ">=0.37.2,<0.38.0" +typing-extensions = ">=4.8.0" + +[package.extras] +all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] + +[[package]] +name = "filelock" +version = "3.13.3" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.8" +files = [ + {file = "filelock-3.13.3-py3-none-any.whl", hash = "sha256:5ffa845303983e7a0b7ae17636509bc97997d58afeafa72fb141a17b152284cb"}, + {file = "filelock-3.13.3.tar.gz", hash = "sha256:a79895a25bbefdf55d1a2a0a80968f7dbb28edcd6d4234a0afb3f37ecde4b546"}, +] + +[package.extras] +docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] +typing = ["typing-extensions (>=4.8)"] + +[[package]] +name = "greenlet" +version = "3.0.3" +description = "Lightweight in-process concurrent programming" +optional = false +python-versions = ">=3.7" +files = [ + {file = "greenlet-3.0.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9da2bd29ed9e4f15955dd1595ad7bc9320308a3b766ef7f837e23ad4b4aac31a"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d353cadd6083fdb056bb46ed07e4340b0869c305c8ca54ef9da3421acbdf6881"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dca1e2f3ca00b84a396bc1bce13dd21f680f035314d2379c4160c98153b2059b"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ed7fb269f15dc662787f4119ec300ad0702fa1b19d2135a37c2c4de6fadfd4a"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd4f49ae60e10adbc94b45c0b5e6a179acc1736cf7a90160b404076ee283cf83"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:73a411ef564e0e097dbe7e866bb2dda0f027e072b04da387282b02c308807405"}, + {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7f362975f2d179f9e26928c5b517524e89dd48530a0202570d55ad6ca5d8a56f"}, + {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:649dde7de1a5eceb258f9cb00bdf50e978c9db1b996964cd80703614c86495eb"}, + {file = "greenlet-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:68834da854554926fbedd38c76e60c4a2e3198c6fbed520b106a8986445caaf9"}, + {file = "greenlet-3.0.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:b1b5667cced97081bf57b8fa1d6bfca67814b0afd38208d52538316e9422fc61"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52f59dd9c96ad2fc0d5724107444f76eb20aaccb675bf825df6435acb7703559"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:afaff6cf5200befd5cec055b07d1c0a5a06c040fe5ad148abcd11ba6ab9b114e"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2797aa5aedac23af156bbb5a6aa2cd3427ada2972c828244eb7d1b9255846379"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7f009caad047246ed379e1c4dbcb8b020f0a390667ea74d2387be2998f58a22"}, + {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c5e1536de2aad7bf62e27baf79225d0d64360d4168cf2e6becb91baf1ed074f3"}, + {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:894393ce10ceac937e56ec00bb71c4c2f8209ad516e96033e4b3b1de270e200d"}, + {file = "greenlet-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:1ea188d4f49089fc6fb283845ab18a2518d279c7cd9da1065d7a84e991748728"}, + {file = "greenlet-3.0.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:70fb482fdf2c707765ab5f0b6655e9cfcf3780d8d87355a063547b41177599be"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4d1ac74f5c0c0524e4a24335350edad7e5f03b9532da7ea4d3c54d527784f2e"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:149e94a2dd82d19838fe4b2259f1b6b9957d5ba1b25640d2380bea9c5df37676"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15d79dd26056573940fcb8c7413d84118086f2ec1a8acdfa854631084393efcc"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b7db1ebff4ba09aaaeae6aa491daeb226c8150fc20e836ad00041bcb11230"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fcd2469d6a2cf298f198f0487e0a5b1a47a42ca0fa4dfd1b6862c999f018ebbf"}, + {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1f672519db1796ca0d8753f9e78ec02355e862d0998193038c7073045899f305"}, + {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2516a9957eed41dd8f1ec0c604f1cdc86758b587d964668b5b196a9db5bfcde6"}, + {file = "greenlet-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:bba5387a6975598857d86de9eac14210a49d554a77eb8261cc68b7d082f78ce2"}, + {file = "greenlet-3.0.3-cp37-cp37m-macosx_11_0_universal2.whl", hash = "sha256:5b51e85cb5ceda94e79d019ed36b35386e8c37d22f07d6a751cb659b180d5274"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:daf3cb43b7cf2ba96d614252ce1684c1bccee6b2183a01328c98d36fcd7d5cb0"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99bf650dc5d69546e076f413a87481ee1d2d09aaaaaca058c9251b6d8c14783f"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dd6e660effd852586b6a8478a1d244b8dc90ab5b1321751d2ea15deb49ed414"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3391d1e16e2a5a1507d83e4a8b100f4ee626e8eca43cf2cadb543de69827c4c"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1f145462f1fa6e4a4ae3c0f782e580ce44d57c8f2c7aae1b6fa88c0b2efdb41"}, + {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1a7191e42732df52cb5f39d3527217e7ab73cae2cb3694d241e18f53d84ea9a7"}, + {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0448abc479fab28b00cb472d278828b3ccca164531daab4e970a0458786055d6"}, + {file = "greenlet-3.0.3-cp37-cp37m-win32.whl", hash = "sha256:b542be2440edc2d48547b5923c408cbe0fc94afb9f18741faa6ae970dbcb9b6d"}, + {file = "greenlet-3.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:01bc7ea167cf943b4c802068e178bbf70ae2e8c080467070d01bfa02f337ee67"}, + {file = "greenlet-3.0.3-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:1996cb9306c8595335bb157d133daf5cf9f693ef413e7673cb07e3e5871379ca"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc0f794e6ad661e321caa8d2f0a55ce01213c74722587256fb6566049a8b04"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9db1c18f0eaad2f804728c67d6c610778456e3e1cc4ab4bbd5eeb8e6053c6fc"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7170375bcc99f1a2fbd9c306f5be8764eaf3ac6b5cb968862cad4c7057756506"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b66c9c1e7ccabad3a7d037b2bcb740122a7b17a53734b7d72a344ce39882a1b"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:098d86f528c855ead3479afe84b49242e174ed262456c342d70fc7f972bc13c4"}, + {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:81bb9c6d52e8321f09c3d165b2a78c680506d9af285bfccbad9fb7ad5a5da3e5"}, + {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da"}, + {file = "greenlet-3.0.3-cp38-cp38-win32.whl", hash = "sha256:d46677c85c5ba00a9cb6f7a00b2bfa6f812192d2c9f7d9c4f6a55b60216712f3"}, + {file = "greenlet-3.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:419b386f84949bf0e7c73e6032e3457b82a787c1ab4a0e43732898a761cc9dbf"}, + {file = "greenlet-3.0.3-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:da70d4d51c8b306bb7a031d5cff6cc25ad253affe89b70352af5f1cb68e74b53"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086152f8fbc5955df88382e8a75984e2bb1c892ad2e3c80a2508954e52295257"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d73a9fe764d77f87f8ec26a0c85144d6a951a6c438dfe50487df5595c6373eac"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7dcbe92cc99f08c8dd11f930de4d99ef756c3591a5377d1d9cd7dd5e896da71"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1551a8195c0d4a68fac7a4325efac0d541b48def35feb49d803674ac32582f61"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:64d7675ad83578e3fc149b617a444fab8efdafc9385471f868eb5ff83e446b8b"}, + {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b37eef18ea55f2ffd8f00ff8fe7c8d3818abd3e25fb73fae2ca3b672e333a7a6"}, + {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:77457465d89b8263bca14759d7c1684df840b6811b2499838cc5b040a8b5b113"}, + {file = "greenlet-3.0.3-cp39-cp39-win32.whl", hash = "sha256:57e8974f23e47dac22b83436bdcf23080ade568ce77df33159e019d161ce1d1e"}, + {file = "greenlet-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:c5ee858cfe08f34712f548c3c363e807e7186f03ad7a5039ebadb29e8c6be067"}, + {file = "greenlet-3.0.3.tar.gz", hash = "sha256:43374442353259554ce33599da8b692d5aa96f8976d567d4badf263371fbe491"}, +] + +[package.extras] +docs = ["Sphinx", "furo"] +test = ["objgraph", "psutil"] + +[[package]] +name = "h11" +version = "0.14.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.7" +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + +[[package]] +name = "httptools" +version = "0.6.1" +description = "A collection of framework independent HTTP protocol utils." +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "httptools-0.6.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d2f6c3c4cb1948d912538217838f6e9960bc4a521d7f9b323b3da579cd14532f"}, + {file = "httptools-0.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:00d5d4b68a717765b1fabfd9ca755bd12bf44105eeb806c03d1962acd9b8e563"}, + {file = "httptools-0.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:639dc4f381a870c9ec860ce5c45921db50205a37cc3334e756269736ff0aac58"}, + {file = "httptools-0.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e57997ac7fb7ee43140cc03664de5f268813a481dff6245e0075925adc6aa185"}, + {file = "httptools-0.6.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0ac5a0ae3d9f4fe004318d64b8a854edd85ab76cffbf7ef5e32920faef62f142"}, + {file = "httptools-0.6.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3f30d3ce413088a98b9db71c60a6ada2001a08945cb42dd65a9a9fe228627658"}, + {file = "httptools-0.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:1ed99a373e327f0107cb513b61820102ee4f3675656a37a50083eda05dc9541b"}, + {file = "httptools-0.6.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7a7ea483c1a4485c71cb5f38be9db078f8b0e8b4c4dc0210f531cdd2ddac1ef1"}, + {file = "httptools-0.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:85ed077c995e942b6f1b07583e4eb0a8d324d418954fc6af913d36db7c05a5a0"}, + {file = "httptools-0.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b0bb634338334385351a1600a73e558ce619af390c2b38386206ac6a27fecfc"}, + {file = "httptools-0.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d9ceb2c957320def533671fc9c715a80c47025139c8d1f3797477decbc6edd2"}, + {file = "httptools-0.6.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4f0f8271c0a4db459f9dc807acd0eadd4839934a4b9b892f6f160e94da309837"}, + {file = "httptools-0.6.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6a4f5ccead6d18ec072ac0b84420e95d27c1cdf5c9f1bc8fbd8daf86bd94f43d"}, + {file = "httptools-0.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:5cceac09f164bcba55c0500a18fe3c47df29b62353198e4f37bbcc5d591172c3"}, + {file = "httptools-0.6.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:75c8022dca7935cba14741a42744eee13ba05db00b27a4b940f0d646bd4d56d0"}, + {file = "httptools-0.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:48ed8129cd9a0d62cf4d1575fcf90fb37e3ff7d5654d3a5814eb3d55f36478c2"}, + {file = "httptools-0.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f58e335a1402fb5a650e271e8c2d03cfa7cea46ae124649346d17bd30d59c90"}, + {file = "httptools-0.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93ad80d7176aa5788902f207a4e79885f0576134695dfb0fefc15b7a4648d503"}, + {file = "httptools-0.6.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9bb68d3a085c2174c2477eb3ffe84ae9fb4fde8792edb7bcd09a1d8467e30a84"}, + {file = "httptools-0.6.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b512aa728bc02354e5ac086ce76c3ce635b62f5fbc32ab7082b5e582d27867bb"}, + {file = "httptools-0.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:97662ce7fb196c785344d00d638fc9ad69e18ee4bfb4000b35a52efe5adcc949"}, + {file = "httptools-0.6.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8e216a038d2d52ea13fdd9b9c9c7459fb80d78302b257828285eca1c773b99b3"}, + {file = "httptools-0.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3e802e0b2378ade99cd666b5bffb8b2a7cc8f3d28988685dc300469ea8dd86cb"}, + {file = "httptools-0.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4bd3e488b447046e386a30f07af05f9b38d3d368d1f7b4d8f7e10af85393db97"}, + {file = "httptools-0.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe467eb086d80217b7584e61313ebadc8d187a4d95bb62031b7bab4b205c3ba3"}, + {file = "httptools-0.6.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3c3b214ce057c54675b00108ac42bacf2ab8f85c58e3f324a4e963bbc46424f4"}, + {file = "httptools-0.6.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8ae5b97f690badd2ca27cbf668494ee1b6d34cf1c464271ef7bfa9ca6b83ffaf"}, + {file = "httptools-0.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:405784577ba6540fa7d6ff49e37daf104e04f4b4ff2d1ac0469eaa6a20fde084"}, + {file = "httptools-0.6.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:95fb92dd3649f9cb139e9c56604cc2d7c7bf0fc2e7c8d7fbd58f96e35eddd2a3"}, + {file = "httptools-0.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dcbab042cc3ef272adc11220517278519adf8f53fd3056d0e68f0a6f891ba94e"}, + {file = "httptools-0.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cf2372e98406efb42e93bfe10f2948e467edfd792b015f1b4ecd897903d3e8d"}, + {file = "httptools-0.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:678fcbae74477a17d103b7cae78b74800d795d702083867ce160fc202104d0da"}, + {file = "httptools-0.6.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e0b281cf5a125c35f7f6722b65d8542d2e57331be573e9e88bc8b0115c4a7a81"}, + {file = "httptools-0.6.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:95658c342529bba4e1d3d2b1a874db16c7cca435e8827422154c9da76ac4e13a"}, + {file = "httptools-0.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:7ebaec1bf683e4bf5e9fbb49b8cc36da482033596a415b3e4ebab5a4c0d7ec5e"}, + {file = "httptools-0.6.1.tar.gz", hash = "sha256:c6e26c30455600b95d94b1b836085138e82f177351454ee841c148f93a9bad5a"}, +] + +[package.extras] +test = ["Cython (>=0.29.24,<0.30.0)"] + +[[package]] +name = "identify" +version = "2.5.35" +description = "File identification library for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "identify-2.5.35-py2.py3-none-any.whl", hash = "sha256:c4de0081837b211594f8e877a6b4fad7ca32bbfc1a9307fdd61c28bfe923f13e"}, + {file = "identify-2.5.35.tar.gz", hash = "sha256:10a7ca245cfcd756a554a7288159f72ff105ad233c7c4b9c6f0f4d108f5f6791"}, +] + +[package.extras] +license = ["ukkonen"] + +[[package]] +name = "idna" +version = "3.6" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, + {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "nodeenv" +version = "1.8.0" +description = "Node.js virtual environment builder" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" +files = [ + {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, + {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, +] + +[package.dependencies] +setuptools = "*" + +[[package]] +name = "packaging" +version = "24.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, + {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, +] + +[[package]] +name = "platformdirs" +version = "4.2.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +optional = false +python-versions = ">=3.8" +files = [ + {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"}, + {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"}, +] + +[package.extras] +docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] + +[[package]] +name = "pluggy" +version = "1.4.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, + {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pre-commit" +version = "3.7.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +optional = false +python-versions = ">=3.9" +files = [ + {file = "pre_commit-3.7.0-py2.py3-none-any.whl", hash = "sha256:5eae9e10c2b5ac51577c3452ec0a490455c45a0533f7960f993a0d01e59decab"}, + {file = "pre_commit-3.7.0.tar.gz", hash = "sha256:e209d61b8acdcf742404408531f0c37d49d2c734fd7cff2d6076083d191cb060"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + +[[package]] +name = "pydantic" +version = "2.6.4" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic-2.6.4-py3-none-any.whl", hash = "sha256:cc46fce86607580867bdc3361ad462bab9c222ef042d3da86f2fb333e1d916c5"}, + {file = "pydantic-2.6.4.tar.gz", hash = "sha256:b1704e0847db01817624a6b86766967f552dd9dbf3afba4004409f908dcc84e6"}, +] + +[package.dependencies] +annotated-types = ">=0.4.0" +pydantic-core = "2.16.3" +typing-extensions = ">=4.6.1" + +[package.extras] +email = ["email-validator (>=2.0.0)"] + +[[package]] +name = "pydantic-core" +version = "2.16.3" +description = "" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic_core-2.16.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:75b81e678d1c1ede0785c7f46690621e4c6e63ccd9192af1f0bd9d504bbb6bf4"}, + {file = "pydantic_core-2.16.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9c865a7ee6f93783bd5d781af5a4c43dadc37053a5b42f7d18dc019f8c9d2bd1"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:162e498303d2b1c036b957a1278fa0899d02b2842f1ff901b6395104c5554a45"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2f583bd01bbfbff4eaee0868e6fc607efdfcc2b03c1c766b06a707abbc856187"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b926dd38db1519ed3043a4de50214e0d600d404099c3392f098a7f9d75029ff8"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:716b542728d4c742353448765aa7cdaa519a7b82f9564130e2b3f6766018c9ec"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc4ad7f7ee1a13d9cb49d8198cd7d7e3aa93e425f371a68235f784e99741561f"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bd87f48924f360e5d1c5f770d6155ce0e7d83f7b4e10c2f9ec001c73cf475c99"}, + {file = "pydantic_core-2.16.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0df446663464884297c793874573549229f9eca73b59360878f382a0fc085979"}, + {file = "pydantic_core-2.16.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4df8a199d9f6afc5ae9a65f8f95ee52cae389a8c6b20163762bde0426275b7db"}, + {file = "pydantic_core-2.16.3-cp310-none-win32.whl", hash = "sha256:456855f57b413f077dff513a5a28ed838dbbb15082ba00f80750377eed23d132"}, + {file = "pydantic_core-2.16.3-cp310-none-win_amd64.whl", hash = "sha256:732da3243e1b8d3eab8c6ae23ae6a58548849d2e4a4e03a1924c8ddf71a387cb"}, + {file = "pydantic_core-2.16.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:519ae0312616026bf4cedc0fe459e982734f3ca82ee8c7246c19b650b60a5ee4"}, + {file = "pydantic_core-2.16.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b3992a322a5617ded0a9f23fd06dbc1e4bd7cf39bc4ccf344b10f80af58beacd"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d62da299c6ecb04df729e4b5c52dc0d53f4f8430b4492b93aa8de1f541c4aac"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2acca2be4bb2f2147ada8cac612f8a98fc09f41c89f87add7256ad27332c2fda"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b662180108c55dfbf1280d865b2d116633d436cfc0bba82323554873967b340"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e7c6ed0dc9d8e65f24f5824291550139fe6f37fac03788d4580da0d33bc00c97"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b1bb0827f56654b4437955555dc3aeeebeddc47c2d7ed575477f082622c49e"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e56f8186d6210ac7ece503193ec84104da7ceb98f68ce18c07282fcc2452e76f"}, + {file = "pydantic_core-2.16.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:936e5db01dd49476fa8f4383c259b8b1303d5dd5fb34c97de194560698cc2c5e"}, + {file = "pydantic_core-2.16.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:33809aebac276089b78db106ee692bdc9044710e26f24a9a2eaa35a0f9fa70ba"}, + {file = "pydantic_core-2.16.3-cp311-none-win32.whl", hash = "sha256:ded1c35f15c9dea16ead9bffcde9bb5c7c031bff076355dc58dcb1cb436c4721"}, + {file = "pydantic_core-2.16.3-cp311-none-win_amd64.whl", hash = "sha256:d89ca19cdd0dd5f31606a9329e309d4fcbb3df860960acec32630297d61820df"}, + {file = "pydantic_core-2.16.3-cp311-none-win_arm64.whl", hash = "sha256:6162f8d2dc27ba21027f261e4fa26f8bcb3cf9784b7f9499466a311ac284b5b9"}, + {file = "pydantic_core-2.16.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0f56ae86b60ea987ae8bcd6654a887238fd53d1384f9b222ac457070b7ac4cff"}, + {file = "pydantic_core-2.16.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9bd22a2a639e26171068f8ebb5400ce2c1bc7d17959f60a3b753ae13c632975"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4204e773b4b408062960e65468d5346bdfe139247ee5f1ca2a378983e11388a2"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f651dd19363c632f4abe3480a7c87a9773be27cfe1341aef06e8759599454120"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aaf09e615a0bf98d406657e0008e4a8701b11481840be7d31755dc9f97c44053"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8e47755d8152c1ab5b55928ab422a76e2e7b22b5ed8e90a7d584268dd49e9c6b"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:500960cb3a0543a724a81ba859da816e8cf01b0e6aaeedf2c3775d12ee49cade"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cf6204fe865da605285c34cf1172879d0314ff267b1c35ff59de7154f35fdc2e"}, + {file = "pydantic_core-2.16.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d33dd21f572545649f90c38c227cc8631268ba25c460b5569abebdd0ec5974ca"}, + {file = "pydantic_core-2.16.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:49d5d58abd4b83fb8ce763be7794d09b2f50f10aa65c0f0c1696c677edeb7cbf"}, + {file = "pydantic_core-2.16.3-cp312-none-win32.whl", hash = "sha256:f53aace168a2a10582e570b7736cc5bef12cae9cf21775e3eafac597e8551fbe"}, + {file = "pydantic_core-2.16.3-cp312-none-win_amd64.whl", hash = "sha256:0d32576b1de5a30d9a97f300cc6a3f4694c428d956adbc7e6e2f9cad279e45ed"}, + {file = "pydantic_core-2.16.3-cp312-none-win_arm64.whl", hash = "sha256:ec08be75bb268473677edb83ba71e7e74b43c008e4a7b1907c6d57e940bf34b6"}, + {file = "pydantic_core-2.16.3-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:b1f6f5938d63c6139860f044e2538baeee6f0b251a1816e7adb6cbce106a1f01"}, + {file = "pydantic_core-2.16.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2a1ef6a36fdbf71538142ed604ad19b82f67b05749512e47f247a6ddd06afdc7"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:704d35ecc7e9c31d48926150afada60401c55efa3b46cd1ded5a01bdffaf1d48"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d937653a696465677ed583124b94a4b2d79f5e30b2c46115a68e482c6a591c8a"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9803edf8e29bd825f43481f19c37f50d2b01899448273b3a7758441b512acf8"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:72282ad4892a9fb2da25defeac8c2e84352c108705c972db82ab121d15f14e6d"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f752826b5b8361193df55afcdf8ca6a57d0232653494ba473630a83ba50d8c9"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4384a8f68ddb31a0b0c3deae88765f5868a1b9148939c3f4121233314ad5532c"}, + {file = "pydantic_core-2.16.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a4b2bf78342c40b3dc830880106f54328928ff03e357935ad26c7128bbd66ce8"}, + {file = "pydantic_core-2.16.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:13dcc4802961b5f843a9385fc821a0b0135e8c07fc3d9949fd49627c1a5e6ae5"}, + {file = "pydantic_core-2.16.3-cp38-none-win32.whl", hash = "sha256:e3e70c94a0c3841e6aa831edab1619ad5c511199be94d0c11ba75fe06efe107a"}, + {file = "pydantic_core-2.16.3-cp38-none-win_amd64.whl", hash = "sha256:ecdf6bf5f578615f2e985a5e1f6572e23aa632c4bd1dc67f8f406d445ac115ed"}, + {file = "pydantic_core-2.16.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:bda1ee3e08252b8d41fa5537413ffdddd58fa73107171a126d3b9ff001b9b820"}, + {file = "pydantic_core-2.16.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:21b888c973e4f26b7a96491c0965a8a312e13be108022ee510248fe379a5fa23"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be0ec334369316fa73448cc8c982c01e5d2a81c95969d58b8f6e272884df0074"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5b6079cc452a7c53dd378c6f881ac528246b3ac9aae0f8eef98498a75657805"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ee8d5f878dccb6d499ba4d30d757111847b6849ae07acdd1205fffa1fc1253c"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7233d65d9d651242a68801159763d09e9ec96e8a158dbf118dc090cd77a104c9"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6119dc90483a5cb50a1306adb8d52c66e447da88ea44f323e0ae1a5fcb14256"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:578114bc803a4c1ff9946d977c221e4376620a46cf78da267d946397dc9514a8"}, + {file = "pydantic_core-2.16.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d8f99b147ff3fcf6b3cc60cb0c39ea443884d5559a30b1481e92495f2310ff2b"}, + {file = "pydantic_core-2.16.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4ac6b4ce1e7283d715c4b729d8f9dab9627586dafce81d9eaa009dd7f25dd972"}, + {file = "pydantic_core-2.16.3-cp39-none-win32.whl", hash = "sha256:e7774b570e61cb998490c5235740d475413a1f6de823169b4cf94e2fe9e9f6b2"}, + {file = "pydantic_core-2.16.3-cp39-none-win_amd64.whl", hash = "sha256:9091632a25b8b87b9a605ec0e61f241c456e9248bfdcf7abdf344fdb169c81cf"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:36fa178aacbc277bc6b62a2c3da95226520da4f4e9e206fdf076484363895d2c"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:dcca5d2bf65c6fb591fff92da03f94cd4f315972f97c21975398bd4bd046854a"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a72fb9963cba4cd5793854fd12f4cfee731e86df140f59ff52a49b3552db241"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b60cc1a081f80a2105a59385b92d82278b15d80ebb3adb200542ae165cd7d183"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cbcc558401de90a746d02ef330c528f2e668c83350f045833543cd57ecead1ad"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:fee427241c2d9fb7192b658190f9f5fd6dfe41e02f3c1489d2ec1e6a5ab1e04a"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f4cb85f693044e0f71f394ff76c98ddc1bc0953e48c061725e540396d5c8a2e1"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b29eeb887aa931c2fcef5aa515d9d176d25006794610c264ddc114c053bf96fe"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a425479ee40ff021f8216c9d07a6a3b54b31c8267c6e17aa88b70d7ebd0e5e5b"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:5c5cbc703168d1b7a838668998308018a2718c2130595e8e190220238addc96f"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99b6add4c0b39a513d323d3b93bc173dac663c27b99860dd5bf491b240d26137"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f76ee558751746d6a38f89d60b6228fa174e5172d143886af0f85aa306fd89"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:00ee1c97b5364b84cb0bd82e9bbf645d5e2871fb8c58059d158412fee2d33d8a"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:287073c66748f624be4cef893ef9174e3eb88fe0b8a78dc22e88eca4bc357ca6"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ed25e1835c00a332cb10c683cd39da96a719ab1dfc08427d476bce41b92531fc"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:86b3d0033580bd6bbe07590152007275bd7af95f98eaa5bd36f3da219dcd93da"}, + {file = "pydantic_core-2.16.3.tar.gz", hash = "sha256:1cac689f80a3abab2d3c0048b29eea5751114054f032a941a32de4c852c59cad"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + +[[package]] +name = "pydantic-settings" +version = "2.2.1" +description = "Settings management using Pydantic" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic_settings-2.2.1-py3-none-any.whl", hash = "sha256:0235391d26db4d2190cb9b31051c4b46882d28a51533f97440867f012d4da091"}, + {file = "pydantic_settings-2.2.1.tar.gz", hash = "sha256:00b9f6a5e95553590434c0fa01ead0b216c3e10bc54ae02e37f359948643c5ed"}, +] + +[package.dependencies] +pydantic = ">=2.3.0" +python-dotenv = ">=0.21.0" + +[package.extras] +toml = ["tomli (>=2.0.1)"] +yaml = ["pyyaml (>=6.0.1)"] + +[[package]] +name = "pytest" +version = "8.1.1" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-8.1.1-py3-none-any.whl", hash = "sha256:2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7"}, + {file = "pytest-8.1.1.tar.gz", hash = "sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.4,<2.0" + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "python-dotenv" +version = "1.0.1" +description = "Read key-value pairs from a .env file and set them as environment variables" +optional = false +python-versions = ">=3.8" +files = [ + {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, + {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, +] + +[package.extras] +cli = ["click (>=5.0)"] + +[[package]] +name = "pyyaml" +version = "6.0.1" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, + {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, +] + +[[package]] +name = "setuptools" +version = "69.2.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "setuptools-69.2.0-py3-none-any.whl", hash = "sha256:c21c49fb1042386df081cb5d86759792ab89efca84cf114889191cd09aacc80c"}, + {file = "setuptools-69.2.0.tar.gz", hash = "sha256:0ff4183f8f42cd8fa3acea16c45205521a4ef28f73c6391d8a25e92893134f2e"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] + +[[package]] +name = "sniffio" +version = "1.3.1" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.29" +description = "Database Abstraction Library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "SQLAlchemy-2.0.29-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4c142852ae192e9fe5aad5c350ea6befe9db14370b34047e1f0f7cf99e63c63b"}, + {file = "SQLAlchemy-2.0.29-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:99a1e69d4e26f71e750e9ad6fdc8614fbddb67cfe2173a3628a2566034e223c7"}, + {file = "SQLAlchemy-2.0.29-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ef3fbccb4058355053c51b82fd3501a6e13dd808c8d8cd2561e610c5456013c"}, + {file = "SQLAlchemy-2.0.29-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d6753305936eddc8ed190e006b7bb33a8f50b9854823485eed3a886857ab8d1"}, + {file = "SQLAlchemy-2.0.29-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0f3ca96af060a5250a8ad5a63699180bc780c2edf8abf96c58af175921df847a"}, + {file = "SQLAlchemy-2.0.29-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c4520047006b1d3f0d89e0532978c0688219857eb2fee7c48052560ae76aca1e"}, + {file = "SQLAlchemy-2.0.29-cp310-cp310-win32.whl", hash = "sha256:b2a0e3cf0caac2085ff172c3faacd1e00c376e6884b5bc4dd5b6b84623e29e4f"}, + {file = "SQLAlchemy-2.0.29-cp310-cp310-win_amd64.whl", hash = "sha256:01d10638a37460616708062a40c7b55f73e4d35eaa146781c683e0fa7f6c43fb"}, + {file = "SQLAlchemy-2.0.29-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:308ef9cb41d099099fffc9d35781638986870b29f744382904bf9c7dadd08513"}, + {file = "SQLAlchemy-2.0.29-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:296195df68326a48385e7a96e877bc19aa210e485fa381c5246bc0234c36c78e"}, + {file = "SQLAlchemy-2.0.29-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a13b917b4ffe5a0a31b83d051d60477819ddf18276852ea68037a144a506efb9"}, + {file = "SQLAlchemy-2.0.29-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f6d971255d9ddbd3189e2e79d743ff4845c07f0633adfd1de3f63d930dbe673"}, + {file = "SQLAlchemy-2.0.29-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:61405ea2d563407d316c63a7b5271ae5d274a2a9fbcd01b0aa5503635699fa1e"}, + {file = "SQLAlchemy-2.0.29-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:de7202ffe4d4a8c1e3cde1c03e01c1a3772c92858837e8f3879b497158e4cb44"}, + {file = "SQLAlchemy-2.0.29-cp311-cp311-win32.whl", hash = "sha256:b5d7ed79df55a731749ce65ec20d666d82b185fa4898430b17cb90c892741520"}, + {file = "SQLAlchemy-2.0.29-cp311-cp311-win_amd64.whl", hash = "sha256:205f5a2b39d7c380cbc3b5dcc8f2762fb5bcb716838e2d26ccbc54330775b003"}, + {file = "SQLAlchemy-2.0.29-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d96710d834a6fb31e21381c6d7b76ec729bd08c75a25a5184b1089141356171f"}, + {file = "SQLAlchemy-2.0.29-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:52de4736404e53c5c6a91ef2698c01e52333988ebdc218f14c833237a0804f1b"}, + {file = "SQLAlchemy-2.0.29-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c7b02525ede2a164c5fa5014915ba3591730f2cc831f5be9ff3b7fd3e30958e"}, + {file = "SQLAlchemy-2.0.29-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dfefdb3e54cd15f5d56fd5ae32f1da2d95d78319c1f6dfb9bcd0eb15d603d5d"}, + {file = "SQLAlchemy-2.0.29-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a88913000da9205b13f6f195f0813b6ffd8a0c0c2bd58d499e00a30eb508870c"}, + {file = "SQLAlchemy-2.0.29-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fecd5089c4be1bcc37c35e9aa678938d2888845a134dd016de457b942cf5a758"}, + {file = "SQLAlchemy-2.0.29-cp312-cp312-win32.whl", hash = "sha256:8197d6f7a3d2b468861ebb4c9f998b9df9e358d6e1cf9c2a01061cb9b6cf4e41"}, + {file = "SQLAlchemy-2.0.29-cp312-cp312-win_amd64.whl", hash = "sha256:9b19836ccca0d321e237560e475fd99c3d8655d03da80c845c4da20dda31b6e1"}, + {file = "SQLAlchemy-2.0.29-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:87a1d53a5382cdbbf4b7619f107cc862c1b0a4feb29000922db72e5a66a5ffc0"}, + {file = "SQLAlchemy-2.0.29-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a0732dffe32333211801b28339d2a0babc1971bc90a983e3035e7b0d6f06b93"}, + {file = "SQLAlchemy-2.0.29-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90453597a753322d6aa770c5935887ab1fc49cc4c4fdd436901308383d698b4b"}, + {file = "SQLAlchemy-2.0.29-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ea311d4ee9a8fa67f139c088ae9f905fcf0277d6cd75c310a21a88bf85e130f5"}, + {file = "SQLAlchemy-2.0.29-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:5f20cb0a63a3e0ec4e169aa8890e32b949c8145983afa13a708bc4b0a1f30e03"}, + {file = "SQLAlchemy-2.0.29-cp37-cp37m-win32.whl", hash = "sha256:e5bbe55e8552019c6463709b39634a5fc55e080d0827e2a3a11e18eb73f5cdbd"}, + {file = "SQLAlchemy-2.0.29-cp37-cp37m-win_amd64.whl", hash = "sha256:c2f9c762a2735600654c654bf48dad388b888f8ce387b095806480e6e4ff6907"}, + {file = "SQLAlchemy-2.0.29-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7e614d7a25a43a9f54fcce4675c12761b248547f3d41b195e8010ca7297c369c"}, + {file = "SQLAlchemy-2.0.29-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:471fcb39c6adf37f820350c28aac4a7df9d3940c6548b624a642852e727ea586"}, + {file = "SQLAlchemy-2.0.29-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:988569c8732f54ad3234cf9c561364221a9e943b78dc7a4aaf35ccc2265f1930"}, + {file = "SQLAlchemy-2.0.29-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dddaae9b81c88083e6437de95c41e86823d150f4ee94bf24e158a4526cbead01"}, + {file = "SQLAlchemy-2.0.29-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:334184d1ab8f4c87f9652b048af3f7abea1c809dfe526fb0435348a6fef3d380"}, + {file = "SQLAlchemy-2.0.29-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:38b624e5cf02a69b113c8047cf7f66b5dfe4a2ca07ff8b8716da4f1b3ae81567"}, + {file = "SQLAlchemy-2.0.29-cp38-cp38-win32.whl", hash = "sha256:bab41acf151cd68bc2b466deae5deeb9e8ae9c50ad113444151ad965d5bf685b"}, + {file = "SQLAlchemy-2.0.29-cp38-cp38-win_amd64.whl", hash = "sha256:52c8011088305476691b8750c60e03b87910a123cfd9ad48576d6414b6ec2a1d"}, + {file = "SQLAlchemy-2.0.29-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3071ad498896907a5ef756206b9dc750f8e57352113c19272bdfdc429c7bd7de"}, + {file = "SQLAlchemy-2.0.29-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dba622396a3170974f81bad49aacebd243455ec3cc70615aeaef9e9613b5bca5"}, + {file = "SQLAlchemy-2.0.29-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b184e3de58009cc0bf32e20f137f1ec75a32470f5fede06c58f6c355ed42a72"}, + {file = "SQLAlchemy-2.0.29-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c37f1050feb91f3d6c32f864d8e114ff5545a4a7afe56778d76a9aec62638ba"}, + {file = "SQLAlchemy-2.0.29-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bda7ce59b06d0f09afe22c56714c65c957b1068dee3d5e74d743edec7daba552"}, + {file = "SQLAlchemy-2.0.29-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:25664e18bef6dc45015b08f99c63952a53a0a61f61f2e48a9e70cec27e55f699"}, + {file = "SQLAlchemy-2.0.29-cp39-cp39-win32.whl", hash = "sha256:77d29cb6c34b14af8a484e831ab530c0f7188f8efed1c6a833a2c674bf3c26ec"}, + {file = "SQLAlchemy-2.0.29-cp39-cp39-win_amd64.whl", hash = "sha256:04c487305ab035a9548f573763915189fc0fe0824d9ba28433196f8436f1449c"}, + {file = "SQLAlchemy-2.0.29-py3-none-any.whl", hash = "sha256:dc4ee2d4ee43251905f88637d5281a8d52e916a021384ec10758826f5cbae305"}, + {file = "SQLAlchemy-2.0.29.tar.gz", hash = "sha256:bd9566b8e58cabd700bc367b60e90d9349cd16f0984973f98a9a09f9c64e86f0"}, +] + +[package.dependencies] +greenlet = {version = "!=0.4.17", markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\""} +typing-extensions = ">=4.6.0" + +[package.extras] +aiomysql = ["aiomysql (>=0.2.0)", "greenlet (!=0.4.17)"] +aioodbc = ["aioodbc", "greenlet (!=0.4.17)"] +aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"] +asyncio = ["greenlet (!=0.4.17)"] +asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (!=0.4.17)"] +mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5)"] +mssql = ["pyodbc"] +mssql-pymssql = ["pymssql"] +mssql-pyodbc = ["pyodbc"] +mypy = ["mypy (>=0.910)"] +mysql = ["mysqlclient (>=1.4.0)"] +mysql-connector = ["mysql-connector-python"] +oracle = ["cx_oracle (>=8)"] +oracle-oracledb = ["oracledb (>=1.0.1)"] +postgresql = ["psycopg2 (>=2.7)"] +postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] +postgresql-pg8000 = ["pg8000 (>=1.29.1)"] +postgresql-psycopg = ["psycopg (>=3.0.7)"] +postgresql-psycopg2binary = ["psycopg2-binary"] +postgresql-psycopg2cffi = ["psycopg2cffi"] +postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"] +pymysql = ["pymysql"] +sqlcipher = ["sqlcipher3_binary"] + +[[package]] +name = "starlette" +version = "0.37.2" +description = "The little ASGI library that shines." +optional = false +python-versions = ">=3.8" +files = [ + {file = "starlette-0.37.2-py3-none-any.whl", hash = "sha256:6fe59f29268538e5d0d182f2791a479a0c64638e6935d1c6989e63fb2699c6ee"}, + {file = "starlette-0.37.2.tar.gz", hash = "sha256:9af890290133b79fc3db55474ade20f6220a364a0402e0b556e7cd5e1e093823"}, +] + +[package.dependencies] +anyio = ">=3.4.0,<5" + +[package.extras] +full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"] + +[[package]] +name = "typing-extensions" +version = "4.10.0" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475"}, + {file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"}, +] + +[[package]] +name = "uvicorn" +version = "0.29.0" +description = "The lightning-fast ASGI server." +optional = false +python-versions = ">=3.8" +files = [ + {file = "uvicorn-0.29.0-py3-none-any.whl", hash = "sha256:2c2aac7ff4f4365c206fd773a39bf4ebd1047c238f8b8268ad996829323473de"}, + {file = "uvicorn-0.29.0.tar.gz", hash = "sha256:6a69214c0b6a087462412670b3ef21224fa48cae0e452b5883e8e8bdfdd11dd0"}, +] + +[package.dependencies] +click = ">=7.0" +colorama = {version = ">=0.4", optional = true, markers = "sys_platform == \"win32\" and extra == \"standard\""} +h11 = ">=0.8" +httptools = {version = ">=0.5.0", optional = true, markers = "extra == \"standard\""} +python-dotenv = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} +pyyaml = {version = ">=5.1", optional = true, markers = "extra == \"standard\""} +uvloop = {version = ">=0.14.0,<0.15.0 || >0.15.0,<0.15.1 || >0.15.1", optional = true, markers = "(sys_platform != \"win32\" and sys_platform != \"cygwin\") and platform_python_implementation != \"PyPy\" and extra == \"standard\""} +watchfiles = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} +websockets = {version = ">=10.4", optional = true, markers = "extra == \"standard\""} + +[package.extras] +standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] + +[[package]] +name = "uvloop" +version = "0.19.0" +description = "Fast implementation of asyncio event loop on top of libuv" +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "uvloop-0.19.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:de4313d7f575474c8f5a12e163f6d89c0a878bc49219641d49e6f1444369a90e"}, + {file = "uvloop-0.19.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5588bd21cf1fcf06bded085f37e43ce0e00424197e7c10e77afd4bbefffef428"}, + {file = "uvloop-0.19.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b1fd71c3843327f3bbc3237bedcdb6504fd50368ab3e04d0410e52ec293f5b8"}, + {file = "uvloop-0.19.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a05128d315e2912791de6088c34136bfcdd0c7cbc1cf85fd6fd1bb321b7c849"}, + {file = "uvloop-0.19.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:cd81bdc2b8219cb4b2556eea39d2e36bfa375a2dd021404f90a62e44efaaf957"}, + {file = "uvloop-0.19.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5f17766fb6da94135526273080f3455a112f82570b2ee5daa64d682387fe0dcd"}, + {file = "uvloop-0.19.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4ce6b0af8f2729a02a5d1575feacb2a94fc7b2e983868b009d51c9a9d2149bef"}, + {file = "uvloop-0.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:31e672bb38b45abc4f26e273be83b72a0d28d074d5b370fc4dcf4c4eb15417d2"}, + {file = "uvloop-0.19.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:570fc0ed613883d8d30ee40397b79207eedd2624891692471808a95069a007c1"}, + {file = "uvloop-0.19.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5138821e40b0c3e6c9478643b4660bd44372ae1e16a322b8fc07478f92684e24"}, + {file = "uvloop-0.19.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:91ab01c6cd00e39cde50173ba4ec68a1e578fee9279ba64f5221810a9e786533"}, + {file = "uvloop-0.19.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:47bf3e9312f63684efe283f7342afb414eea4d3011542155c7e625cd799c3b12"}, + {file = "uvloop-0.19.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:da8435a3bd498419ee8c13c34b89b5005130a476bda1d6ca8cfdde3de35cd650"}, + {file = "uvloop-0.19.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:02506dc23a5d90e04d4f65c7791e65cf44bd91b37f24cfc3ef6cf2aff05dc7ec"}, + {file = "uvloop-0.19.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2693049be9d36fef81741fddb3f441673ba12a34a704e7b4361efb75cf30befc"}, + {file = "uvloop-0.19.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7010271303961c6f0fe37731004335401eb9075a12680738731e9c92ddd96ad6"}, + {file = "uvloop-0.19.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:5daa304d2161d2918fa9a17d5635099a2f78ae5b5960e742b2fcfbb7aefaa593"}, + {file = "uvloop-0.19.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:7207272c9520203fea9b93843bb775d03e1cf88a80a936ce760f60bb5add92f3"}, + {file = "uvloop-0.19.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:78ab247f0b5671cc887c31d33f9b3abfb88d2614b84e4303f1a63b46c046c8bd"}, + {file = "uvloop-0.19.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:472d61143059c84947aa8bb74eabbace30d577a03a1805b77933d6bd13ddebbd"}, + {file = "uvloop-0.19.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45bf4c24c19fb8a50902ae37c5de50da81de4922af65baf760f7c0c42e1088be"}, + {file = "uvloop-0.19.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271718e26b3e17906b28b67314c45d19106112067205119dddbd834c2b7ce797"}, + {file = "uvloop-0.19.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:34175c9fd2a4bc3adc1380e1261f60306344e3407c20a4d684fd5f3be010fa3d"}, + {file = "uvloop-0.19.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e27f100e1ff17f6feeb1f33968bc185bf8ce41ca557deee9d9bbbffeb72030b7"}, + {file = "uvloop-0.19.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:13dfdf492af0aa0a0edf66807d2b465607d11c4fa48f4a1fd41cbea5b18e8e8b"}, + {file = "uvloop-0.19.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6e3d4e85ac060e2342ff85e90d0c04157acb210b9ce508e784a944f852a40e67"}, + {file = "uvloop-0.19.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca4956c9ab567d87d59d49fa3704cf29e37109ad348f2d5223c9bf761a332e7"}, + {file = "uvloop-0.19.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f467a5fd23b4fc43ed86342641f3936a68ded707f4627622fa3f82a120e18256"}, + {file = "uvloop-0.19.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:492e2c32c2af3f971473bc22f086513cedfc66a130756145a931a90c3958cb17"}, + {file = "uvloop-0.19.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2df95fca285a9f5bfe730e51945ffe2fa71ccbfdde3b0da5772b4ee4f2e770d5"}, + {file = "uvloop-0.19.0.tar.gz", hash = "sha256:0246f4fd1bf2bf702e06b0d45ee91677ee5c31242f39aab4ea6fe0c51aedd0fd"}, +] + +[package.extras] +docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] +test = ["Cython (>=0.29.36,<0.30.0)", "aiohttp (==3.9.0b0)", "aiohttp (>=3.8.1)", "flake8 (>=5.0,<6.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=23.0.0,<23.1.0)", "pycodestyle (>=2.9.0,<2.10.0)"] + +[[package]] +name = "virtualenv" +version = "20.25.1" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.7" +files = [ + {file = "virtualenv-20.25.1-py3-none-any.whl", hash = "sha256:961c026ac520bac5f69acb8ea063e8a4f071bcc9457b9c1f28f6b085c511583a"}, + {file = "virtualenv-20.25.1.tar.gz", hash = "sha256:e08e13ecdca7a0bd53798f356d5831434afa5b07b93f0abdf0797b7a06ffe197"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<5" + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] + +[[package]] +name = "watchfiles" +version = "0.21.0" +description = "Simple, modern and high performance file watching and code reload in python." +optional = false +python-versions = ">=3.8" +files = [ + {file = "watchfiles-0.21.0-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:27b4035013f1ea49c6c0b42d983133b136637a527e48c132d368eb19bf1ac6aa"}, + {file = "watchfiles-0.21.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c81818595eff6e92535ff32825f31c116f867f64ff8cdf6562cd1d6b2e1e8f3e"}, + {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6c107ea3cf2bd07199d66f156e3ea756d1b84dfd43b542b2d870b77868c98c03"}, + {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d9ac347653ebd95839a7c607608703b20bc07e577e870d824fa4801bc1cb124"}, + {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5eb86c6acb498208e7663ca22dbe68ca2cf42ab5bf1c776670a50919a56e64ab"}, + {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f564bf68404144ea6b87a78a3f910cc8de216c6b12a4cf0b27718bf4ec38d303"}, + {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d0f32ebfaa9c6011f8454994f86108c2eb9c79b8b7de00b36d558cadcedaa3d"}, + {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6d45d9b699ecbac6c7bd8e0a2609767491540403610962968d258fd6405c17c"}, + {file = "watchfiles-0.21.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:aff06b2cac3ef4616e26ba17a9c250c1fe9dd8a5d907d0193f84c499b1b6e6a9"}, + {file = "watchfiles-0.21.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d9792dff410f266051025ecfaa927078b94cc7478954b06796a9756ccc7e14a9"}, + {file = "watchfiles-0.21.0-cp310-none-win32.whl", hash = "sha256:214cee7f9e09150d4fb42e24919a1e74d8c9b8a9306ed1474ecaddcd5479c293"}, + {file = "watchfiles-0.21.0-cp310-none-win_amd64.whl", hash = "sha256:1ad7247d79f9f55bb25ab1778fd47f32d70cf36053941f07de0b7c4e96b5d235"}, + {file = "watchfiles-0.21.0-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:668c265d90de8ae914f860d3eeb164534ba2e836811f91fecc7050416ee70aa7"}, + {file = "watchfiles-0.21.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3a23092a992e61c3a6a70f350a56db7197242f3490da9c87b500f389b2d01eef"}, + {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e7941bbcfdded9c26b0bf720cb7e6fd803d95a55d2c14b4bd1f6a2772230c586"}, + {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:11cd0c3100e2233e9c53106265da31d574355c288e15259c0d40a4405cbae317"}, + {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d78f30cbe8b2ce770160d3c08cff01b2ae9306fe66ce899b73f0409dc1846c1b"}, + {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6674b00b9756b0af620aa2a3346b01f8e2a3dc729d25617e1b89cf6af4a54eb1"}, + {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd7ac678b92b29ba630d8c842d8ad6c555abda1b9ef044d6cc092dacbfc9719d"}, + {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c873345680c1b87f1e09e0eaf8cf6c891b9851d8b4d3645e7efe2ec20a20cc7"}, + {file = "watchfiles-0.21.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:49f56e6ecc2503e7dbe233fa328b2be1a7797d31548e7a193237dcdf1ad0eee0"}, + {file = "watchfiles-0.21.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:02d91cbac553a3ad141db016e3350b03184deaafeba09b9d6439826ee594b365"}, + {file = "watchfiles-0.21.0-cp311-none-win32.whl", hash = "sha256:ebe684d7d26239e23d102a2bad2a358dedf18e462e8808778703427d1f584400"}, + {file = "watchfiles-0.21.0-cp311-none-win_amd64.whl", hash = "sha256:4566006aa44cb0d21b8ab53baf4b9c667a0ed23efe4aaad8c227bfba0bf15cbe"}, + {file = "watchfiles-0.21.0-cp311-none-win_arm64.whl", hash = "sha256:c550a56bf209a3d987d5a975cdf2063b3389a5d16caf29db4bdddeae49f22078"}, + {file = "watchfiles-0.21.0-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:51ddac60b96a42c15d24fbdc7a4bfcd02b5a29c047b7f8bf63d3f6f5a860949a"}, + {file = "watchfiles-0.21.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:511f0b034120cd1989932bf1e9081aa9fb00f1f949fbd2d9cab6264916ae89b1"}, + {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:cfb92d49dbb95ec7a07511bc9efb0faff8fe24ef3805662b8d6808ba8409a71a"}, + {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f92944efc564867bbf841c823c8b71bb0be75e06b8ce45c084b46411475a915"}, + {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:642d66b75eda909fd1112d35c53816d59789a4b38c141a96d62f50a3ef9b3360"}, + {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d23bcd6c8eaa6324fe109d8cac01b41fe9a54b8c498af9ce464c1aeeb99903d6"}, + {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18d5b4da8cf3e41895b34e8c37d13c9ed294954907929aacd95153508d5d89d7"}, + {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b8d1eae0f65441963d805f766c7e9cd092f91e0c600c820c764a4ff71a0764c"}, + {file = "watchfiles-0.21.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1fd9a5205139f3c6bb60d11f6072e0552f0a20b712c85f43d42342d162be1235"}, + {file = "watchfiles-0.21.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a1e3014a625bcf107fbf38eece0e47fa0190e52e45dc6eee5a8265ddc6dc5ea7"}, + {file = "watchfiles-0.21.0-cp312-none-win32.whl", hash = "sha256:9d09869f2c5a6f2d9df50ce3064b3391d3ecb6dced708ad64467b9e4f2c9bef3"}, + {file = "watchfiles-0.21.0-cp312-none-win_amd64.whl", hash = "sha256:18722b50783b5e30a18a8a5db3006bab146d2b705c92eb9a94f78c72beb94094"}, + {file = "watchfiles-0.21.0-cp312-none-win_arm64.whl", hash = "sha256:a3b9bec9579a15fb3ca2d9878deae789df72f2b0fdaf90ad49ee389cad5edab6"}, + {file = "watchfiles-0.21.0-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:4ea10a29aa5de67de02256a28d1bf53d21322295cb00bd2d57fcd19b850ebd99"}, + {file = "watchfiles-0.21.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:40bca549fdc929b470dd1dbfcb47b3295cb46a6d2c90e50588b0a1b3bd98f429"}, + {file = "watchfiles-0.21.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9b37a7ba223b2f26122c148bb8d09a9ff312afca998c48c725ff5a0a632145f7"}, + {file = "watchfiles-0.21.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec8c8900dc5c83650a63dd48c4d1d245343f904c4b64b48798c67a3767d7e165"}, + {file = "watchfiles-0.21.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8ad3fe0a3567c2f0f629d800409cd528cb6251da12e81a1f765e5c5345fd0137"}, + {file = "watchfiles-0.21.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d353c4cfda586db2a176ce42c88f2fc31ec25e50212650c89fdd0f560ee507b"}, + {file = "watchfiles-0.21.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:83a696da8922314ff2aec02987eefb03784f473281d740bf9170181829133765"}, + {file = "watchfiles-0.21.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a03651352fc20975ee2a707cd2d74a386cd303cc688f407296064ad1e6d1562"}, + {file = "watchfiles-0.21.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3ad692bc7792be8c32918c699638b660c0de078a6cbe464c46e1340dadb94c19"}, + {file = "watchfiles-0.21.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06247538e8253975bdb328e7683f8515ff5ff041f43be6c40bff62d989b7d0b0"}, + {file = "watchfiles-0.21.0-cp38-none-win32.whl", hash = "sha256:9a0aa47f94ea9a0b39dd30850b0adf2e1cd32a8b4f9c7aa443d852aacf9ca214"}, + {file = "watchfiles-0.21.0-cp38-none-win_amd64.whl", hash = "sha256:8d5f400326840934e3507701f9f7269247f7c026d1b6cfd49477d2be0933cfca"}, + {file = "watchfiles-0.21.0-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:7f762a1a85a12cc3484f77eee7be87b10f8c50b0b787bb02f4e357403cad0c0e"}, + {file = "watchfiles-0.21.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6e9be3ef84e2bb9710f3f777accce25556f4a71e15d2b73223788d528fcc2052"}, + {file = "watchfiles-0.21.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4c48a10d17571d1275701e14a601e36959ffada3add8cdbc9e5061a6e3579a5d"}, + {file = "watchfiles-0.21.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c889025f59884423428c261f212e04d438de865beda0b1e1babab85ef4c0f01"}, + {file = "watchfiles-0.21.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:66fac0c238ab9a2e72d026b5fb91cb902c146202bbd29a9a1a44e8db7b710b6f"}, + {file = "watchfiles-0.21.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b4a21f71885aa2744719459951819e7bf5a906a6448a6b2bbce8e9cc9f2c8128"}, + {file = "watchfiles-0.21.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c9198c989f47898b2c22201756f73249de3748e0fc9de44adaf54a8b259cc0c"}, + {file = "watchfiles-0.21.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8f57c4461cd24fda22493109c45b3980863c58a25b8bec885ca8bea6b8d4b28"}, + {file = "watchfiles-0.21.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:853853cbf7bf9408b404754b92512ebe3e3a83587503d766d23e6bf83d092ee6"}, + {file = "watchfiles-0.21.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d5b1dc0e708fad9f92c296ab2f948af403bf201db8fb2eb4c8179db143732e49"}, + {file = "watchfiles-0.21.0-cp39-none-win32.whl", hash = "sha256:59137c0c6826bd56c710d1d2bda81553b5e6b7c84d5a676747d80caf0409ad94"}, + {file = "watchfiles-0.21.0-cp39-none-win_amd64.whl", hash = "sha256:6cb8fdc044909e2078c248986f2fc76f911f72b51ea4a4fbbf472e01d14faa58"}, + {file = "watchfiles-0.21.0-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:ab03a90b305d2588e8352168e8c5a1520b721d2d367f31e9332c4235b30b8994"}, + {file = "watchfiles-0.21.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:927c589500f9f41e370b0125c12ac9e7d3a2fd166b89e9ee2828b3dda20bfe6f"}, + {file = "watchfiles-0.21.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bd467213195e76f838caf2c28cd65e58302d0254e636e7c0fca81efa4a2e62c"}, + {file = "watchfiles-0.21.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02b73130687bc3f6bb79d8a170959042eb56eb3a42df3671c79b428cd73f17cc"}, + {file = "watchfiles-0.21.0-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:08dca260e85ffae975448e344834d765983237ad6dc308231aa16e7933db763e"}, + {file = "watchfiles-0.21.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:3ccceb50c611c433145502735e0370877cced72a6c70fd2410238bcbc7fe51d8"}, + {file = "watchfiles-0.21.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57d430f5fb63fea141ab71ca9c064e80de3a20b427ca2febcbfcef70ff0ce895"}, + {file = "watchfiles-0.21.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dd5fad9b9c0dd89904bbdea978ce89a2b692a7ee8a0ce19b940e538c88a809c"}, + {file = "watchfiles-0.21.0-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:be6dd5d52b73018b21adc1c5d28ac0c68184a64769052dfeb0c5d9998e7f56a2"}, + {file = "watchfiles-0.21.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:b3cab0e06143768499384a8a5efb9c4dc53e19382952859e4802f294214f36ec"}, + {file = "watchfiles-0.21.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c6ed10c2497e5fedadf61e465b3ca12a19f96004c15dcffe4bd442ebadc2d85"}, + {file = "watchfiles-0.21.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43babacef21c519bc6631c5fce2a61eccdfc011b4bcb9047255e9620732c8097"}, + {file = "watchfiles-0.21.0.tar.gz", hash = "sha256:c76c635fabf542bb78524905718c39f736a98e5ab25b23ec6d4abede1a85a6a3"}, +] + +[package.dependencies] +anyio = ">=3.0.0" + +[[package]] +name = "websockets" +version = "12.0" +description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "websockets-12.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d554236b2a2006e0ce16315c16eaa0d628dab009c33b63ea03f41c6107958374"}, + {file = "websockets-12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2d225bb6886591b1746b17c0573e29804619c8f755b5598d875bb4235ea639be"}, + {file = "websockets-12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:eb809e816916a3b210bed3c82fb88eaf16e8afcf9c115ebb2bacede1797d2547"}, + {file = "websockets-12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c588f6abc13f78a67044c6b1273a99e1cf31038ad51815b3b016ce699f0d75c2"}, + {file = "websockets-12.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5aa9348186d79a5f232115ed3fa9020eab66d6c3437d72f9d2c8ac0c6858c558"}, + {file = "websockets-12.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6350b14a40c95ddd53e775dbdbbbc59b124a5c8ecd6fbb09c2e52029f7a9f480"}, + {file = "websockets-12.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:70ec754cc2a769bcd218ed8d7209055667b30860ffecb8633a834dde27d6307c"}, + {file = "websockets-12.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6e96f5ed1b83a8ddb07909b45bd94833b0710f738115751cdaa9da1fb0cb66e8"}, + {file = "websockets-12.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4d87be612cbef86f994178d5186add3d94e9f31cc3cb499a0482b866ec477603"}, + {file = "websockets-12.0-cp310-cp310-win32.whl", hash = "sha256:befe90632d66caaf72e8b2ed4d7f02b348913813c8b0a32fae1cc5fe3730902f"}, + {file = "websockets-12.0-cp310-cp310-win_amd64.whl", hash = "sha256:363f57ca8bc8576195d0540c648aa58ac18cf85b76ad5202b9f976918f4219cf"}, + {file = "websockets-12.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5d873c7de42dea355d73f170be0f23788cf3fa9f7bed718fd2830eefedce01b4"}, + {file = "websockets-12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3f61726cae9f65b872502ff3c1496abc93ffbe31b278455c418492016e2afc8f"}, + {file = "websockets-12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed2fcf7a07334c77fc8a230755c2209223a7cc44fc27597729b8ef5425aa61a3"}, + {file = "websockets-12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e332c210b14b57904869ca9f9bf4ca32f5427a03eeb625da9b616c85a3a506c"}, + {file = "websockets-12.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5693ef74233122f8ebab026817b1b37fe25c411ecfca084b29bc7d6efc548f45"}, + {file = "websockets-12.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e9e7db18b4539a29cc5ad8c8b252738a30e2b13f033c2d6e9d0549b45841c04"}, + {file = "websockets-12.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6e2df67b8014767d0f785baa98393725739287684b9f8d8a1001eb2839031447"}, + {file = "websockets-12.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bea88d71630c5900690fcb03161ab18f8f244805c59e2e0dc4ffadae0a7ee0ca"}, + {file = "websockets-12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dff6cdf35e31d1315790149fee351f9e52978130cef6c87c4b6c9b3baf78bc53"}, + {file = "websockets-12.0-cp311-cp311-win32.whl", hash = "sha256:3e3aa8c468af01d70332a382350ee95f6986db479ce7af14d5e81ec52aa2b402"}, + {file = "websockets-12.0-cp311-cp311-win_amd64.whl", hash = "sha256:25eb766c8ad27da0f79420b2af4b85d29914ba0edf69f547cc4f06ca6f1d403b"}, + {file = "websockets-12.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0e6e2711d5a8e6e482cacb927a49a3d432345dfe7dea8ace7b5790df5932e4df"}, + {file = "websockets-12.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:dbcf72a37f0b3316e993e13ecf32f10c0e1259c28ffd0a85cee26e8549595fbc"}, + {file = "websockets-12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12743ab88ab2af1d17dd4acb4645677cb7063ef4db93abffbf164218a5d54c6b"}, + {file = "websockets-12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b645f491f3c48d3f8a00d1fce07445fab7347fec54a3e65f0725d730d5b99cb"}, + {file = "websockets-12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9893d1aa45a7f8b3bc4510f6ccf8db8c3b62120917af15e3de247f0780294b92"}, + {file = "websockets-12.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f38a7b376117ef7aff996e737583172bdf535932c9ca021746573bce40165ed"}, + {file = "websockets-12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f764ba54e33daf20e167915edc443b6f88956f37fb606449b4a5b10ba42235a5"}, + {file = "websockets-12.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:1e4b3f8ea6a9cfa8be8484c9221ec0257508e3a1ec43c36acdefb2a9c3b00aa2"}, + {file = "websockets-12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9fdf06fd06c32205a07e47328ab49c40fc1407cdec801d698a7c41167ea45113"}, + {file = "websockets-12.0-cp312-cp312-win32.whl", hash = "sha256:baa386875b70cbd81798fa9f71be689c1bf484f65fd6fb08d051a0ee4e79924d"}, + {file = "websockets-12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ae0a5da8f35a5be197f328d4727dbcfafa53d1824fac3d96cdd3a642fe09394f"}, + {file = "websockets-12.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5f6ffe2c6598f7f7207eef9a1228b6f5c818f9f4d53ee920aacd35cec8110438"}, + {file = "websockets-12.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9edf3fc590cc2ec20dc9d7a45108b5bbaf21c0d89f9fd3fd1685e223771dc0b2"}, + {file = "websockets-12.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8572132c7be52632201a35f5e08348137f658e5ffd21f51f94572ca6c05ea81d"}, + {file = "websockets-12.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:604428d1b87edbf02b233e2c207d7d528460fa978f9e391bd8aaf9c8311de137"}, + {file = "websockets-12.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1a9d160fd080c6285e202327aba140fc9a0d910b09e423afff4ae5cbbf1c7205"}, + {file = "websockets-12.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87b4aafed34653e465eb77b7c93ef058516cb5acf3eb21e42f33928616172def"}, + {file = "websockets-12.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b2ee7288b85959797970114deae81ab41b731f19ebcd3bd499ae9ca0e3f1d2c8"}, + {file = "websockets-12.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:7fa3d25e81bfe6a89718e9791128398a50dec6d57faf23770787ff441d851967"}, + {file = "websockets-12.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:a571f035a47212288e3b3519944f6bf4ac7bc7553243e41eac50dd48552b6df7"}, + {file = "websockets-12.0-cp38-cp38-win32.whl", hash = "sha256:3c6cc1360c10c17463aadd29dd3af332d4a1adaa8796f6b0e9f9df1fdb0bad62"}, + {file = "websockets-12.0-cp38-cp38-win_amd64.whl", hash = "sha256:1bf386089178ea69d720f8db6199a0504a406209a0fc23e603b27b300fdd6892"}, + {file = "websockets-12.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ab3d732ad50a4fbd04a4490ef08acd0517b6ae6b77eb967251f4c263011a990d"}, + {file = "websockets-12.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1d9697f3337a89691e3bd8dc56dea45a6f6d975f92e7d5f773bc715c15dde28"}, + {file = "websockets-12.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1df2fbd2c8a98d38a66f5238484405b8d1d16f929bb7a33ed73e4801222a6f53"}, + {file = "websockets-12.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23509452b3bc38e3a057382c2e941d5ac2e01e251acce7adc74011d7d8de434c"}, + {file = "websockets-12.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e5fc14ec6ea568200ea4ef46545073da81900a2b67b3e666f04adf53ad452ec"}, + {file = "websockets-12.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46e71dbbd12850224243f5d2aeec90f0aaa0f2dde5aeeb8fc8df21e04d99eff9"}, + {file = "websockets-12.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b81f90dcc6c85a9b7f29873beb56c94c85d6f0dac2ea8b60d995bd18bf3e2aae"}, + {file = "websockets-12.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:a02413bc474feda2849c59ed2dfb2cddb4cd3d2f03a2fedec51d6e959d9b608b"}, + {file = "websockets-12.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bbe6013f9f791944ed31ca08b077e26249309639313fff132bfbf3ba105673b9"}, + {file = "websockets-12.0-cp39-cp39-win32.whl", hash = "sha256:cbe83a6bbdf207ff0541de01e11904827540aa069293696dd528a6640bd6a5f6"}, + {file = "websockets-12.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc4e7fa5414512b481a2483775a8e8be7803a35b30ca805afa4998a84f9fd9e8"}, + {file = "websockets-12.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:248d8e2446e13c1d4326e0a6a4e9629cb13a11195051a73acf414812700badbd"}, + {file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f44069528d45a933997a6fef143030d8ca8042f0dfaad753e2906398290e2870"}, + {file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c4e37d36f0d19f0a4413d3e18c0d03d0c268ada2061868c1e6f5ab1a6d575077"}, + {file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d829f975fc2e527a3ef2f9c8f25e553eb7bc779c6665e8e1d52aa22800bb38b"}, + {file = "websockets-12.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2c71bd45a777433dd9113847af751aae36e448bc6b8c361a566cb043eda6ec30"}, + {file = "websockets-12.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0bee75f400895aef54157b36ed6d3b308fcab62e5260703add87f44cee9c82a6"}, + {file = "websockets-12.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:423fc1ed29f7512fceb727e2d2aecb952c46aa34895e9ed96071821309951123"}, + {file = "websockets-12.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27a5e9964ef509016759f2ef3f2c1e13f403725a5e6a1775555994966a66e931"}, + {file = "websockets-12.0-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3181df4583c4d3994d31fb235dc681d2aaad744fbdbf94c4802485ececdecf2"}, + {file = "websockets-12.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:b067cb952ce8bf40115f6c19f478dc71c5e719b7fbaa511359795dfd9d1a6468"}, + {file = "websockets-12.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:00700340c6c7ab788f176d118775202aadea7602c5cc6be6ae127761c16d6b0b"}, + {file = "websockets-12.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e469d01137942849cff40517c97a30a93ae79917752b34029f0ec72df6b46399"}, + {file = "websockets-12.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffefa1374cd508d633646d51a8e9277763a9b78ae71324183693959cf94635a7"}, + {file = "websockets-12.0-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba0cab91b3956dfa9f512147860783a1829a8d905ee218a9837c18f683239611"}, + {file = "websockets-12.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2cb388a5bfb56df4d9a406783b7f9dbefb888c09b71629351cc6b036e9259370"}, + {file = "websockets-12.0-py3-none-any.whl", hash = "sha256:dc284bbc8d7c78a6c69e0c7325ab46ee5e40bb4d50e494d8131a07ef47500e9e"}, + {file = "websockets-12.0.tar.gz", hash = "sha256:81df9cbcbb6c260de1e007e58c011bfebe2dafc8435107b0537f393dd38c8b1b"}, +] + +[metadata] +lock-version = "2.0" +python-versions = "^3.12" +content-hash = "984ae526cd54554a0eea20909da100b028b3c43e0d07c9058d9c2ba7c50ec11f" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..a6cba5f --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,27 @@ +[tool.poetry] +name = "quantdb" +version = "0.1.0" +description = "Quantitative DB" +authors = ["tmsincomb "] +license = "MIT" +readme = "README.md" +packages = [{ include = "quantdb" }] + +[tool.poetry.dependencies] +python = "^3.12" +sqlalchemy = "^2.0.29" +pydantic = "^2.6.4" +fastapi = "^0.110.1" +uvicorn = {extras = ["standard"], version = "^0.29.0"} +pydantic-settings = "^2.2.1" + +[tool.poetry.scripts] +quantdb-fastapi = "uvicorn quantdb.main:app --reload" + +[tool.poetry.group.dev.dependencies] +pytest = "^8.1.1" +pre-commit = "^3.7.0" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" From 29bb91034f6181334a4bc20d96701a449137ee06 Mon Sep 17 00:00:00 2001 From: tmsincomb Date: Tue, 2 Apr 2024 14:48:26 -0700 Subject: [PATCH 02/18] dummy fastpi service --- quantdb/__init__.py | 1 + quantdb/config.py | 10 +++++++ quantdb/main.py | 52 +++++++++++++++++++++++++++++++++++ quantdb/mysql_app/__init__.py | 5 ++++ quantdb/mysql_app/database.py | 17 ++++++++++++ 5 files changed, 85 insertions(+) create mode 100644 quantdb/config.py create mode 100644 quantdb/main.py create mode 100644 quantdb/mysql_app/__init__.py create mode 100644 quantdb/mysql_app/database.py diff --git a/quantdb/__init__.py b/quantdb/__init__.py index e69de29..9d48db4 100644 --- a/quantdb/__init__.py +++ b/quantdb/__init__.py @@ -0,0 +1 @@ +from __future__ import annotations diff --git a/quantdb/config.py b/quantdb/config.py new file mode 100644 index 0000000..ad690e0 --- /dev/null +++ b/quantdb/config.py @@ -0,0 +1,10 @@ +from __future__ import annotations + +from pathlib import Path +from typing import Optional + +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class Settings(BaseSettings): + pass diff --git a/quantdb/main.py b/quantdb/main.py new file mode 100644 index 0000000..a6b1d18 --- /dev/null +++ b/quantdb/main.py @@ -0,0 +1,52 @@ +from __future__ import annotations + +from functools import lru_cache +from typing import Any, Generator + +from fastapi import Depends, FastAPI +from sqlalchemy import text +from sqlalchemy.orm import Session # type: ignore + +from quantdb.config import Settings + +from quantdb import mysql_app + +app = FastAPI() + + +@lru_cache() +def get_settings(): + """ + Get settings from mongodb - alternative for test and production credientials + + Returns + ------- + Settings + pydantic BaseSettings model with MongoDB credentials + """ + return Settings() + + +def get_mysql_db() -> Generator[Session, Any, None]: + db = mysql_app.database.SessionLocal() + try: + yield db + finally: + db.close() + + +@app.get( + "/example", + status_code=200, +) +def get_example( + mysql_db: Session = Depends(get_mysql_db), # type: ignore +): + query = text( + """ + select * from limit 10 + """ + ) + result = mysql_db.execute(query) + data = result.mappings().all() + return data diff --git a/quantdb/mysql_app/__init__.py b/quantdb/mysql_app/__init__.py new file mode 100644 index 0000000..4064ab4 --- /dev/null +++ b/quantdb/mysql_app/__init__.py @@ -0,0 +1,5 @@ +from __future__ import annotations + +from . import database + +__all__ = ["database"] diff --git a/quantdb/mysql_app/database.py b/quantdb/mysql_app/database.py new file mode 100644 index 0000000..411d706 --- /dev/null +++ b/quantdb/mysql_app/database.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from typing import Any + +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker + +from quantdb.config import Settings + +# TODO: get new setting env setup first +# SQLALCHEMY_DATABASE_URL = Settings().MYSQL_URL + +# engine = create_engine(SQLALCHEMY_DATABASE_URL) +# SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +# Base: Any = declarative_base() From ccb189fba98f00affab0f5210099578d859ba456 Mon Sep 17 00:00:00 2001 From: tmsincomb Date: Tue, 2 Apr 2024 14:48:56 -0700 Subject: [PATCH 03/18] mysql reformat to black --- sql/postgres.sql | 1 - sql/tables.sql | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/sql/postgres.sql b/sql/postgres.sql index 1a0727c..4af6647 100644 --- a/sql/postgres.sql +++ b/sql/postgres.sql @@ -35,4 +35,3 @@ CREATE DATABASE :database -- quantdb LC_COLLATE = 'en_US.UTF-8' -- this was a gentoo locale issue check ${LANG} LC_CTYPE = 'en_US.UTF-8' CONNECTION LIMIT = -1; - diff --git a/sql/tables.sql b/sql/tables.sql index c68293d..86691e5 100644 --- a/sql/tables.sql +++ b/sql/tables.sql @@ -130,7 +130,7 @@ CREATE TYPE prov_type AS ENUM ( ); create table provenance( --- +-- id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, type prov_type, -- this control the interpretation of the field source field_name text, From ed5404642e110f71e0bcad696fbd3f4b9785bc1a Mon Sep 17 00:00:00 2001 From: tmsincomb Date: Wed, 3 Apr 2024 12:52:57 -0700 Subject: [PATCH 04/18] native pyproject --- poetry.lock | 969 ------------------------------------------------- pyproject.toml | 37 +- 2 files changed, 17 insertions(+), 989 deletions(-) delete mode 100644 poetry.lock diff --git a/poetry.lock b/poetry.lock deleted file mode 100644 index 1d5fa74..0000000 --- a/poetry.lock +++ /dev/null @@ -1,969 +0,0 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. - -[[package]] -name = "annotated-types" -version = "0.6.0" -description = "Reusable constraint types to use with typing.Annotated" -optional = false -python-versions = ">=3.8" -files = [ - {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"}, - {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, -] - -[[package]] -name = "anyio" -version = "4.3.0" -description = "High level compatibility layer for multiple asynchronous event loop implementations" -optional = false -python-versions = ">=3.8" -files = [ - {file = "anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8"}, - {file = "anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6"}, -] - -[package.dependencies] -idna = ">=2.8" -sniffio = ">=1.1" - -[package.extras] -doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] -test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] -trio = ["trio (>=0.23)"] - -[[package]] -name = "cfgv" -version = "3.4.0" -description = "Validate configuration and produce human readable error messages." -optional = false -python-versions = ">=3.8" -files = [ - {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, - {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, -] - -[[package]] -name = "click" -version = "8.1.7" -description = "Composable command line interface toolkit" -optional = false -python-versions = ">=3.7" -files = [ - {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, - {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] - -[[package]] -name = "distlib" -version = "0.3.8" -description = "Distribution utilities" -optional = false -python-versions = "*" -files = [ - {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, - {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, -] - -[[package]] -name = "fastapi" -version = "0.110.1" -description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" -optional = false -python-versions = ">=3.8" -files = [ - {file = "fastapi-0.110.1-py3-none-any.whl", hash = "sha256:5df913203c482f820d31f48e635e022f8cbfe7350e4830ef05a3163925b1addc"}, - {file = "fastapi-0.110.1.tar.gz", hash = "sha256:6feac43ec359dfe4f45b2c18ec8c94edb8dc2dfc461d417d9e626590c071baad"}, -] - -[package.dependencies] -pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" -starlette = ">=0.37.2,<0.38.0" -typing-extensions = ">=4.8.0" - -[package.extras] -all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] - -[[package]] -name = "filelock" -version = "3.13.3" -description = "A platform independent file lock." -optional = false -python-versions = ">=3.8" -files = [ - {file = "filelock-3.13.3-py3-none-any.whl", hash = "sha256:5ffa845303983e7a0b7ae17636509bc97997d58afeafa72fb141a17b152284cb"}, - {file = "filelock-3.13.3.tar.gz", hash = "sha256:a79895a25bbefdf55d1a2a0a80968f7dbb28edcd6d4234a0afb3f37ecde4b546"}, -] - -[package.extras] -docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] -typing = ["typing-extensions (>=4.8)"] - -[[package]] -name = "greenlet" -version = "3.0.3" -description = "Lightweight in-process concurrent programming" -optional = false -python-versions = ">=3.7" -files = [ - {file = "greenlet-3.0.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9da2bd29ed9e4f15955dd1595ad7bc9320308a3b766ef7f837e23ad4b4aac31a"}, - {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d353cadd6083fdb056bb46ed07e4340b0869c305c8ca54ef9da3421acbdf6881"}, - {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dca1e2f3ca00b84a396bc1bce13dd21f680f035314d2379c4160c98153b2059b"}, - {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ed7fb269f15dc662787f4119ec300ad0702fa1b19d2135a37c2c4de6fadfd4a"}, - {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd4f49ae60e10adbc94b45c0b5e6a179acc1736cf7a90160b404076ee283cf83"}, - {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:73a411ef564e0e097dbe7e866bb2dda0f027e072b04da387282b02c308807405"}, - {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7f362975f2d179f9e26928c5b517524e89dd48530a0202570d55ad6ca5d8a56f"}, - {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:649dde7de1a5eceb258f9cb00bdf50e978c9db1b996964cd80703614c86495eb"}, - {file = "greenlet-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:68834da854554926fbedd38c76e60c4a2e3198c6fbed520b106a8986445caaf9"}, - {file = "greenlet-3.0.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:b1b5667cced97081bf57b8fa1d6bfca67814b0afd38208d52538316e9422fc61"}, - {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52f59dd9c96ad2fc0d5724107444f76eb20aaccb675bf825df6435acb7703559"}, - {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:afaff6cf5200befd5cec055b07d1c0a5a06c040fe5ad148abcd11ba6ab9b114e"}, - {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33"}, - {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2797aa5aedac23af156bbb5a6aa2cd3427ada2972c828244eb7d1b9255846379"}, - {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7f009caad047246ed379e1c4dbcb8b020f0a390667ea74d2387be2998f58a22"}, - {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c5e1536de2aad7bf62e27baf79225d0d64360d4168cf2e6becb91baf1ed074f3"}, - {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:894393ce10ceac937e56ec00bb71c4c2f8209ad516e96033e4b3b1de270e200d"}, - {file = "greenlet-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:1ea188d4f49089fc6fb283845ab18a2518d279c7cd9da1065d7a84e991748728"}, - {file = "greenlet-3.0.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:70fb482fdf2c707765ab5f0b6655e9cfcf3780d8d87355a063547b41177599be"}, - {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4d1ac74f5c0c0524e4a24335350edad7e5f03b9532da7ea4d3c54d527784f2e"}, - {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:149e94a2dd82d19838fe4b2259f1b6b9957d5ba1b25640d2380bea9c5df37676"}, - {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15d79dd26056573940fcb8c7413d84118086f2ec1a8acdfa854631084393efcc"}, - {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b7db1ebff4ba09aaaeae6aa491daeb226c8150fc20e836ad00041bcb11230"}, - {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fcd2469d6a2cf298f198f0487e0a5b1a47a42ca0fa4dfd1b6862c999f018ebbf"}, - {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1f672519db1796ca0d8753f9e78ec02355e862d0998193038c7073045899f305"}, - {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2516a9957eed41dd8f1ec0c604f1cdc86758b587d964668b5b196a9db5bfcde6"}, - {file = "greenlet-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:bba5387a6975598857d86de9eac14210a49d554a77eb8261cc68b7d082f78ce2"}, - {file = "greenlet-3.0.3-cp37-cp37m-macosx_11_0_universal2.whl", hash = "sha256:5b51e85cb5ceda94e79d019ed36b35386e8c37d22f07d6a751cb659b180d5274"}, - {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:daf3cb43b7cf2ba96d614252ce1684c1bccee6b2183a01328c98d36fcd7d5cb0"}, - {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99bf650dc5d69546e076f413a87481ee1d2d09aaaaaca058c9251b6d8c14783f"}, - {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dd6e660effd852586b6a8478a1d244b8dc90ab5b1321751d2ea15deb49ed414"}, - {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3391d1e16e2a5a1507d83e4a8b100f4ee626e8eca43cf2cadb543de69827c4c"}, - {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1f145462f1fa6e4a4ae3c0f782e580ce44d57c8f2c7aae1b6fa88c0b2efdb41"}, - {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1a7191e42732df52cb5f39d3527217e7ab73cae2cb3694d241e18f53d84ea9a7"}, - {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0448abc479fab28b00cb472d278828b3ccca164531daab4e970a0458786055d6"}, - {file = "greenlet-3.0.3-cp37-cp37m-win32.whl", hash = "sha256:b542be2440edc2d48547b5923c408cbe0fc94afb9f18741faa6ae970dbcb9b6d"}, - {file = "greenlet-3.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:01bc7ea167cf943b4c802068e178bbf70ae2e8c080467070d01bfa02f337ee67"}, - {file = "greenlet-3.0.3-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:1996cb9306c8595335bb157d133daf5cf9f693ef413e7673cb07e3e5871379ca"}, - {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc0f794e6ad661e321caa8d2f0a55ce01213c74722587256fb6566049a8b04"}, - {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9db1c18f0eaad2f804728c67d6c610778456e3e1cc4ab4bbd5eeb8e6053c6fc"}, - {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7170375bcc99f1a2fbd9c306f5be8764eaf3ac6b5cb968862cad4c7057756506"}, - {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b66c9c1e7ccabad3a7d037b2bcb740122a7b17a53734b7d72a344ce39882a1b"}, - {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:098d86f528c855ead3479afe84b49242e174ed262456c342d70fc7f972bc13c4"}, - {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:81bb9c6d52e8321f09c3d165b2a78c680506d9af285bfccbad9fb7ad5a5da3e5"}, - {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da"}, - {file = "greenlet-3.0.3-cp38-cp38-win32.whl", hash = "sha256:d46677c85c5ba00a9cb6f7a00b2bfa6f812192d2c9f7d9c4f6a55b60216712f3"}, - {file = "greenlet-3.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:419b386f84949bf0e7c73e6032e3457b82a787c1ab4a0e43732898a761cc9dbf"}, - {file = "greenlet-3.0.3-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:da70d4d51c8b306bb7a031d5cff6cc25ad253affe89b70352af5f1cb68e74b53"}, - {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086152f8fbc5955df88382e8a75984e2bb1c892ad2e3c80a2508954e52295257"}, - {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d73a9fe764d77f87f8ec26a0c85144d6a951a6c438dfe50487df5595c6373eac"}, - {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7dcbe92cc99f08c8dd11f930de4d99ef756c3591a5377d1d9cd7dd5e896da71"}, - {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1551a8195c0d4a68fac7a4325efac0d541b48def35feb49d803674ac32582f61"}, - {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:64d7675ad83578e3fc149b617a444fab8efdafc9385471f868eb5ff83e446b8b"}, - {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b37eef18ea55f2ffd8f00ff8fe7c8d3818abd3e25fb73fae2ca3b672e333a7a6"}, - {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:77457465d89b8263bca14759d7c1684df840b6811b2499838cc5b040a8b5b113"}, - {file = "greenlet-3.0.3-cp39-cp39-win32.whl", hash = "sha256:57e8974f23e47dac22b83436bdcf23080ade568ce77df33159e019d161ce1d1e"}, - {file = "greenlet-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:c5ee858cfe08f34712f548c3c363e807e7186f03ad7a5039ebadb29e8c6be067"}, - {file = "greenlet-3.0.3.tar.gz", hash = "sha256:43374442353259554ce33599da8b692d5aa96f8976d567d4badf263371fbe491"}, -] - -[package.extras] -docs = ["Sphinx", "furo"] -test = ["objgraph", "psutil"] - -[[package]] -name = "h11" -version = "0.14.0" -description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" -optional = false -python-versions = ">=3.7" -files = [ - {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, - {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, -] - -[[package]] -name = "httptools" -version = "0.6.1" -description = "A collection of framework independent HTTP protocol utils." -optional = false -python-versions = ">=3.8.0" -files = [ - {file = "httptools-0.6.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d2f6c3c4cb1948d912538217838f6e9960bc4a521d7f9b323b3da579cd14532f"}, - {file = "httptools-0.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:00d5d4b68a717765b1fabfd9ca755bd12bf44105eeb806c03d1962acd9b8e563"}, - {file = "httptools-0.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:639dc4f381a870c9ec860ce5c45921db50205a37cc3334e756269736ff0aac58"}, - {file = "httptools-0.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e57997ac7fb7ee43140cc03664de5f268813a481dff6245e0075925adc6aa185"}, - {file = "httptools-0.6.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0ac5a0ae3d9f4fe004318d64b8a854edd85ab76cffbf7ef5e32920faef62f142"}, - {file = "httptools-0.6.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3f30d3ce413088a98b9db71c60a6ada2001a08945cb42dd65a9a9fe228627658"}, - {file = "httptools-0.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:1ed99a373e327f0107cb513b61820102ee4f3675656a37a50083eda05dc9541b"}, - {file = "httptools-0.6.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7a7ea483c1a4485c71cb5f38be9db078f8b0e8b4c4dc0210f531cdd2ddac1ef1"}, - {file = "httptools-0.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:85ed077c995e942b6f1b07583e4eb0a8d324d418954fc6af913d36db7c05a5a0"}, - {file = "httptools-0.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b0bb634338334385351a1600a73e558ce619af390c2b38386206ac6a27fecfc"}, - {file = "httptools-0.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d9ceb2c957320def533671fc9c715a80c47025139c8d1f3797477decbc6edd2"}, - {file = "httptools-0.6.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4f0f8271c0a4db459f9dc807acd0eadd4839934a4b9b892f6f160e94da309837"}, - {file = "httptools-0.6.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6a4f5ccead6d18ec072ac0b84420e95d27c1cdf5c9f1bc8fbd8daf86bd94f43d"}, - {file = "httptools-0.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:5cceac09f164bcba55c0500a18fe3c47df29b62353198e4f37bbcc5d591172c3"}, - {file = "httptools-0.6.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:75c8022dca7935cba14741a42744eee13ba05db00b27a4b940f0d646bd4d56d0"}, - {file = "httptools-0.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:48ed8129cd9a0d62cf4d1575fcf90fb37e3ff7d5654d3a5814eb3d55f36478c2"}, - {file = "httptools-0.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f58e335a1402fb5a650e271e8c2d03cfa7cea46ae124649346d17bd30d59c90"}, - {file = "httptools-0.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93ad80d7176aa5788902f207a4e79885f0576134695dfb0fefc15b7a4648d503"}, - {file = "httptools-0.6.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9bb68d3a085c2174c2477eb3ffe84ae9fb4fde8792edb7bcd09a1d8467e30a84"}, - {file = "httptools-0.6.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b512aa728bc02354e5ac086ce76c3ce635b62f5fbc32ab7082b5e582d27867bb"}, - {file = "httptools-0.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:97662ce7fb196c785344d00d638fc9ad69e18ee4bfb4000b35a52efe5adcc949"}, - {file = "httptools-0.6.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8e216a038d2d52ea13fdd9b9c9c7459fb80d78302b257828285eca1c773b99b3"}, - {file = "httptools-0.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3e802e0b2378ade99cd666b5bffb8b2a7cc8f3d28988685dc300469ea8dd86cb"}, - {file = "httptools-0.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4bd3e488b447046e386a30f07af05f9b38d3d368d1f7b4d8f7e10af85393db97"}, - {file = "httptools-0.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe467eb086d80217b7584e61313ebadc8d187a4d95bb62031b7bab4b205c3ba3"}, - {file = "httptools-0.6.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3c3b214ce057c54675b00108ac42bacf2ab8f85c58e3f324a4e963bbc46424f4"}, - {file = "httptools-0.6.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8ae5b97f690badd2ca27cbf668494ee1b6d34cf1c464271ef7bfa9ca6b83ffaf"}, - {file = "httptools-0.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:405784577ba6540fa7d6ff49e37daf104e04f4b4ff2d1ac0469eaa6a20fde084"}, - {file = "httptools-0.6.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:95fb92dd3649f9cb139e9c56604cc2d7c7bf0fc2e7c8d7fbd58f96e35eddd2a3"}, - {file = "httptools-0.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dcbab042cc3ef272adc11220517278519adf8f53fd3056d0e68f0a6f891ba94e"}, - {file = "httptools-0.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cf2372e98406efb42e93bfe10f2948e467edfd792b015f1b4ecd897903d3e8d"}, - {file = "httptools-0.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:678fcbae74477a17d103b7cae78b74800d795d702083867ce160fc202104d0da"}, - {file = "httptools-0.6.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e0b281cf5a125c35f7f6722b65d8542d2e57331be573e9e88bc8b0115c4a7a81"}, - {file = "httptools-0.6.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:95658c342529bba4e1d3d2b1a874db16c7cca435e8827422154c9da76ac4e13a"}, - {file = "httptools-0.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:7ebaec1bf683e4bf5e9fbb49b8cc36da482033596a415b3e4ebab5a4c0d7ec5e"}, - {file = "httptools-0.6.1.tar.gz", hash = "sha256:c6e26c30455600b95d94b1b836085138e82f177351454ee841c148f93a9bad5a"}, -] - -[package.extras] -test = ["Cython (>=0.29.24,<0.30.0)"] - -[[package]] -name = "identify" -version = "2.5.35" -description = "File identification library for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "identify-2.5.35-py2.py3-none-any.whl", hash = "sha256:c4de0081837b211594f8e877a6b4fad7ca32bbfc1a9307fdd61c28bfe923f13e"}, - {file = "identify-2.5.35.tar.gz", hash = "sha256:10a7ca245cfcd756a554a7288159f72ff105ad233c7c4b9c6f0f4d108f5f6791"}, -] - -[package.extras] -license = ["ukkonen"] - -[[package]] -name = "idna" -version = "3.6" -description = "Internationalized Domain Names in Applications (IDNA)" -optional = false -python-versions = ">=3.5" -files = [ - {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, - {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, -] - -[[package]] -name = "iniconfig" -version = "2.0.0" -description = "brain-dead simple config-ini parsing" -optional = false -python-versions = ">=3.7" -files = [ - {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, - {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, -] - -[[package]] -name = "nodeenv" -version = "1.8.0" -description = "Node.js virtual environment builder" -optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" -files = [ - {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, - {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, -] - -[package.dependencies] -setuptools = "*" - -[[package]] -name = "packaging" -version = "24.0" -description = "Core utilities for Python packages" -optional = false -python-versions = ">=3.7" -files = [ - {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, - {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, -] - -[[package]] -name = "platformdirs" -version = "4.2.0" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -optional = false -python-versions = ">=3.8" -files = [ - {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"}, - {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"}, -] - -[package.extras] -docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] - -[[package]] -name = "pluggy" -version = "1.4.0" -description = "plugin and hook calling mechanisms for python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, - {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, -] - -[package.extras] -dev = ["pre-commit", "tox"] -testing = ["pytest", "pytest-benchmark"] - -[[package]] -name = "pre-commit" -version = "3.7.0" -description = "A framework for managing and maintaining multi-language pre-commit hooks." -optional = false -python-versions = ">=3.9" -files = [ - {file = "pre_commit-3.7.0-py2.py3-none-any.whl", hash = "sha256:5eae9e10c2b5ac51577c3452ec0a490455c45a0533f7960f993a0d01e59decab"}, - {file = "pre_commit-3.7.0.tar.gz", hash = "sha256:e209d61b8acdcf742404408531f0c37d49d2c734fd7cff2d6076083d191cb060"}, -] - -[package.dependencies] -cfgv = ">=2.0.0" -identify = ">=1.0.0" -nodeenv = ">=0.11.1" -pyyaml = ">=5.1" -virtualenv = ">=20.10.0" - -[[package]] -name = "pydantic" -version = "2.6.4" -description = "Data validation using Python type hints" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pydantic-2.6.4-py3-none-any.whl", hash = "sha256:cc46fce86607580867bdc3361ad462bab9c222ef042d3da86f2fb333e1d916c5"}, - {file = "pydantic-2.6.4.tar.gz", hash = "sha256:b1704e0847db01817624a6b86766967f552dd9dbf3afba4004409f908dcc84e6"}, -] - -[package.dependencies] -annotated-types = ">=0.4.0" -pydantic-core = "2.16.3" -typing-extensions = ">=4.6.1" - -[package.extras] -email = ["email-validator (>=2.0.0)"] - -[[package]] -name = "pydantic-core" -version = "2.16.3" -description = "" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pydantic_core-2.16.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:75b81e678d1c1ede0785c7f46690621e4c6e63ccd9192af1f0bd9d504bbb6bf4"}, - {file = "pydantic_core-2.16.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9c865a7ee6f93783bd5d781af5a4c43dadc37053a5b42f7d18dc019f8c9d2bd1"}, - {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:162e498303d2b1c036b957a1278fa0899d02b2842f1ff901b6395104c5554a45"}, - {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2f583bd01bbfbff4eaee0868e6fc607efdfcc2b03c1c766b06a707abbc856187"}, - {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b926dd38db1519ed3043a4de50214e0d600d404099c3392f098a7f9d75029ff8"}, - {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:716b542728d4c742353448765aa7cdaa519a7b82f9564130e2b3f6766018c9ec"}, - {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc4ad7f7ee1a13d9cb49d8198cd7d7e3aa93e425f371a68235f784e99741561f"}, - {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bd87f48924f360e5d1c5f770d6155ce0e7d83f7b4e10c2f9ec001c73cf475c99"}, - {file = "pydantic_core-2.16.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0df446663464884297c793874573549229f9eca73b59360878f382a0fc085979"}, - {file = "pydantic_core-2.16.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4df8a199d9f6afc5ae9a65f8f95ee52cae389a8c6b20163762bde0426275b7db"}, - {file = "pydantic_core-2.16.3-cp310-none-win32.whl", hash = "sha256:456855f57b413f077dff513a5a28ed838dbbb15082ba00f80750377eed23d132"}, - {file = "pydantic_core-2.16.3-cp310-none-win_amd64.whl", hash = "sha256:732da3243e1b8d3eab8c6ae23ae6a58548849d2e4a4e03a1924c8ddf71a387cb"}, - {file = "pydantic_core-2.16.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:519ae0312616026bf4cedc0fe459e982734f3ca82ee8c7246c19b650b60a5ee4"}, - {file = "pydantic_core-2.16.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b3992a322a5617ded0a9f23fd06dbc1e4bd7cf39bc4ccf344b10f80af58beacd"}, - {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d62da299c6ecb04df729e4b5c52dc0d53f4f8430b4492b93aa8de1f541c4aac"}, - {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2acca2be4bb2f2147ada8cac612f8a98fc09f41c89f87add7256ad27332c2fda"}, - {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b662180108c55dfbf1280d865b2d116633d436cfc0bba82323554873967b340"}, - {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e7c6ed0dc9d8e65f24f5824291550139fe6f37fac03788d4580da0d33bc00c97"}, - {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b1bb0827f56654b4437955555dc3aeeebeddc47c2d7ed575477f082622c49e"}, - {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e56f8186d6210ac7ece503193ec84104da7ceb98f68ce18c07282fcc2452e76f"}, - {file = "pydantic_core-2.16.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:936e5db01dd49476fa8f4383c259b8b1303d5dd5fb34c97de194560698cc2c5e"}, - {file = "pydantic_core-2.16.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:33809aebac276089b78db106ee692bdc9044710e26f24a9a2eaa35a0f9fa70ba"}, - {file = "pydantic_core-2.16.3-cp311-none-win32.whl", hash = "sha256:ded1c35f15c9dea16ead9bffcde9bb5c7c031bff076355dc58dcb1cb436c4721"}, - {file = "pydantic_core-2.16.3-cp311-none-win_amd64.whl", hash = "sha256:d89ca19cdd0dd5f31606a9329e309d4fcbb3df860960acec32630297d61820df"}, - {file = "pydantic_core-2.16.3-cp311-none-win_arm64.whl", hash = "sha256:6162f8d2dc27ba21027f261e4fa26f8bcb3cf9784b7f9499466a311ac284b5b9"}, - {file = "pydantic_core-2.16.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0f56ae86b60ea987ae8bcd6654a887238fd53d1384f9b222ac457070b7ac4cff"}, - {file = "pydantic_core-2.16.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9bd22a2a639e26171068f8ebb5400ce2c1bc7d17959f60a3b753ae13c632975"}, - {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4204e773b4b408062960e65468d5346bdfe139247ee5f1ca2a378983e11388a2"}, - {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f651dd19363c632f4abe3480a7c87a9773be27cfe1341aef06e8759599454120"}, - {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aaf09e615a0bf98d406657e0008e4a8701b11481840be7d31755dc9f97c44053"}, - {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8e47755d8152c1ab5b55928ab422a76e2e7b22b5ed8e90a7d584268dd49e9c6b"}, - {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:500960cb3a0543a724a81ba859da816e8cf01b0e6aaeedf2c3775d12ee49cade"}, - {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cf6204fe865da605285c34cf1172879d0314ff267b1c35ff59de7154f35fdc2e"}, - {file = "pydantic_core-2.16.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d33dd21f572545649f90c38c227cc8631268ba25c460b5569abebdd0ec5974ca"}, - {file = "pydantic_core-2.16.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:49d5d58abd4b83fb8ce763be7794d09b2f50f10aa65c0f0c1696c677edeb7cbf"}, - {file = "pydantic_core-2.16.3-cp312-none-win32.whl", hash = "sha256:f53aace168a2a10582e570b7736cc5bef12cae9cf21775e3eafac597e8551fbe"}, - {file = "pydantic_core-2.16.3-cp312-none-win_amd64.whl", hash = "sha256:0d32576b1de5a30d9a97f300cc6a3f4694c428d956adbc7e6e2f9cad279e45ed"}, - {file = "pydantic_core-2.16.3-cp312-none-win_arm64.whl", hash = "sha256:ec08be75bb268473677edb83ba71e7e74b43c008e4a7b1907c6d57e940bf34b6"}, - {file = "pydantic_core-2.16.3-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:b1f6f5938d63c6139860f044e2538baeee6f0b251a1816e7adb6cbce106a1f01"}, - {file = "pydantic_core-2.16.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2a1ef6a36fdbf71538142ed604ad19b82f67b05749512e47f247a6ddd06afdc7"}, - {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:704d35ecc7e9c31d48926150afada60401c55efa3b46cd1ded5a01bdffaf1d48"}, - {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d937653a696465677ed583124b94a4b2d79f5e30b2c46115a68e482c6a591c8a"}, - {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9803edf8e29bd825f43481f19c37f50d2b01899448273b3a7758441b512acf8"}, - {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:72282ad4892a9fb2da25defeac8c2e84352c108705c972db82ab121d15f14e6d"}, - {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f752826b5b8361193df55afcdf8ca6a57d0232653494ba473630a83ba50d8c9"}, - {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4384a8f68ddb31a0b0c3deae88765f5868a1b9148939c3f4121233314ad5532c"}, - {file = "pydantic_core-2.16.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a4b2bf78342c40b3dc830880106f54328928ff03e357935ad26c7128bbd66ce8"}, - {file = "pydantic_core-2.16.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:13dcc4802961b5f843a9385fc821a0b0135e8c07fc3d9949fd49627c1a5e6ae5"}, - {file = "pydantic_core-2.16.3-cp38-none-win32.whl", hash = "sha256:e3e70c94a0c3841e6aa831edab1619ad5c511199be94d0c11ba75fe06efe107a"}, - {file = "pydantic_core-2.16.3-cp38-none-win_amd64.whl", hash = "sha256:ecdf6bf5f578615f2e985a5e1f6572e23aa632c4bd1dc67f8f406d445ac115ed"}, - {file = "pydantic_core-2.16.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:bda1ee3e08252b8d41fa5537413ffdddd58fa73107171a126d3b9ff001b9b820"}, - {file = "pydantic_core-2.16.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:21b888c973e4f26b7a96491c0965a8a312e13be108022ee510248fe379a5fa23"}, - {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be0ec334369316fa73448cc8c982c01e5d2a81c95969d58b8f6e272884df0074"}, - {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5b6079cc452a7c53dd378c6f881ac528246b3ac9aae0f8eef98498a75657805"}, - {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ee8d5f878dccb6d499ba4d30d757111847b6849ae07acdd1205fffa1fc1253c"}, - {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7233d65d9d651242a68801159763d09e9ec96e8a158dbf118dc090cd77a104c9"}, - {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6119dc90483a5cb50a1306adb8d52c66e447da88ea44f323e0ae1a5fcb14256"}, - {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:578114bc803a4c1ff9946d977c221e4376620a46cf78da267d946397dc9514a8"}, - {file = "pydantic_core-2.16.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d8f99b147ff3fcf6b3cc60cb0c39ea443884d5559a30b1481e92495f2310ff2b"}, - {file = "pydantic_core-2.16.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4ac6b4ce1e7283d715c4b729d8f9dab9627586dafce81d9eaa009dd7f25dd972"}, - {file = "pydantic_core-2.16.3-cp39-none-win32.whl", hash = "sha256:e7774b570e61cb998490c5235740d475413a1f6de823169b4cf94e2fe9e9f6b2"}, - {file = "pydantic_core-2.16.3-cp39-none-win_amd64.whl", hash = "sha256:9091632a25b8b87b9a605ec0e61f241c456e9248bfdcf7abdf344fdb169c81cf"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:36fa178aacbc277bc6b62a2c3da95226520da4f4e9e206fdf076484363895d2c"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:dcca5d2bf65c6fb591fff92da03f94cd4f315972f97c21975398bd4bd046854a"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a72fb9963cba4cd5793854fd12f4cfee731e86df140f59ff52a49b3552db241"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b60cc1a081f80a2105a59385b92d82278b15d80ebb3adb200542ae165cd7d183"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cbcc558401de90a746d02ef330c528f2e668c83350f045833543cd57ecead1ad"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:fee427241c2d9fb7192b658190f9f5fd6dfe41e02f3c1489d2ec1e6a5ab1e04a"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f4cb85f693044e0f71f394ff76c98ddc1bc0953e48c061725e540396d5c8a2e1"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b29eeb887aa931c2fcef5aa515d9d176d25006794610c264ddc114c053bf96fe"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a425479ee40ff021f8216c9d07a6a3b54b31c8267c6e17aa88b70d7ebd0e5e5b"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:5c5cbc703168d1b7a838668998308018a2718c2130595e8e190220238addc96f"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99b6add4c0b39a513d323d3b93bc173dac663c27b99860dd5bf491b240d26137"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f76ee558751746d6a38f89d60b6228fa174e5172d143886af0f85aa306fd89"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:00ee1c97b5364b84cb0bd82e9bbf645d5e2871fb8c58059d158412fee2d33d8a"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:287073c66748f624be4cef893ef9174e3eb88fe0b8a78dc22e88eca4bc357ca6"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ed25e1835c00a332cb10c683cd39da96a719ab1dfc08427d476bce41b92531fc"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:86b3d0033580bd6bbe07590152007275bd7af95f98eaa5bd36f3da219dcd93da"}, - {file = "pydantic_core-2.16.3.tar.gz", hash = "sha256:1cac689f80a3abab2d3c0048b29eea5751114054f032a941a32de4c852c59cad"}, -] - -[package.dependencies] -typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" - -[[package]] -name = "pydantic-settings" -version = "2.2.1" -description = "Settings management using Pydantic" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pydantic_settings-2.2.1-py3-none-any.whl", hash = "sha256:0235391d26db4d2190cb9b31051c4b46882d28a51533f97440867f012d4da091"}, - {file = "pydantic_settings-2.2.1.tar.gz", hash = "sha256:00b9f6a5e95553590434c0fa01ead0b216c3e10bc54ae02e37f359948643c5ed"}, -] - -[package.dependencies] -pydantic = ">=2.3.0" -python-dotenv = ">=0.21.0" - -[package.extras] -toml = ["tomli (>=2.0.1)"] -yaml = ["pyyaml (>=6.0.1)"] - -[[package]] -name = "pytest" -version = "8.1.1" -description = "pytest: simple powerful testing with Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pytest-8.1.1-py3-none-any.whl", hash = "sha256:2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7"}, - {file = "pytest-8.1.1.tar.gz", hash = "sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "sys_platform == \"win32\""} -iniconfig = "*" -packaging = "*" -pluggy = ">=1.4,<2.0" - -[package.extras] -testing = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] - -[[package]] -name = "python-dotenv" -version = "1.0.1" -description = "Read key-value pairs from a .env file and set them as environment variables" -optional = false -python-versions = ">=3.8" -files = [ - {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, - {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, -] - -[package.extras] -cli = ["click (>=5.0)"] - -[[package]] -name = "pyyaml" -version = "6.0.1" -description = "YAML parser and emitter for Python" -optional = false -python-versions = ">=3.6" -files = [ - {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, - {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, - {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, - {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, - {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, - {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, - {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, - {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, - {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, - {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, - {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, - {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, - {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, - {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, - {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, - {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, - {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, - {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, - {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, - {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, - {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, - {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, -] - -[[package]] -name = "setuptools" -version = "69.2.0" -description = "Easily download, build, install, upgrade, and uninstall Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "setuptools-69.2.0-py3-none-any.whl", hash = "sha256:c21c49fb1042386df081cb5d86759792ab89efca84cf114889191cd09aacc80c"}, - {file = "setuptools-69.2.0.tar.gz", hash = "sha256:0ff4183f8f42cd8fa3acea16c45205521a4ef28f73c6391d8a25e92893134f2e"}, -] - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] - -[[package]] -name = "sniffio" -version = "1.3.1" -description = "Sniff out which async library your code is running under" -optional = false -python-versions = ">=3.7" -files = [ - {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, - {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, -] - -[[package]] -name = "sqlalchemy" -version = "2.0.29" -description = "Database Abstraction Library" -optional = false -python-versions = ">=3.7" -files = [ - {file = "SQLAlchemy-2.0.29-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4c142852ae192e9fe5aad5c350ea6befe9db14370b34047e1f0f7cf99e63c63b"}, - {file = "SQLAlchemy-2.0.29-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:99a1e69d4e26f71e750e9ad6fdc8614fbddb67cfe2173a3628a2566034e223c7"}, - {file = "SQLAlchemy-2.0.29-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ef3fbccb4058355053c51b82fd3501a6e13dd808c8d8cd2561e610c5456013c"}, - {file = "SQLAlchemy-2.0.29-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d6753305936eddc8ed190e006b7bb33a8f50b9854823485eed3a886857ab8d1"}, - {file = "SQLAlchemy-2.0.29-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0f3ca96af060a5250a8ad5a63699180bc780c2edf8abf96c58af175921df847a"}, - {file = "SQLAlchemy-2.0.29-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c4520047006b1d3f0d89e0532978c0688219857eb2fee7c48052560ae76aca1e"}, - {file = "SQLAlchemy-2.0.29-cp310-cp310-win32.whl", hash = "sha256:b2a0e3cf0caac2085ff172c3faacd1e00c376e6884b5bc4dd5b6b84623e29e4f"}, - {file = "SQLAlchemy-2.0.29-cp310-cp310-win_amd64.whl", hash = "sha256:01d10638a37460616708062a40c7b55f73e4d35eaa146781c683e0fa7f6c43fb"}, - {file = "SQLAlchemy-2.0.29-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:308ef9cb41d099099fffc9d35781638986870b29f744382904bf9c7dadd08513"}, - {file = "SQLAlchemy-2.0.29-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:296195df68326a48385e7a96e877bc19aa210e485fa381c5246bc0234c36c78e"}, - {file = "SQLAlchemy-2.0.29-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a13b917b4ffe5a0a31b83d051d60477819ddf18276852ea68037a144a506efb9"}, - {file = "SQLAlchemy-2.0.29-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f6d971255d9ddbd3189e2e79d743ff4845c07f0633adfd1de3f63d930dbe673"}, - {file = "SQLAlchemy-2.0.29-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:61405ea2d563407d316c63a7b5271ae5d274a2a9fbcd01b0aa5503635699fa1e"}, - {file = "SQLAlchemy-2.0.29-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:de7202ffe4d4a8c1e3cde1c03e01c1a3772c92858837e8f3879b497158e4cb44"}, - {file = "SQLAlchemy-2.0.29-cp311-cp311-win32.whl", hash = "sha256:b5d7ed79df55a731749ce65ec20d666d82b185fa4898430b17cb90c892741520"}, - {file = "SQLAlchemy-2.0.29-cp311-cp311-win_amd64.whl", hash = "sha256:205f5a2b39d7c380cbc3b5dcc8f2762fb5bcb716838e2d26ccbc54330775b003"}, - {file = "SQLAlchemy-2.0.29-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d96710d834a6fb31e21381c6d7b76ec729bd08c75a25a5184b1089141356171f"}, - {file = "SQLAlchemy-2.0.29-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:52de4736404e53c5c6a91ef2698c01e52333988ebdc218f14c833237a0804f1b"}, - {file = "SQLAlchemy-2.0.29-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c7b02525ede2a164c5fa5014915ba3591730f2cc831f5be9ff3b7fd3e30958e"}, - {file = "SQLAlchemy-2.0.29-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dfefdb3e54cd15f5d56fd5ae32f1da2d95d78319c1f6dfb9bcd0eb15d603d5d"}, - {file = "SQLAlchemy-2.0.29-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a88913000da9205b13f6f195f0813b6ffd8a0c0c2bd58d499e00a30eb508870c"}, - {file = "SQLAlchemy-2.0.29-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fecd5089c4be1bcc37c35e9aa678938d2888845a134dd016de457b942cf5a758"}, - {file = "SQLAlchemy-2.0.29-cp312-cp312-win32.whl", hash = "sha256:8197d6f7a3d2b468861ebb4c9f998b9df9e358d6e1cf9c2a01061cb9b6cf4e41"}, - {file = "SQLAlchemy-2.0.29-cp312-cp312-win_amd64.whl", hash = "sha256:9b19836ccca0d321e237560e475fd99c3d8655d03da80c845c4da20dda31b6e1"}, - {file = "SQLAlchemy-2.0.29-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:87a1d53a5382cdbbf4b7619f107cc862c1b0a4feb29000922db72e5a66a5ffc0"}, - {file = "SQLAlchemy-2.0.29-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a0732dffe32333211801b28339d2a0babc1971bc90a983e3035e7b0d6f06b93"}, - {file = "SQLAlchemy-2.0.29-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90453597a753322d6aa770c5935887ab1fc49cc4c4fdd436901308383d698b4b"}, - {file = "SQLAlchemy-2.0.29-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ea311d4ee9a8fa67f139c088ae9f905fcf0277d6cd75c310a21a88bf85e130f5"}, - {file = "SQLAlchemy-2.0.29-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:5f20cb0a63a3e0ec4e169aa8890e32b949c8145983afa13a708bc4b0a1f30e03"}, - {file = "SQLAlchemy-2.0.29-cp37-cp37m-win32.whl", hash = "sha256:e5bbe55e8552019c6463709b39634a5fc55e080d0827e2a3a11e18eb73f5cdbd"}, - {file = "SQLAlchemy-2.0.29-cp37-cp37m-win_amd64.whl", hash = "sha256:c2f9c762a2735600654c654bf48dad388b888f8ce387b095806480e6e4ff6907"}, - {file = "SQLAlchemy-2.0.29-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7e614d7a25a43a9f54fcce4675c12761b248547f3d41b195e8010ca7297c369c"}, - {file = "SQLAlchemy-2.0.29-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:471fcb39c6adf37f820350c28aac4a7df9d3940c6548b624a642852e727ea586"}, - {file = "SQLAlchemy-2.0.29-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:988569c8732f54ad3234cf9c561364221a9e943b78dc7a4aaf35ccc2265f1930"}, - {file = "SQLAlchemy-2.0.29-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dddaae9b81c88083e6437de95c41e86823d150f4ee94bf24e158a4526cbead01"}, - {file = "SQLAlchemy-2.0.29-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:334184d1ab8f4c87f9652b048af3f7abea1c809dfe526fb0435348a6fef3d380"}, - {file = "SQLAlchemy-2.0.29-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:38b624e5cf02a69b113c8047cf7f66b5dfe4a2ca07ff8b8716da4f1b3ae81567"}, - {file = "SQLAlchemy-2.0.29-cp38-cp38-win32.whl", hash = "sha256:bab41acf151cd68bc2b466deae5deeb9e8ae9c50ad113444151ad965d5bf685b"}, - {file = "SQLAlchemy-2.0.29-cp38-cp38-win_amd64.whl", hash = "sha256:52c8011088305476691b8750c60e03b87910a123cfd9ad48576d6414b6ec2a1d"}, - {file = "SQLAlchemy-2.0.29-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3071ad498896907a5ef756206b9dc750f8e57352113c19272bdfdc429c7bd7de"}, - {file = "SQLAlchemy-2.0.29-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dba622396a3170974f81bad49aacebd243455ec3cc70615aeaef9e9613b5bca5"}, - {file = "SQLAlchemy-2.0.29-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b184e3de58009cc0bf32e20f137f1ec75a32470f5fede06c58f6c355ed42a72"}, - {file = "SQLAlchemy-2.0.29-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c37f1050feb91f3d6c32f864d8e114ff5545a4a7afe56778d76a9aec62638ba"}, - {file = "SQLAlchemy-2.0.29-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bda7ce59b06d0f09afe22c56714c65c957b1068dee3d5e74d743edec7daba552"}, - {file = "SQLAlchemy-2.0.29-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:25664e18bef6dc45015b08f99c63952a53a0a61f61f2e48a9e70cec27e55f699"}, - {file = "SQLAlchemy-2.0.29-cp39-cp39-win32.whl", hash = "sha256:77d29cb6c34b14af8a484e831ab530c0f7188f8efed1c6a833a2c674bf3c26ec"}, - {file = "SQLAlchemy-2.0.29-cp39-cp39-win_amd64.whl", hash = "sha256:04c487305ab035a9548f573763915189fc0fe0824d9ba28433196f8436f1449c"}, - {file = "SQLAlchemy-2.0.29-py3-none-any.whl", hash = "sha256:dc4ee2d4ee43251905f88637d5281a8d52e916a021384ec10758826f5cbae305"}, - {file = "SQLAlchemy-2.0.29.tar.gz", hash = "sha256:bd9566b8e58cabd700bc367b60e90d9349cd16f0984973f98a9a09f9c64e86f0"}, -] - -[package.dependencies] -greenlet = {version = "!=0.4.17", markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\""} -typing-extensions = ">=4.6.0" - -[package.extras] -aiomysql = ["aiomysql (>=0.2.0)", "greenlet (!=0.4.17)"] -aioodbc = ["aioodbc", "greenlet (!=0.4.17)"] -aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"] -asyncio = ["greenlet (!=0.4.17)"] -asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (!=0.4.17)"] -mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5)"] -mssql = ["pyodbc"] -mssql-pymssql = ["pymssql"] -mssql-pyodbc = ["pyodbc"] -mypy = ["mypy (>=0.910)"] -mysql = ["mysqlclient (>=1.4.0)"] -mysql-connector = ["mysql-connector-python"] -oracle = ["cx_oracle (>=8)"] -oracle-oracledb = ["oracledb (>=1.0.1)"] -postgresql = ["psycopg2 (>=2.7)"] -postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] -postgresql-pg8000 = ["pg8000 (>=1.29.1)"] -postgresql-psycopg = ["psycopg (>=3.0.7)"] -postgresql-psycopg2binary = ["psycopg2-binary"] -postgresql-psycopg2cffi = ["psycopg2cffi"] -postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"] -pymysql = ["pymysql"] -sqlcipher = ["sqlcipher3_binary"] - -[[package]] -name = "starlette" -version = "0.37.2" -description = "The little ASGI library that shines." -optional = false -python-versions = ">=3.8" -files = [ - {file = "starlette-0.37.2-py3-none-any.whl", hash = "sha256:6fe59f29268538e5d0d182f2791a479a0c64638e6935d1c6989e63fb2699c6ee"}, - {file = "starlette-0.37.2.tar.gz", hash = "sha256:9af890290133b79fc3db55474ade20f6220a364a0402e0b556e7cd5e1e093823"}, -] - -[package.dependencies] -anyio = ">=3.4.0,<5" - -[package.extras] -full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"] - -[[package]] -name = "typing-extensions" -version = "4.10.0" -description = "Backported and Experimental Type Hints for Python 3.8+" -optional = false -python-versions = ">=3.8" -files = [ - {file = "typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475"}, - {file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"}, -] - -[[package]] -name = "uvicorn" -version = "0.29.0" -description = "The lightning-fast ASGI server." -optional = false -python-versions = ">=3.8" -files = [ - {file = "uvicorn-0.29.0-py3-none-any.whl", hash = "sha256:2c2aac7ff4f4365c206fd773a39bf4ebd1047c238f8b8268ad996829323473de"}, - {file = "uvicorn-0.29.0.tar.gz", hash = "sha256:6a69214c0b6a087462412670b3ef21224fa48cae0e452b5883e8e8bdfdd11dd0"}, -] - -[package.dependencies] -click = ">=7.0" -colorama = {version = ">=0.4", optional = true, markers = "sys_platform == \"win32\" and extra == \"standard\""} -h11 = ">=0.8" -httptools = {version = ">=0.5.0", optional = true, markers = "extra == \"standard\""} -python-dotenv = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} -pyyaml = {version = ">=5.1", optional = true, markers = "extra == \"standard\""} -uvloop = {version = ">=0.14.0,<0.15.0 || >0.15.0,<0.15.1 || >0.15.1", optional = true, markers = "(sys_platform != \"win32\" and sys_platform != \"cygwin\") and platform_python_implementation != \"PyPy\" and extra == \"standard\""} -watchfiles = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} -websockets = {version = ">=10.4", optional = true, markers = "extra == \"standard\""} - -[package.extras] -standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] - -[[package]] -name = "uvloop" -version = "0.19.0" -description = "Fast implementation of asyncio event loop on top of libuv" -optional = false -python-versions = ">=3.8.0" -files = [ - {file = "uvloop-0.19.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:de4313d7f575474c8f5a12e163f6d89c0a878bc49219641d49e6f1444369a90e"}, - {file = "uvloop-0.19.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5588bd21cf1fcf06bded085f37e43ce0e00424197e7c10e77afd4bbefffef428"}, - {file = "uvloop-0.19.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b1fd71c3843327f3bbc3237bedcdb6504fd50368ab3e04d0410e52ec293f5b8"}, - {file = "uvloop-0.19.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a05128d315e2912791de6088c34136bfcdd0c7cbc1cf85fd6fd1bb321b7c849"}, - {file = "uvloop-0.19.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:cd81bdc2b8219cb4b2556eea39d2e36bfa375a2dd021404f90a62e44efaaf957"}, - {file = "uvloop-0.19.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5f17766fb6da94135526273080f3455a112f82570b2ee5daa64d682387fe0dcd"}, - {file = "uvloop-0.19.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4ce6b0af8f2729a02a5d1575feacb2a94fc7b2e983868b009d51c9a9d2149bef"}, - {file = "uvloop-0.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:31e672bb38b45abc4f26e273be83b72a0d28d074d5b370fc4dcf4c4eb15417d2"}, - {file = "uvloop-0.19.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:570fc0ed613883d8d30ee40397b79207eedd2624891692471808a95069a007c1"}, - {file = "uvloop-0.19.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5138821e40b0c3e6c9478643b4660bd44372ae1e16a322b8fc07478f92684e24"}, - {file = "uvloop-0.19.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:91ab01c6cd00e39cde50173ba4ec68a1e578fee9279ba64f5221810a9e786533"}, - {file = "uvloop-0.19.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:47bf3e9312f63684efe283f7342afb414eea4d3011542155c7e625cd799c3b12"}, - {file = "uvloop-0.19.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:da8435a3bd498419ee8c13c34b89b5005130a476bda1d6ca8cfdde3de35cd650"}, - {file = "uvloop-0.19.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:02506dc23a5d90e04d4f65c7791e65cf44bd91b37f24cfc3ef6cf2aff05dc7ec"}, - {file = "uvloop-0.19.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2693049be9d36fef81741fddb3f441673ba12a34a704e7b4361efb75cf30befc"}, - {file = "uvloop-0.19.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7010271303961c6f0fe37731004335401eb9075a12680738731e9c92ddd96ad6"}, - {file = "uvloop-0.19.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:5daa304d2161d2918fa9a17d5635099a2f78ae5b5960e742b2fcfbb7aefaa593"}, - {file = "uvloop-0.19.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:7207272c9520203fea9b93843bb775d03e1cf88a80a936ce760f60bb5add92f3"}, - {file = "uvloop-0.19.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:78ab247f0b5671cc887c31d33f9b3abfb88d2614b84e4303f1a63b46c046c8bd"}, - {file = "uvloop-0.19.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:472d61143059c84947aa8bb74eabbace30d577a03a1805b77933d6bd13ddebbd"}, - {file = "uvloop-0.19.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45bf4c24c19fb8a50902ae37c5de50da81de4922af65baf760f7c0c42e1088be"}, - {file = "uvloop-0.19.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271718e26b3e17906b28b67314c45d19106112067205119dddbd834c2b7ce797"}, - {file = "uvloop-0.19.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:34175c9fd2a4bc3adc1380e1261f60306344e3407c20a4d684fd5f3be010fa3d"}, - {file = "uvloop-0.19.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e27f100e1ff17f6feeb1f33968bc185bf8ce41ca557deee9d9bbbffeb72030b7"}, - {file = "uvloop-0.19.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:13dfdf492af0aa0a0edf66807d2b465607d11c4fa48f4a1fd41cbea5b18e8e8b"}, - {file = "uvloop-0.19.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6e3d4e85ac060e2342ff85e90d0c04157acb210b9ce508e784a944f852a40e67"}, - {file = "uvloop-0.19.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca4956c9ab567d87d59d49fa3704cf29e37109ad348f2d5223c9bf761a332e7"}, - {file = "uvloop-0.19.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f467a5fd23b4fc43ed86342641f3936a68ded707f4627622fa3f82a120e18256"}, - {file = "uvloop-0.19.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:492e2c32c2af3f971473bc22f086513cedfc66a130756145a931a90c3958cb17"}, - {file = "uvloop-0.19.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2df95fca285a9f5bfe730e51945ffe2fa71ccbfdde3b0da5772b4ee4f2e770d5"}, - {file = "uvloop-0.19.0.tar.gz", hash = "sha256:0246f4fd1bf2bf702e06b0d45ee91677ee5c31242f39aab4ea6fe0c51aedd0fd"}, -] - -[package.extras] -docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] -test = ["Cython (>=0.29.36,<0.30.0)", "aiohttp (==3.9.0b0)", "aiohttp (>=3.8.1)", "flake8 (>=5.0,<6.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=23.0.0,<23.1.0)", "pycodestyle (>=2.9.0,<2.10.0)"] - -[[package]] -name = "virtualenv" -version = "20.25.1" -description = "Virtual Python Environment builder" -optional = false -python-versions = ">=3.7" -files = [ - {file = "virtualenv-20.25.1-py3-none-any.whl", hash = "sha256:961c026ac520bac5f69acb8ea063e8a4f071bcc9457b9c1f28f6b085c511583a"}, - {file = "virtualenv-20.25.1.tar.gz", hash = "sha256:e08e13ecdca7a0bd53798f356d5831434afa5b07b93f0abdf0797b7a06ffe197"}, -] - -[package.dependencies] -distlib = ">=0.3.7,<1" -filelock = ">=3.12.2,<4" -platformdirs = ">=3.9.1,<5" - -[package.extras] -docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] -test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] - -[[package]] -name = "watchfiles" -version = "0.21.0" -description = "Simple, modern and high performance file watching and code reload in python." -optional = false -python-versions = ">=3.8" -files = [ - {file = "watchfiles-0.21.0-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:27b4035013f1ea49c6c0b42d983133b136637a527e48c132d368eb19bf1ac6aa"}, - {file = "watchfiles-0.21.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c81818595eff6e92535ff32825f31c116f867f64ff8cdf6562cd1d6b2e1e8f3e"}, - {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6c107ea3cf2bd07199d66f156e3ea756d1b84dfd43b542b2d870b77868c98c03"}, - {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d9ac347653ebd95839a7c607608703b20bc07e577e870d824fa4801bc1cb124"}, - {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5eb86c6acb498208e7663ca22dbe68ca2cf42ab5bf1c776670a50919a56e64ab"}, - {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f564bf68404144ea6b87a78a3f910cc8de216c6b12a4cf0b27718bf4ec38d303"}, - {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d0f32ebfaa9c6011f8454994f86108c2eb9c79b8b7de00b36d558cadcedaa3d"}, - {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6d45d9b699ecbac6c7bd8e0a2609767491540403610962968d258fd6405c17c"}, - {file = "watchfiles-0.21.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:aff06b2cac3ef4616e26ba17a9c250c1fe9dd8a5d907d0193f84c499b1b6e6a9"}, - {file = "watchfiles-0.21.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d9792dff410f266051025ecfaa927078b94cc7478954b06796a9756ccc7e14a9"}, - {file = "watchfiles-0.21.0-cp310-none-win32.whl", hash = "sha256:214cee7f9e09150d4fb42e24919a1e74d8c9b8a9306ed1474ecaddcd5479c293"}, - {file = "watchfiles-0.21.0-cp310-none-win_amd64.whl", hash = "sha256:1ad7247d79f9f55bb25ab1778fd47f32d70cf36053941f07de0b7c4e96b5d235"}, - {file = "watchfiles-0.21.0-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:668c265d90de8ae914f860d3eeb164534ba2e836811f91fecc7050416ee70aa7"}, - {file = "watchfiles-0.21.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3a23092a992e61c3a6a70f350a56db7197242f3490da9c87b500f389b2d01eef"}, - {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e7941bbcfdded9c26b0bf720cb7e6fd803d95a55d2c14b4bd1f6a2772230c586"}, - {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:11cd0c3100e2233e9c53106265da31d574355c288e15259c0d40a4405cbae317"}, - {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d78f30cbe8b2ce770160d3c08cff01b2ae9306fe66ce899b73f0409dc1846c1b"}, - {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6674b00b9756b0af620aa2a3346b01f8e2a3dc729d25617e1b89cf6af4a54eb1"}, - {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd7ac678b92b29ba630d8c842d8ad6c555abda1b9ef044d6cc092dacbfc9719d"}, - {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c873345680c1b87f1e09e0eaf8cf6c891b9851d8b4d3645e7efe2ec20a20cc7"}, - {file = "watchfiles-0.21.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:49f56e6ecc2503e7dbe233fa328b2be1a7797d31548e7a193237dcdf1ad0eee0"}, - {file = "watchfiles-0.21.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:02d91cbac553a3ad141db016e3350b03184deaafeba09b9d6439826ee594b365"}, - {file = "watchfiles-0.21.0-cp311-none-win32.whl", hash = "sha256:ebe684d7d26239e23d102a2bad2a358dedf18e462e8808778703427d1f584400"}, - {file = "watchfiles-0.21.0-cp311-none-win_amd64.whl", hash = "sha256:4566006aa44cb0d21b8ab53baf4b9c667a0ed23efe4aaad8c227bfba0bf15cbe"}, - {file = "watchfiles-0.21.0-cp311-none-win_arm64.whl", hash = "sha256:c550a56bf209a3d987d5a975cdf2063b3389a5d16caf29db4bdddeae49f22078"}, - {file = "watchfiles-0.21.0-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:51ddac60b96a42c15d24fbdc7a4bfcd02b5a29c047b7f8bf63d3f6f5a860949a"}, - {file = "watchfiles-0.21.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:511f0b034120cd1989932bf1e9081aa9fb00f1f949fbd2d9cab6264916ae89b1"}, - {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:cfb92d49dbb95ec7a07511bc9efb0faff8fe24ef3805662b8d6808ba8409a71a"}, - {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f92944efc564867bbf841c823c8b71bb0be75e06b8ce45c084b46411475a915"}, - {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:642d66b75eda909fd1112d35c53816d59789a4b38c141a96d62f50a3ef9b3360"}, - {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d23bcd6c8eaa6324fe109d8cac01b41fe9a54b8c498af9ce464c1aeeb99903d6"}, - {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18d5b4da8cf3e41895b34e8c37d13c9ed294954907929aacd95153508d5d89d7"}, - {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b8d1eae0f65441963d805f766c7e9cd092f91e0c600c820c764a4ff71a0764c"}, - {file = "watchfiles-0.21.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1fd9a5205139f3c6bb60d11f6072e0552f0a20b712c85f43d42342d162be1235"}, - {file = "watchfiles-0.21.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a1e3014a625bcf107fbf38eece0e47fa0190e52e45dc6eee5a8265ddc6dc5ea7"}, - {file = "watchfiles-0.21.0-cp312-none-win32.whl", hash = "sha256:9d09869f2c5a6f2d9df50ce3064b3391d3ecb6dced708ad64467b9e4f2c9bef3"}, - {file = "watchfiles-0.21.0-cp312-none-win_amd64.whl", hash = "sha256:18722b50783b5e30a18a8a5db3006bab146d2b705c92eb9a94f78c72beb94094"}, - {file = "watchfiles-0.21.0-cp312-none-win_arm64.whl", hash = "sha256:a3b9bec9579a15fb3ca2d9878deae789df72f2b0fdaf90ad49ee389cad5edab6"}, - {file = "watchfiles-0.21.0-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:4ea10a29aa5de67de02256a28d1bf53d21322295cb00bd2d57fcd19b850ebd99"}, - {file = "watchfiles-0.21.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:40bca549fdc929b470dd1dbfcb47b3295cb46a6d2c90e50588b0a1b3bd98f429"}, - {file = "watchfiles-0.21.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9b37a7ba223b2f26122c148bb8d09a9ff312afca998c48c725ff5a0a632145f7"}, - {file = "watchfiles-0.21.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec8c8900dc5c83650a63dd48c4d1d245343f904c4b64b48798c67a3767d7e165"}, - {file = "watchfiles-0.21.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8ad3fe0a3567c2f0f629d800409cd528cb6251da12e81a1f765e5c5345fd0137"}, - {file = "watchfiles-0.21.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d353c4cfda586db2a176ce42c88f2fc31ec25e50212650c89fdd0f560ee507b"}, - {file = "watchfiles-0.21.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:83a696da8922314ff2aec02987eefb03784f473281d740bf9170181829133765"}, - {file = "watchfiles-0.21.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a03651352fc20975ee2a707cd2d74a386cd303cc688f407296064ad1e6d1562"}, - {file = "watchfiles-0.21.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3ad692bc7792be8c32918c699638b660c0de078a6cbe464c46e1340dadb94c19"}, - {file = "watchfiles-0.21.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06247538e8253975bdb328e7683f8515ff5ff041f43be6c40bff62d989b7d0b0"}, - {file = "watchfiles-0.21.0-cp38-none-win32.whl", hash = "sha256:9a0aa47f94ea9a0b39dd30850b0adf2e1cd32a8b4f9c7aa443d852aacf9ca214"}, - {file = "watchfiles-0.21.0-cp38-none-win_amd64.whl", hash = "sha256:8d5f400326840934e3507701f9f7269247f7c026d1b6cfd49477d2be0933cfca"}, - {file = "watchfiles-0.21.0-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:7f762a1a85a12cc3484f77eee7be87b10f8c50b0b787bb02f4e357403cad0c0e"}, - {file = "watchfiles-0.21.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6e9be3ef84e2bb9710f3f777accce25556f4a71e15d2b73223788d528fcc2052"}, - {file = "watchfiles-0.21.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4c48a10d17571d1275701e14a601e36959ffada3add8cdbc9e5061a6e3579a5d"}, - {file = "watchfiles-0.21.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c889025f59884423428c261f212e04d438de865beda0b1e1babab85ef4c0f01"}, - {file = "watchfiles-0.21.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:66fac0c238ab9a2e72d026b5fb91cb902c146202bbd29a9a1a44e8db7b710b6f"}, - {file = "watchfiles-0.21.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b4a21f71885aa2744719459951819e7bf5a906a6448a6b2bbce8e9cc9f2c8128"}, - {file = "watchfiles-0.21.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c9198c989f47898b2c22201756f73249de3748e0fc9de44adaf54a8b259cc0c"}, - {file = "watchfiles-0.21.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8f57c4461cd24fda22493109c45b3980863c58a25b8bec885ca8bea6b8d4b28"}, - {file = "watchfiles-0.21.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:853853cbf7bf9408b404754b92512ebe3e3a83587503d766d23e6bf83d092ee6"}, - {file = "watchfiles-0.21.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d5b1dc0e708fad9f92c296ab2f948af403bf201db8fb2eb4c8179db143732e49"}, - {file = "watchfiles-0.21.0-cp39-none-win32.whl", hash = "sha256:59137c0c6826bd56c710d1d2bda81553b5e6b7c84d5a676747d80caf0409ad94"}, - {file = "watchfiles-0.21.0-cp39-none-win_amd64.whl", hash = "sha256:6cb8fdc044909e2078c248986f2fc76f911f72b51ea4a4fbbf472e01d14faa58"}, - {file = "watchfiles-0.21.0-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:ab03a90b305d2588e8352168e8c5a1520b721d2d367f31e9332c4235b30b8994"}, - {file = "watchfiles-0.21.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:927c589500f9f41e370b0125c12ac9e7d3a2fd166b89e9ee2828b3dda20bfe6f"}, - {file = "watchfiles-0.21.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bd467213195e76f838caf2c28cd65e58302d0254e636e7c0fca81efa4a2e62c"}, - {file = "watchfiles-0.21.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02b73130687bc3f6bb79d8a170959042eb56eb3a42df3671c79b428cd73f17cc"}, - {file = "watchfiles-0.21.0-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:08dca260e85ffae975448e344834d765983237ad6dc308231aa16e7933db763e"}, - {file = "watchfiles-0.21.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:3ccceb50c611c433145502735e0370877cced72a6c70fd2410238bcbc7fe51d8"}, - {file = "watchfiles-0.21.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57d430f5fb63fea141ab71ca9c064e80de3a20b427ca2febcbfcef70ff0ce895"}, - {file = "watchfiles-0.21.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dd5fad9b9c0dd89904bbdea978ce89a2b692a7ee8a0ce19b940e538c88a809c"}, - {file = "watchfiles-0.21.0-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:be6dd5d52b73018b21adc1c5d28ac0c68184a64769052dfeb0c5d9998e7f56a2"}, - {file = "watchfiles-0.21.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:b3cab0e06143768499384a8a5efb9c4dc53e19382952859e4802f294214f36ec"}, - {file = "watchfiles-0.21.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c6ed10c2497e5fedadf61e465b3ca12a19f96004c15dcffe4bd442ebadc2d85"}, - {file = "watchfiles-0.21.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43babacef21c519bc6631c5fce2a61eccdfc011b4bcb9047255e9620732c8097"}, - {file = "watchfiles-0.21.0.tar.gz", hash = "sha256:c76c635fabf542bb78524905718c39f736a98e5ab25b23ec6d4abede1a85a6a3"}, -] - -[package.dependencies] -anyio = ">=3.0.0" - -[[package]] -name = "websockets" -version = "12.0" -description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" -optional = false -python-versions = ">=3.8" -files = [ - {file = "websockets-12.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d554236b2a2006e0ce16315c16eaa0d628dab009c33b63ea03f41c6107958374"}, - {file = "websockets-12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2d225bb6886591b1746b17c0573e29804619c8f755b5598d875bb4235ea639be"}, - {file = "websockets-12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:eb809e816916a3b210bed3c82fb88eaf16e8afcf9c115ebb2bacede1797d2547"}, - {file = "websockets-12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c588f6abc13f78a67044c6b1273a99e1cf31038ad51815b3b016ce699f0d75c2"}, - {file = "websockets-12.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5aa9348186d79a5f232115ed3fa9020eab66d6c3437d72f9d2c8ac0c6858c558"}, - {file = "websockets-12.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6350b14a40c95ddd53e775dbdbbbc59b124a5c8ecd6fbb09c2e52029f7a9f480"}, - {file = "websockets-12.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:70ec754cc2a769bcd218ed8d7209055667b30860ffecb8633a834dde27d6307c"}, - {file = "websockets-12.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6e96f5ed1b83a8ddb07909b45bd94833b0710f738115751cdaa9da1fb0cb66e8"}, - {file = "websockets-12.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4d87be612cbef86f994178d5186add3d94e9f31cc3cb499a0482b866ec477603"}, - {file = "websockets-12.0-cp310-cp310-win32.whl", hash = "sha256:befe90632d66caaf72e8b2ed4d7f02b348913813c8b0a32fae1cc5fe3730902f"}, - {file = "websockets-12.0-cp310-cp310-win_amd64.whl", hash = "sha256:363f57ca8bc8576195d0540c648aa58ac18cf85b76ad5202b9f976918f4219cf"}, - {file = "websockets-12.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5d873c7de42dea355d73f170be0f23788cf3fa9f7bed718fd2830eefedce01b4"}, - {file = "websockets-12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3f61726cae9f65b872502ff3c1496abc93ffbe31b278455c418492016e2afc8f"}, - {file = "websockets-12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed2fcf7a07334c77fc8a230755c2209223a7cc44fc27597729b8ef5425aa61a3"}, - {file = "websockets-12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e332c210b14b57904869ca9f9bf4ca32f5427a03eeb625da9b616c85a3a506c"}, - {file = "websockets-12.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5693ef74233122f8ebab026817b1b37fe25c411ecfca084b29bc7d6efc548f45"}, - {file = "websockets-12.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e9e7db18b4539a29cc5ad8c8b252738a30e2b13f033c2d6e9d0549b45841c04"}, - {file = "websockets-12.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6e2df67b8014767d0f785baa98393725739287684b9f8d8a1001eb2839031447"}, - {file = "websockets-12.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bea88d71630c5900690fcb03161ab18f8f244805c59e2e0dc4ffadae0a7ee0ca"}, - {file = "websockets-12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dff6cdf35e31d1315790149fee351f9e52978130cef6c87c4b6c9b3baf78bc53"}, - {file = "websockets-12.0-cp311-cp311-win32.whl", hash = "sha256:3e3aa8c468af01d70332a382350ee95f6986db479ce7af14d5e81ec52aa2b402"}, - {file = "websockets-12.0-cp311-cp311-win_amd64.whl", hash = "sha256:25eb766c8ad27da0f79420b2af4b85d29914ba0edf69f547cc4f06ca6f1d403b"}, - {file = "websockets-12.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0e6e2711d5a8e6e482cacb927a49a3d432345dfe7dea8ace7b5790df5932e4df"}, - {file = "websockets-12.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:dbcf72a37f0b3316e993e13ecf32f10c0e1259c28ffd0a85cee26e8549595fbc"}, - {file = "websockets-12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12743ab88ab2af1d17dd4acb4645677cb7063ef4db93abffbf164218a5d54c6b"}, - {file = "websockets-12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b645f491f3c48d3f8a00d1fce07445fab7347fec54a3e65f0725d730d5b99cb"}, - {file = "websockets-12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9893d1aa45a7f8b3bc4510f6ccf8db8c3b62120917af15e3de247f0780294b92"}, - {file = "websockets-12.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f38a7b376117ef7aff996e737583172bdf535932c9ca021746573bce40165ed"}, - {file = "websockets-12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f764ba54e33daf20e167915edc443b6f88956f37fb606449b4a5b10ba42235a5"}, - {file = "websockets-12.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:1e4b3f8ea6a9cfa8be8484c9221ec0257508e3a1ec43c36acdefb2a9c3b00aa2"}, - {file = "websockets-12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9fdf06fd06c32205a07e47328ab49c40fc1407cdec801d698a7c41167ea45113"}, - {file = "websockets-12.0-cp312-cp312-win32.whl", hash = "sha256:baa386875b70cbd81798fa9f71be689c1bf484f65fd6fb08d051a0ee4e79924d"}, - {file = "websockets-12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ae0a5da8f35a5be197f328d4727dbcfafa53d1824fac3d96cdd3a642fe09394f"}, - {file = "websockets-12.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5f6ffe2c6598f7f7207eef9a1228b6f5c818f9f4d53ee920aacd35cec8110438"}, - {file = "websockets-12.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9edf3fc590cc2ec20dc9d7a45108b5bbaf21c0d89f9fd3fd1685e223771dc0b2"}, - {file = "websockets-12.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8572132c7be52632201a35f5e08348137f658e5ffd21f51f94572ca6c05ea81d"}, - {file = "websockets-12.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:604428d1b87edbf02b233e2c207d7d528460fa978f9e391bd8aaf9c8311de137"}, - {file = "websockets-12.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1a9d160fd080c6285e202327aba140fc9a0d910b09e423afff4ae5cbbf1c7205"}, - {file = "websockets-12.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87b4aafed34653e465eb77b7c93ef058516cb5acf3eb21e42f33928616172def"}, - {file = "websockets-12.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b2ee7288b85959797970114deae81ab41b731f19ebcd3bd499ae9ca0e3f1d2c8"}, - {file = "websockets-12.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:7fa3d25e81bfe6a89718e9791128398a50dec6d57faf23770787ff441d851967"}, - {file = "websockets-12.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:a571f035a47212288e3b3519944f6bf4ac7bc7553243e41eac50dd48552b6df7"}, - {file = "websockets-12.0-cp38-cp38-win32.whl", hash = "sha256:3c6cc1360c10c17463aadd29dd3af332d4a1adaa8796f6b0e9f9df1fdb0bad62"}, - {file = "websockets-12.0-cp38-cp38-win_amd64.whl", hash = "sha256:1bf386089178ea69d720f8db6199a0504a406209a0fc23e603b27b300fdd6892"}, - {file = "websockets-12.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ab3d732ad50a4fbd04a4490ef08acd0517b6ae6b77eb967251f4c263011a990d"}, - {file = "websockets-12.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1d9697f3337a89691e3bd8dc56dea45a6f6d975f92e7d5f773bc715c15dde28"}, - {file = "websockets-12.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1df2fbd2c8a98d38a66f5238484405b8d1d16f929bb7a33ed73e4801222a6f53"}, - {file = "websockets-12.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23509452b3bc38e3a057382c2e941d5ac2e01e251acce7adc74011d7d8de434c"}, - {file = "websockets-12.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e5fc14ec6ea568200ea4ef46545073da81900a2b67b3e666f04adf53ad452ec"}, - {file = "websockets-12.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46e71dbbd12850224243f5d2aeec90f0aaa0f2dde5aeeb8fc8df21e04d99eff9"}, - {file = "websockets-12.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b81f90dcc6c85a9b7f29873beb56c94c85d6f0dac2ea8b60d995bd18bf3e2aae"}, - {file = "websockets-12.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:a02413bc474feda2849c59ed2dfb2cddb4cd3d2f03a2fedec51d6e959d9b608b"}, - {file = "websockets-12.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bbe6013f9f791944ed31ca08b077e26249309639313fff132bfbf3ba105673b9"}, - {file = "websockets-12.0-cp39-cp39-win32.whl", hash = "sha256:cbe83a6bbdf207ff0541de01e11904827540aa069293696dd528a6640bd6a5f6"}, - {file = "websockets-12.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc4e7fa5414512b481a2483775a8e8be7803a35b30ca805afa4998a84f9fd9e8"}, - {file = "websockets-12.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:248d8e2446e13c1d4326e0a6a4e9629cb13a11195051a73acf414812700badbd"}, - {file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f44069528d45a933997a6fef143030d8ca8042f0dfaad753e2906398290e2870"}, - {file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c4e37d36f0d19f0a4413d3e18c0d03d0c268ada2061868c1e6f5ab1a6d575077"}, - {file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d829f975fc2e527a3ef2f9c8f25e553eb7bc779c6665e8e1d52aa22800bb38b"}, - {file = "websockets-12.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2c71bd45a777433dd9113847af751aae36e448bc6b8c361a566cb043eda6ec30"}, - {file = "websockets-12.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0bee75f400895aef54157b36ed6d3b308fcab62e5260703add87f44cee9c82a6"}, - {file = "websockets-12.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:423fc1ed29f7512fceb727e2d2aecb952c46aa34895e9ed96071821309951123"}, - {file = "websockets-12.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27a5e9964ef509016759f2ef3f2c1e13f403725a5e6a1775555994966a66e931"}, - {file = "websockets-12.0-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3181df4583c4d3994d31fb235dc681d2aaad744fbdbf94c4802485ececdecf2"}, - {file = "websockets-12.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:b067cb952ce8bf40115f6c19f478dc71c5e719b7fbaa511359795dfd9d1a6468"}, - {file = "websockets-12.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:00700340c6c7ab788f176d118775202aadea7602c5cc6be6ae127761c16d6b0b"}, - {file = "websockets-12.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e469d01137942849cff40517c97a30a93ae79917752b34029f0ec72df6b46399"}, - {file = "websockets-12.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffefa1374cd508d633646d51a8e9277763a9b78ae71324183693959cf94635a7"}, - {file = "websockets-12.0-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba0cab91b3956dfa9f512147860783a1829a8d905ee218a9837c18f683239611"}, - {file = "websockets-12.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2cb388a5bfb56df4d9a406783b7f9dbefb888c09b71629351cc6b036e9259370"}, - {file = "websockets-12.0-py3-none-any.whl", hash = "sha256:dc284bbc8d7c78a6c69e0c7325ab46ee5e40bb4d50e494d8131a07ef47500e9e"}, - {file = "websockets-12.0.tar.gz", hash = "sha256:81df9cbcbb6c260de1e007e58c011bfebe2dafc8435107b0537f393dd38c8b1b"}, -] - -[metadata] -lock-version = "2.0" -python-versions = "^3.12" -content-hash = "984ae526cd54554a0eea20909da100b028b3c43e0d07c9058d9c2ba7c50ec11f" diff --git a/pyproject.toml b/pyproject.toml index a6cba5f..3f32edf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,27 +1,24 @@ -[tool.poetry] +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] name = "quantdb" version = "0.1.0" description = "Quantitative DB" -authors = ["tmsincomb "] -license = "MIT" readme = "README.md" packages = [{ include = "quantdb" }] +dependencies = [ + "sqlalchemy ~= 2.0.29", + "pydantic ~= 2.6.4", + "fastapi ~= 0.110.1", + "uvicorn ~= 0.29.0", + "pydantic-settings ~= 2.2.1", +] +requires-python = ">=3.12" -[tool.poetry.dependencies] -python = "^3.12" -sqlalchemy = "^2.0.29" -pydantic = "^2.6.4" -fastapi = "^0.110.1" -uvicorn = {extras = ["standard"], version = "^0.29.0"} -pydantic-settings = "^2.2.1" - -[tool.poetry.scripts] -quantdb-fastapi = "uvicorn quantdb.main:app --reload" +# [project.scripts] +# quantdb-fastapi = "uvicorn quantdb.main:app --reload" -[tool.poetry.group.dev.dependencies] -pytest = "^8.1.1" -pre-commit = "^3.7.0" - -[build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" +[project.optional-dependencies] +dev = ["pytest ~= 8.1.1", "pre-commit ~= 3.7.0"] From 11c9264ba7d8609f81c70c8d25ed3c5d5526df3a Mon Sep 17 00:00:00 2001 From: EC2 Default User Date: Tue, 7 May 2024 21:11:12 +0000 Subject: [PATCH 05/18] move fastapi to router.py --- pyproject.toml | 2 +- quantdb/config.py | 13 ---------- quantdb/main.py | 60 +++-------------------------------------------- quantdb/router.py | 52 ++++++++++++++++++++++++++++++++++++++++ sql/postgres.sql | 16 ------------- 5 files changed, 56 insertions(+), 87 deletions(-) create mode 100644 quantdb/router.py diff --git a/pyproject.toml b/pyproject.toml index 3f32edf..601e297 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ dependencies = [ "uvicorn ~= 0.29.0", "pydantic-settings ~= 2.2.1", ] -requires-python = ">=3.12" +requires-python = ">=3.10" # [project.scripts] # quantdb-fastapi = "uvicorn quantdb.main:app --reload" diff --git a/quantdb/config.py b/quantdb/config.py index 135c0bb..aa2a831 100644 --- a/quantdb/config.py +++ b/quantdb/config.py @@ -1,15 +1,2 @@ -<<<<<<< HEAD -from __future__ import annotations - -from pathlib import Path -from typing import Optional - -from pydantic_settings import BaseSettings, SettingsConfigDict - - -class Settings(BaseSettings): - pass -======= import orthauth as oa auth = oa.configure_here('auth-config.py', __name__) ->>>>>>> master diff --git a/quantdb/main.py b/quantdb/main.py index 32bf274..ad52ff0 100644 --- a/quantdb/main.py +++ b/quantdb/main.py @@ -1,66 +1,12 @@ -<<<<<<< HEAD -from __future__ import annotations - -from functools import lru_cache -from typing import Any, Generator - -from fastapi import Depends, FastAPI -from sqlalchemy import text -from sqlalchemy.orm import Session # type: ignore - -from quantdb.config import Settings - -from quantdb import mysql_app - -app = FastAPI() - - -@lru_cache() -def get_settings(): - """ - Get settings from mongodb - alternative for test and production credientials - - Returns - ------- - Settings - pydantic BaseSettings model with MongoDB credentials - """ - return Settings() - - -def get_mysql_db() -> Generator[Session, Any, None]: - db = mysql_app.database.SessionLocal() - try: - yield db - finally: - db.close() - - -@app.get( - "/example", - status_code=200, -) -def get_example( - mysql_db: Session = Depends(get_mysql_db), # type: ignore -): - query = text( - """ - select * from limit 10 - """ - ) - result = mysql_db.execute(query) - data = result.mappings().all() - return data -======= from fastapi import FastAPI from fastapi.middleware.wsgi import WSGIMiddleware from quantdb.api_server import app as flask_app app = FastAPI() -app.mount('/', WSGIMiddleware(flask_app)) +app.mount("/", WSGIMiddleware(flask_app)) if __name__ == "__main__": import uvicorn - uvicorn.run("quantdb.main:app", host='127.0.0.1', port=8990, reload=True) ->>>>>>> master + + uvicorn.run("quantdb.main:app", host="127.0.0.1", port=8990, reload=True) diff --git a/quantdb/router.py b/quantdb/router.py new file mode 100644 index 0000000..a6b1d18 --- /dev/null +++ b/quantdb/router.py @@ -0,0 +1,52 @@ +from __future__ import annotations + +from functools import lru_cache +from typing import Any, Generator + +from fastapi import Depends, FastAPI +from sqlalchemy import text +from sqlalchemy.orm import Session # type: ignore + +from quantdb.config import Settings + +from quantdb import mysql_app + +app = FastAPI() + + +@lru_cache() +def get_settings(): + """ + Get settings from mongodb - alternative for test and production credientials + + Returns + ------- + Settings + pydantic BaseSettings model with MongoDB credentials + """ + return Settings() + + +def get_mysql_db() -> Generator[Session, Any, None]: + db = mysql_app.database.SessionLocal() + try: + yield db + finally: + db.close() + + +@app.get( + "/example", + status_code=200, +) +def get_example( + mysql_db: Session = Depends(get_mysql_db), # type: ignore +): + query = text( + """ + select * from limit 10 + """ + ) + result = mysql_db.execute(query) + data = result.mappings().all() + return data diff --git a/sql/postgres.sql b/sql/postgres.sql index f4c9d35..dbbe6ce 100644 --- a/sql/postgres.sql +++ b/sql/postgres.sql @@ -36,19 +36,3 @@ ALTER ROLE "quantdb-test-user" SET search_path = quantdb, public; ALTER ROLE "quantdb-admin" SET search_path = quantdb, public; ALTER ROLE "quantdb-user" SET search_path = quantdb, public; -<<<<<<< HEAD --- postgres postgres - -DROP DATABASE IF EXISTS :database; - --- postgres postgres - -CREATE DATABASE :database -- quantdb - WITH OWNER = 'quantdb-admin' - ENCODING = 'UTF8' - TABLESPACE = pg_default - LC_COLLATE = 'en_US.UTF-8' -- this was a gentoo locale issue check ${LANG} - LC_CTYPE = 'en_US.UTF-8' - CONNECTION LIMIT = -1; -======= ->>>>>>> master From 76b8072dd12318fe6ed3b361614940a3ee92339a Mon Sep 17 00:00:00 2001 From: EC2 Default User Date: Thu, 9 May 2024 17:53:42 +0000 Subject: [PATCH 06/18] flask dep --- pyproject.toml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 601e297..2c0e355 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,18 +3,21 @@ requires = ["hatchling"] build-backend = "hatchling.build" [project] -name = "quantdb" -version = "0.1.0" -description = "Quantitative DB" -readme = "README.md" -packages = [{ include = "quantdb" }] dependencies = [ "sqlalchemy ~= 2.0.29", "pydantic ~= 2.6.4", "fastapi ~= 0.110.1", "uvicorn ~= 0.29.0", "pydantic-settings ~= 2.2.1", + "flask ~= 3.0.3", + "Flask-SQLAlchemy ~= 3.1.1", + ] +name = "quantdb" +version = "0.1.0" +description = "Quantitative DB" +readme = "README.md" +packages = [{ include = "quantdb" }] requires-python = ">=3.10" # [project.scripts] From d09bbff4f9a80a1745e0d57f99dbf4cb9f18a5b9 Mon Sep 17 00:00:00 2001 From: EC2 Default User Date: Tue, 21 May 2024 19:43:36 +0000 Subject: [PATCH 07/18] changes for server --- pyproject.toml | 4 +- quantdb/api.py | 899 +++++++++++++++++++++++++--------------------- quantdb/router.py | 171 +++++++++ 3 files changed, 663 insertions(+), 411 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2c0e355..24def26 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,9 @@ dependencies = [ "pydantic-settings ~= 2.2.1", "flask ~= 3.0.3", "Flask-SQLAlchemy ~= 3.1.1", - + "orthauth ~= 0.0.18", + "cython", + "psycopg-binary", ] name = "quantdb" version = "0.1.0" diff --git a/quantdb/api.py b/quantdb/api.py index 952688f..b911f84 100644 --- a/quantdb/api.py +++ b/quantdb/api.py @@ -10,7 +10,7 @@ from quantdb import exceptions as exc -log = log.getChild('api') +log = log.getChild("api") class JEncode(json.JSONEncoder): @@ -27,31 +27,40 @@ def default(self, obj): url_sql_where = ( # TODO arity spec here - # dupes overwrite params but that is ok, this way we get the correct table alias for both cases - ('object', 'object', 'cv.object = any(:object)', 'cat'), # XXX should not use this outside values/ unless we left outer due to intersect ? - ('object', 'object', 'qv.object = any(:object)', 'quant'), # XXX should not use this outside values/ unless we left outer due to intersect ? - - ('desc-inst', 'desc_inst', 'idin.label = any(:desc_inst)', 'both'), - ('dataset', 'dataset', 'im.dataset = :dataset', 'both'), - ('inst', 'inst', 'im.id_formal = any(:inst)', 'both'), - ('subject', 'subject', 'im.id_sub = any(:subject)', 'both'), - ('sample', 'sample', 'im.id_sam = any(:sample)', 'both'), - - ('desc-cat', 'desc_cat', 'cd.label = any(:desc_cat)', 'cat'), - - ('value-cat', 'value_cat', 'ct.label = any(:value_cat)', 'cat'), - ('value-cat-open', 'value_cat_open', 'cv.value_open = any(:value_cat_open)', 'cat'), - - ('unit', 'unit', 'u.label = any(:unit)', 'quant'), - ('aspect', 'aspect', 'ain.label = any(:aspect)', 'quant'), - ('agg-type', 'agg_type', 'qd.aggregation_type = :agg_type', 'quant'), + ( + "object", + "object", + "cv.object = any(:object)", + "cat", + ), # XXX should not use this outside values/ unless we left outer due to intersect ? + ( + "object", + "object", + "qv.object = any(:object)", + "quant", + ), # XXX should not use this outside values/ unless we left outer due to intersect ? + ("desc-inst", "desc_inst", "idin.label = any(:desc_inst)", "both"), + ("dataset", "dataset", "im.dataset = :dataset", "both"), + ("inst", "inst", "im.id_formal = any(:inst)", "both"), + ("subject", "subject", "im.id_sub = any(:subject)", "both"), + ("sample", "sample", "im.id_sam = any(:sample)", "both"), + ("desc-cat", "desc_cat", "cd.label = any(:desc_cat)", "cat"), + ("value-cat", "value_cat", "ct.label = any(:value_cat)", "cat"), + ("value-cat-open", "value_cat_open", "cv.value_open = any(:value_cat_open)", "cat"), + ("unit", "unit", "u.label = any(:unit)", "quant"), + ("aspect", "aspect", "ain.label = any(:aspect)", "quant"), + ("agg-type", "agg_type", "qd.aggregation_type = :agg_type", "quant"), # TODO shape - - ('value-quant', 'value_quant', 'qv.value = :value_quant', 'quant'), - ('value-quant-margin', 'value_quant_margin', 'qv.value <= :value_quant + :value_quant_margin AND qv.value >= :value_quant - :value_quant_margin', 'quant'), - ('value-quant-min', 'value_quant_min', 'qv.value >= :value_quant_min', 'quant'), - ('value-quant-max', 'value_quant_max', 'qv.value <= :value_quant_min', 'quant'), + ("value-quant", "value_quant", "qv.value = :value_quant", "quant"), + ( + "value-quant-margin", + "value_quant_margin", + "qv.value <= :value_quant + :value_quant_margin AND qv.value >= :value_quant - :value_quant_margin", + "quant", + ), + ("value-quant-min", "value_quant_min", "qv.value >= :value_quant_min", "quant"), + ("value-quant-max", "value_quant_max", "qv.value <= :value_quant_min", "quant"), ) @@ -62,140 +71,126 @@ def get_where(kwargs): for u, s, w, t in url_sql_where: if u in kwargs and kwargs[u]: params[s] = kwargs[u] - if t == 'cat': + if t == "cat": _where_cat.append(w) - elif t == 'quant': + elif t == "quant": _where_quant.append(w) - elif t == 'both': + elif t == "both": _where_cat.append(w) _where_quant.append(w) else: - raise ValueError('wat') + raise ValueError("wat") - where_cat = ' AND '.join(_where_cat) - where_quant = ' AND '.join(_where_quant) - log.log(9, f'\nwhere-quant\n{where_quant}\nwhere-quant') + where_cat = " AND ".join(_where_cat) + where_quant = " AND ".join(_where_quant) + log.log(9, f"\nwhere-quant\n{where_quant}\nwhere-quant") return where_cat, where_quant, params def main_query(endpoint, kwargs): ep_select = { #'instances': 'im.dataset, im.id_formal, im.id_sam, im.id_sub, id.label', - 'values/inst': ( - 'im.dataset, ' - 'im.id_formal AS inst, ' - 'im.id_sam AS sample, ' - 'im.id_sub AS subject, ' - 'id.label AS desc_inst' + "values/inst": ( + "im.dataset, " + "im.id_formal AS inst, " + "im.id_sam AS sample, " + "im.id_sub AS subject, " + "id.label AS desc_inst" ), - 'objects': ( # TODO probably some path metadata file type, etc. too - 'im.dataset, ' - 'o.id, ' - 'o.id_type, ' - 'oi.updated_transitive' + "objects": ( # TODO probably some path metadata file type, etc. too + "im.dataset, " "o.id, " "o.id_type, " "oi.updated_transitive" ), - 'values/cat': ( - 'im.dataset, ' - 'im.id_formal AS inst, ' - 'id.label AS desc_inst, ' - 'cdid.label AS domain, ' - 'cd.range, ' - 'cd.label AS desc_cat, ' - 'cv.value_open, ' - 'ct.label AS value_controlled' # TODO and where did it come from TODO iri + "values/cat": ( + "im.dataset, " + "im.id_formal AS inst, " + "id.label AS desc_inst, " + "cdid.label AS domain, " + "cd.range, " + "cd.label AS desc_cat, " + "cv.value_open, " + "ct.label AS value_controlled" # TODO and where did it come from TODO iri ), # TODO will want/need to return the shape of the value for these as well since that will be needed to correctly interpret the contents of the value field in the future - 'values/quant': ( - 'im.dataset, ' - 'im.id_formal AS inst, ' - 'id.label AS desc_inst, ' - 'qd.aggregation_type AS agg_type, ' - 'a.label AS aspect, ' - 'u.label AS unit, qv.value' # TODO and where did it come from + "values/quant": ( + "im.dataset, " + "im.id_formal AS inst, " + "id.label AS desc_inst, " + "qd.aggregation_type AS agg_type, " + "a.label AS aspect, " + "u.label AS unit, qv.value" # TODO and where did it come from ), - 'values/cat-quant': ( + "values/cat-quant": ( ( "'value-cat' AS type, " - 'im.dataset, ' - 'im.id_formal AS inst, ' - 'id.label AS desc_inst, ' - 'cdid.label AS domain, ' - 'cd.range, ' - 'NULL::quant_agg_type as agg_type, ' # have to annoate the nulls because distinct causes type inference to fail ??? - 'cd.label AS pred_or_asp, ' - 'cv.value_open AS vo_or_unit, ' - 'ct.label AS value_controlled, ' - 'NULL::numeric AS value') - , ( + "im.dataset, " + "im.id_formal AS inst, " + "id.label AS desc_inst, " + "cdid.label AS domain, " + "cd.range, " + "NULL::quant_agg_type as agg_type, " # have to annoate the nulls because distinct causes type inference to fail ??? + "cd.label AS pred_or_asp, " + "cv.value_open AS vo_or_unit, " + "ct.label AS value_controlled, " + "NULL::numeric AS value" + ), + ( "'value-quant' AS type, im.dataset, " - 'im.id_formal AS inst, id.label AS desc_inst, ' - 'NULL AS domain, ' - 'NULL::cat_range_type AS range, ' - 'qd.aggregation_type AS agg_type, ' - 'a.label AS aspect, ' - 'u.label AS unit, ' - 'NULL AS vc, qv.value' - )), - 'desc/inst': ( - 'id.iri, ' - 'id.label, ' - 'idpar.label as subclassof ' + "im.id_formal AS inst, id.label AS desc_inst, " + "NULL AS domain, " + "NULL::cat_range_type AS range, " + "qd.aggregation_type AS agg_type, " + "a.label AS aspect, " + "u.label AS unit, " + "NULL AS vc, qv.value" + ), ), - 'desc/cat': ( - 'cd.label, ' - 'cdid.label AS domain, ' - 'cd.range, ' - 'cd.description ' - ), - 'desc/quant': ( - 'qd.label, ' - 'id.label AS domain, ' - 'qd.shape, ' - 'qd.aggregation_type as agg_type, ' - 'a.label AS aspect, ' - 'u.label AS unit, ' - 'qd.description ' - ), - 'terms': ( - 'ct.iri, ' - 'ct.label ' - ), - 'units': ( - 'u.iri, ' - 'u.label ' - ), - 'aspects': ( - 'a.iri, ' - 'a.label, ' - 'aspar.label as subclassof ' + "desc/inst": ("id.iri, " "id.label, " "idpar.label as subclassof "), + "desc/cat": ("cd.label, " "cdid.label AS domain, " "cd.range, " "cd.description "), + "desc/quant": ( + "qd.label, " + "id.label AS domain, " + "qd.shape, " + "qd.aggregation_type as agg_type, " + "a.label AS aspect, " + "u.label AS unit, " + "qd.description " ), + "terms": ("ct.iri, " "ct.label "), + "units": ("u.iri, " "u.label "), + "aspects": ("a.iri, " "a.label, " "aspar.label as subclassof "), }[endpoint] # FIXME move extra and select out and pass then in in as arguments ? or retain control here? - def gkw(k): return k in kwargs and kwargs[k] + def gkw(k): + return k in kwargs and kwargs[k] class sn: # select needs - objects = endpoint == 'objects' - desc_inst = endpoint not in ('objects', 'terms', 'units', 'aspects',) - desc_cat = endpoint in ('values/cat', 'values/cat-quant', 'desc/cat') - value_cat = endpoint in ('values/cat', 'values/cat-quant', 'terms') - aspect = endpoint in ('values/quant', 'values/cat-quant', 'desc/quant', 'aspects') - unit = endpoint in ('values/quant', 'values/cat-quant', 'desc/quant', 'units') - agg_type = endpoint in ('values/quant', 'values/cat-quant') - desc_quant = (aspect or unit or agg_type) - parent_aspect = endpoint == 'aspects' - parent_desc_inst = endpoint == 'desc/inst' + objects = endpoint == "objects" + desc_inst = endpoint not in ( + "objects", + "terms", + "units", + "aspects", + ) + desc_cat = endpoint in ("values/cat", "values/cat-quant", "desc/cat") + value_cat = endpoint in ("values/cat", "values/cat-quant", "terms") + aspect = endpoint in ("values/quant", "values/cat-quant", "desc/quant", "aspects") + unit = endpoint in ("values/quant", "values/cat-quant", "desc/quant", "units") + agg_type = endpoint in ("values/quant", "values/cat-quant") + desc_quant = aspect or unit or agg_type + parent_aspect = endpoint == "aspects" + parent_desc_inst = endpoint == "desc/inst" class kw: # keywords - prov = gkw('prov') - source_only = gkw('source-only') - desc_inst = gkw('desc-inst') - desc_cat = gkw('desc-cat') - value_cat = gkw('value-cat') - aspect = gkw('aspect') - unit = gkw('unit') - agg_type = gkw('agg-type') - desc_quant = (aspect or unit or agg_type) + prov = gkw("prov") + source_only = gkw("source-only") + desc_inst = gkw("desc-inst") + desc_cat = gkw("desc-cat") + value_cat = gkw("value-cat") + aspect = gkw("aspect") + unit = gkw("unit") + agg_type = gkw("agg-type") + desc_quant = aspect or unit or agg_type q_par_desc_inst = """ JOIN descriptors_inst AS idstart ON idstart.id = {join_to}.desc_inst @@ -235,7 +230,8 @@ class kw: # keywords adc.addr_type as prov_value_addr_type, adc.addr_field as prov_value_addr_field, adc.value_type as prov_value_type -""" + (""", +""" + ( + """, NULL::address_type as prov_unit_addr_type, NULL as prov_unit_addr_field, NULL::field_value_type as prov_unit_type, @@ -243,7 +239,10 @@ class kw: # keywords NULL::address_type as prov_aspect_addr_type, NULL as prov_aspect_addr_field, NULL::field_value_type as prov_aspect_type -""" if sn.unit or endpoint == 'values/inst' else '') +""" + if sn.unit or endpoint == "values/inst" + else "" + ) s_prov_q = """ adq.addr_type as prov_value_addr_type, @@ -277,132 +276,211 @@ class kw: # keywords LEFT OUTER JOIN addresses AS ada ON ada.id = odq.addr_aspect """ - maybe_distinct = 'DISTINCT ' if ( - endpoint.startswith('desc/') or - endpoint in ('terms', 'units', 'aspects') or - (sn.objects or kw.prov) and not kw.source_only) else '' + maybe_distinct = ( + "DISTINCT " + if ( + endpoint.startswith("desc/") + or endpoint in ("terms", "units", "aspects") + or (sn.objects or kw.prov) + and not kw.source_only + ) + else "" + ) ep_select_cat, ep_select_quant = ep_select if isinstance(ep_select, tuple) else (ep_select, ep_select) - select_cat = f'SELECT {maybe_distinct}{ep_select_cat}' + ( - (s_prov_objs + s_prov_i + ((',\n' + s_prov_c) if endpoint != 'values/inst' else '')) if kw.prov else '') - select_quant = f'SELECT {maybe_distinct}{ep_select_quant}' + ( - (s_prov_objs + s_prov_i + ((',\n' + s_prov_q) if endpoint != 'values/inst' else '')) if kw.prov else '') + select_cat = f"SELECT {maybe_distinct}{ep_select_cat}" + ( + (s_prov_objs + s_prov_i + ((",\n" + s_prov_c) if endpoint != "values/inst" else "")) if kw.prov else "" + ) + select_quant = f"SELECT {maybe_distinct}{ep_select_quant}" + ( + (s_prov_objs + s_prov_i + ((",\n" + s_prov_q) if endpoint != "values/inst" else "")) if kw.prov else "" + ) _where_cat, _where_quant, params = get_where(kwargs) - where_cat = f'WHERE {_where_cat}' if _where_cat else '' - where_quant = f'WHERE {_where_quant}' if _where_quant else '' + where_cat = f"WHERE {_where_cat}" if _where_cat else "" + where_quant = f"WHERE {_where_quant}" if _where_quant else "" # FIXME even trying to be smart here about which joins to pull just papers over the underlying perf issue # shaves about 140ms off but the underlying issue remains - q_cat = '\n'.join(( - 'FROM values_cat AS cv', - '\n'.join(( - 'JOIN descriptors_inst AS idin', - 'CROSS JOIN LATERAL get_child_closed_desc_inst(idin.id) AS idc ON cv.desc_inst = idc.child -- FIXME', - )) if kw.desc_inst else '', - (q_par_desc_inst.format(join_to='cv') if sn.parent_desc_inst else - 'JOIN descriptors_inst AS id ON cv.desc_inst = id.id' - ) if sn.desc_inst or kw.desc_inst else '', # FIXME handle parents case - 'JOIN values_inst AS im ON cv.instance = im.id', - '\n'.join(( - 'JOIN descriptors_cat AS cd ON cv.desc_cat = cd.id', - 'LEFT OUTER JOIN descriptors_inst AS cdid ON cd.domain = cdid.id -- XXX TODO mismach', - )) if sn.desc_cat or kw.desc_cat else '', - 'LEFT OUTER JOIN controlled_terms AS ct ON cv.value_controlled = ct.id' if sn.value_cat or kw.value_cat else '', - (('\n' - 'JOIN objects AS o ON cv.object = o.id\n' - 'LEFT OUTER JOIN objects_internal AS oi\n' - 'ON oi.id = o.id\n') - if kw.source_only else - ('\n' # have to use LEFT OUTER because object might have only one of cat or quant - 'LEFT OUTER JOIN values_quant AS qv ON qv.instance = im.id\n' - 'JOIN objects AS o ON cv.object = o.id OR qv.object = o.id\n' - 'LEFT OUTER JOIN objects_internal AS oi\n' - 'ON oi.id = o.id\n') - ) if sn.objects or kw.prov else '', - (q_prov_i + q_prov_c) if kw.prov else '', - )) - - q_quant = '\n'.join(( - 'FROM values_quant AS qv', - '\n'.join(( - 'JOIN descriptors_inst AS idin', - 'CROSS JOIN LATERAL get_child_closed_desc_inst(idin.id) AS idc ON qv.desc_inst = idc.child -- FIXME', - )) if kw.desc_inst else '', - (q_par_desc_inst.format(join_to='qv') if sn.parent_desc_inst else - 'JOIN descriptors_inst AS id ON qv.desc_inst = id.id' - ) if sn.desc_inst or kw.desc_inst else '', # FIXME handle parents case - 'JOIN values_inst AS im ON qv.instance = im.id', - 'JOIN descriptors_quant AS qd ON qv.desc_quant = qd.id' if ( - sn.desc_quant or kw.desc_quant) else '', - '\n'.join(( - 'JOIN aspects AS ain', - 'CROSS JOIN LATERAL get_child_closed_aspect(ain.id) AS ac ON qd.aspect = ac.child', - 'JOIN aspects AS a ON ac.child = a.id', - )) if kw.aspect else ( - (q_par_aspect if sn.parent_aspect else - 'JOIN aspects AS a ON qd.aspect = a.id' - ) if sn.aspect else ''), # FIXME handle parents case - 'LEFT OUTER JOIN units AS u ON qd.unit = u.id' if sn.unit or kw.unit else '', - (('\n' - 'JOIN objects AS o ON qv.object = o.id\n' - 'LEFT OUTER JOIN objects_internal AS oi ON oi.id = o.id\n') - if kw.source_only else - ('\n' # have to use LEFT OUTER because object might have only one of cat or quant - 'LEFT OUTER JOIN values_cat AS cv ON cv.instance = im.id\n' - 'JOIN objects AS o ON qv.object = o.id OR cv.object = o.id\n' - 'LEFT OUTER JOIN objects_internal AS oi ON oi.id = o.id\n') - ) if sn.objects or kw.prov else '', - (q_prov_i + q_prov_q) if kw.prov else '', - )) - - sw_cat = f'{select_cat}\n{q_cat}\n{where_cat}' # XXX yes this can be malformed in some cases - sw_quant = f'{select_quant}\n{q_quant}\n{where_quant}' # XXX yes this can be malformed in some cases - if endpoint in ('values/cat', 'terms', 'desc/cat'): + q_cat = "\n".join( + ( + "FROM values_cat AS cv", + ( + "\n".join( + ( + "JOIN descriptors_inst AS idin", + "CROSS JOIN LATERAL get_child_closed_desc_inst(idin.id) AS idc ON cv.desc_inst = idc.child -- FIXME", + ) + ) + if kw.desc_inst + else "" + ), + ( + ( + q_par_desc_inst.format(join_to="cv") + if sn.parent_desc_inst + else "JOIN descriptors_inst AS id ON cv.desc_inst = id.id" + ) + if sn.desc_inst or kw.desc_inst + else "" + ), # FIXME handle parents case + "JOIN values_inst AS im ON cv.instance = im.id", + ( + "\n".join( + ( + "JOIN descriptors_cat AS cd ON cv.desc_cat = cd.id", + "LEFT OUTER JOIN descriptors_inst AS cdid ON cd.domain = cdid.id -- XXX TODO mismach", + ) + ) + if sn.desc_cat or kw.desc_cat + else "" + ), + ( + "LEFT OUTER JOIN controlled_terms AS ct ON cv.value_controlled = ct.id" + if sn.value_cat or kw.value_cat + else "" + ), + ( + ( + ( + "\n" + "JOIN objects AS o ON cv.object = o.id\n" + "LEFT OUTER JOIN objects_internal AS oi\n" + "ON oi.id = o.id\n" + ) + if kw.source_only + else ( + "\n" # have to use LEFT OUTER because object might have only one of cat or quant + "LEFT OUTER JOIN values_quant AS qv ON qv.instance = im.id\n" + "JOIN objects AS o ON cv.object = o.id OR qv.object = o.id\n" + "LEFT OUTER JOIN objects_internal AS oi\n" + "ON oi.id = o.id\n" + ) + ) + if sn.objects or kw.prov + else "" + ), + (q_prov_i + q_prov_c) if kw.prov else "", + ) + ) + + q_quant = "\n".join( + ( + "FROM values_quant AS qv", + ( + "\n".join( + ( + "JOIN descriptors_inst AS idin", + "CROSS JOIN LATERAL get_child_closed_desc_inst(idin.id) AS idc ON qv.desc_inst = idc.child -- FIXME", + ) + ) + if kw.desc_inst + else "" + ), + ( + ( + q_par_desc_inst.format(join_to="qv") + if sn.parent_desc_inst + else "JOIN descriptors_inst AS id ON qv.desc_inst = id.id" + ) + if sn.desc_inst or kw.desc_inst + else "" + ), # FIXME handle parents case + "JOIN values_inst AS im ON qv.instance = im.id", + "JOIN descriptors_quant AS qd ON qv.desc_quant = qd.id" if (sn.desc_quant or kw.desc_quant) else "", + ( + "\n".join( + ( + "JOIN aspects AS ain", + "CROSS JOIN LATERAL get_child_closed_aspect(ain.id) AS ac ON qd.aspect = ac.child", + "JOIN aspects AS a ON ac.child = a.id", + ) + ) + if kw.aspect + else ( + (q_par_aspect if sn.parent_aspect else "JOIN aspects AS a ON qd.aspect = a.id") if sn.aspect else "" + ) + ), # FIXME handle parents case + "LEFT OUTER JOIN units AS u ON qd.unit = u.id" if sn.unit or kw.unit else "", + ( + ( + ( + "\n" + "JOIN objects AS o ON qv.object = o.id\n" + "LEFT OUTER JOIN objects_internal AS oi ON oi.id = o.id\n" + ) + if kw.source_only + else ( + "\n" # have to use LEFT OUTER because object might have only one of cat or quant + "LEFT OUTER JOIN values_cat AS cv ON cv.instance = im.id\n" + "JOIN objects AS o ON qv.object = o.id OR cv.object = o.id\n" + "LEFT OUTER JOIN objects_internal AS oi ON oi.id = o.id\n" + ) + ) + if sn.objects or kw.prov + else "" + ), + (q_prov_i + q_prov_q) if kw.prov else "", + ) + ) + + sw_cat = f"{select_cat}\n{q_cat}\n{where_cat}" # XXX yes this can be malformed in some cases + sw_quant = f"{select_quant}\n{q_quant}\n{where_quant}" # XXX yes this can be malformed in some cases + if endpoint in ("values/cat", "terms", "desc/cat"): query = sw_cat - elif endpoint in ('values/quant', 'units', 'aspects', 'desc/quant'): # FIXME TODO make it possible to cross query terms, units, aspects + elif endpoint in ( + "values/quant", + "units", + "aspects", + "desc/quant", + ): # FIXME TODO make it possible to cross query terms, units, aspects query = sw_quant else: - operator = 'UNION' if 'union-cat-quant' in kwargs and kwargs['union-cat-quant'] else 'INTERSECT' - query = f'{sw_cat}\n{operator}\n{sw_quant}' + operator = "UNION" if "union-cat-quant" in kwargs and kwargs["union-cat-quant"] else "INTERSECT" + query = f"{sw_cat}\n{operator}\n{sw_quant}" - log.log(9, '\n' + query) + log.log(9, "\n" + query) return query, params def to_json(record_type, res, prov=False): rows = list(res) if rows: - if record_type == 'object': - result = [{k: v for k, v in r._asdict().items() if k != 'id'} - # do not leak internal ids because the might change and are not meaningful - if r.id_type == 'quantdb' else - {k: v for k, v in r._asdict().items() if k != 'updated_transitive'} - for r in rows] - elif record_type is None and 'type' in rows[0]._fields: - rem_cat = 'value', 'agg_type' + if record_type == "object": + result = [ + ( + {k: v for k, v in r._asdict().items() if k != "id"} + # do not leak internal ids because the might change and are not meaningful + if r.id_type == "quantdb" + else {k: v for k, v in r._asdict().items() if k != "updated_transitive"} + ) + for r in rows + ] + elif record_type is None and "type" in rows[0]._fields: + rem_cat = "value", "agg_type" + def type_fields_cat(k): - if k == 'pred_or_asp': - return 'desc_cat' - elif k == 'vo_or_unit': - return 'value_open' + if k == "pred_or_asp": + return "desc_cat" + elif k == "vo_or_unit": + return "value_open" else: return k - rem_quant = 'domain', 'range', 'value_controlled' + rem_quant = "domain", "range", "value_controlled" + def type_fields_quant(k): - if k == 'pred_or_asp': - return 'aspect' - elif k == 'vo_or_unit': - return 'unit' + if k == "pred_or_asp": + return "aspect" + elif k == "vo_or_unit": + return "unit" else: return k def prow(r): - if r.type == 'value-cat': + if r.type == "value-cat": rem, type_fields = rem_cat, type_fields_cat - elif r.type == 'value-quant': + elif r.type == "value-quant": rem, type_fields = rem_quant, type_fields_quant else: - raise NotImplementedError(f'wat {r.type}') + raise NotImplementedError(f"wat {r.type}") return {type_fields(k): v for k, v in r._asdict().items() if k not in rem} @@ -412,35 +490,40 @@ def prow(r): for r in result: if record_type is not None: - r['type'] = record_type + r["type"] = record_type - for cull_none in ('subclassof',): + for cull_none in ("subclassof",): if cull_none in r and r[cull_none] is None: r.pop(cull_none) if prov: + def pop_prefix(d, prefix): - usc = prefix.count('_') - return {k.split('_', 1 + usc)[-1]:v for k in list(d) if k.startswith(prefix + '_') and (v := d.pop(k)) is not None} + usc = prefix.count("_") + return { + k.split("_", 1 + usc)[-1]: v + for k in list(d) + if k.startswith(prefix + "_") and (v := d.pop(k)) is not None + } for r in result: - provs = pop_prefix(r, 'prov') - if 'source_id_type' in provs and provs['source_id_type'] == 'quantdb': - provs.pop('source_id', None) # don't leak internal ids + provs = pop_prefix(r, "prov") + if "source_id_type" in provs and provs["source_id_type"] == "quantdb": + provs.pop("source_id", None) # don't leak internal ids else: - provs.pop('source_updated_transitive', None) # always None in this case + provs.pop("source_updated_transitive", None) # always None in this case - for prefix in ('desc_inst', 'inst', 'value', 'value', 'source'): + for prefix in ("desc_inst", "inst", "value", "value", "source"): d = pop_prefix(provs, prefix) if d: - d['type'] = 'address' if prefix != 'source' else 'object' + d["type"] = "address" if prefix != "source" else "object" provs[prefix] = d - provs['type'] = 'prov' - r['prov'] = provs + provs["type"] = "prov" + r["prov"] = provs out = result - #breakpoint() + # breakpoint() else: out = [] @@ -455,60 +538,51 @@ def wrap_out(endpoint, kwargs, out): parameters = {k: v for k, v in kwargs.items() if v} n_records = len(out) blob = { - 'type': 'quantdb-query-result', - 'endpoint': endpoint, - 'parameters': parameters, - 'records': n_records, - 'result': out, + "type": "quantdb-query-result", + "endpoint": endpoint, + "parameters": parameters, + "records": n_records, + "result": out, } return blob args_default = { - 'object': [], - 'updated-transitive': None, # TODO needed to query for some internal - + "object": [], + "updated-transitive": None, # TODO needed to query for some internal ## inst - 'desc-inst': [], # aka class - + "desc-inst": [], # aka class # value-inst - 'dataset': None, - 'inst': [], - 'subject': [], - 'sample': [], - 'include-equivalent': False, - + "dataset": None, + "inst": [], + "subject": [], + "sample": [], + "include-equivalent": False, ## cat - 'desc-cat': [], # aka predicate - - 'value-cat': [], - 'value-cat-open': [], - + "desc-cat": [], # aka predicate + "value-cat": [], + "value-cat-open": [], ## quant # desc-quant - 'unit': [], - 'aspect': [], - 'agg-type': None, + "unit": [], + "aspect": [], + "agg-type": None, # TODO shape - - 'value-quant': None, - 'value-quant-margin': None, - 'value-quant-min': None, - 'value-quant-max': None, - - 'limit': 100, + "value-quant": None, + "value-quant-margin": None, + "value-quant-min": None, + "value-quant-max": None, + "limit": 100, #'operator': 'INTERSECT', # XXX ... - 'union-cat-quant': False, # by default we intersect but sometimes you want the union instead e.g. if object is passed - 'source-only': False, - 'include-unused': False, - 'prov': False, - + "union-cat-quant": False, # by default we intersect but sometimes you want the union instead e.g. if object is passed + "source-only": False, + "include-unused": False, + "prov": False, #'cat-value': [], #'class': [], #'predicate': None, #'object': None, #'filter': [], - #'quant-value': None, #'quant-margin': None, #'quant-min': None, @@ -520,62 +594,64 @@ def getArgs(request, endpoint, dev=False): default = copy.deepcopy(args_default) if dev: - default['return-query'] = False + default["return-query"] = False # modify defaults by endpoint - if endpoint != 'objects': - default.pop('source-only') + if endpoint != "objects": + default.pop("source-only") - if not (endpoint.startswith('desc/') or endpoint in ('terms', 'units', 'aspects')): - default.pop('include-unused') + if not (endpoint.startswith("desc/") or endpoint in ("terms", "units", "aspects")): + default.pop("include-unused") else: # prevent filtering on the thing we are trying to query - if endpoint == 'terms': - default.pop('value-cat') - elif endpoint == 'units': - default.pop('unit') - elif endpoint == 'aspects': - default.pop('aspect') - elif endpoint == 'desc/inst': - default.pop('desc-inst') - elif endpoint == 'desc/cat': - default.pop('desc-cat') - - if not endpoint.startswith('values/'): - default.pop('prov') - elif endpoint == 'values/cat': - [default.pop(k) for k in list(default) if k.startswith('value-quant') or k in ('unit', 'aspect', 'agg-type')] - elif endpoint == 'values/quant': - [default.pop(k) for k in list(default) if k in ('desc-cat', 'value-cat', 'value-cat-open')] + if endpoint == "terms": + default.pop("value-cat") + elif endpoint == "units": + default.pop("unit") + elif endpoint == "aspects": + default.pop("aspect") + elif endpoint == "desc/inst": + default.pop("desc-inst") + elif endpoint == "desc/cat": + default.pop("desc-cat") + + if not endpoint.startswith("values/"): + default.pop("prov") + elif endpoint == "values/cat": + [default.pop(k) for k in list(default) if k.startswith("value-quant") or k in ("unit", "aspect", "agg-type")] + elif endpoint == "values/quant": + [default.pop(k) for k in list(default) if k in ("desc-cat", "value-cat", "value-cat-open")] extras = set(request.args) - set(default) if extras: # FIXME raise this as a 401, TODO need error types for this - nl = '\n' - raise exc.UnknownArg(f'unknown args: {nl.join(extras)}') + nl = "\n" + raise exc.UnknownArg(f"unknown args: {nl.join(extras)}") def convert(k, d): if k in request.args: # arity is determined here - if k in ('dataset', 'include-equivalent', 'union-cat-quant', 'include-unused') or k.startswith('value-quant-'): + if k in ("dataset", "include-equivalent", "union-cat-quant", "include-unused") or k.startswith( + "value-quant-" + ): v = request.args[k] - if k in ('dataset',): + if k in ("dataset",): v = uuid.UUID(v) else: v = request.args.getlist(k) - if k in ('object',): + if k in ("object",): v = [uuid.UUID(_) for _ in v] # caste to uuid to simplify sqlalchemy type mapping else: return d - if k in ('include-equivalent', 'union-cat-quant', 'include-unused'): - if v.lower() == 'true': + if k in ("include-equivalent", "union-cat-quant", "include-unused"): + if v.lower() == "true": return True - elif v.lower() == 'false': + elif v.lower() == "false": return False else: raise TypeError(f'Expected a bool, got "{v}" instead.') - elif k.startswith('value-quant') or k in ('limit',): + elif k.startswith("value-quant") or k in ("limit",): try: return float(v) except ValueError as e: @@ -583,34 +659,34 @@ def convert(k, d): else: return v - out = {k:convert(k, v) for k, v in default.items()} + out = {k: convert(k, v) for k, v in default.items()} return out -def make_app(db=None, name='quantdb-api-server', dev=False): +def make_app(db=None, name="quantdb-api-server", dev=False): app = Flask(name) - kwargs = {k:auth.get(f'db-{k}') # TODO integrate with cli options - for k in ('user', 'host', 'port', 'database')} - kwargs['dbuser'] = kwargs.pop('user') - app.config['SQLALCHEMY_DATABASE_URI'] = dbUri(**kwargs) # use os.environ.update - app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False + kwargs = {k: auth.get(f"db-{k}") for k in ("user", "host", "port", "database")} # TODO integrate with cli options + kwargs["dbuser"] = kwargs.pop("user") + app.config["SQLALCHEMY_DATABASE_URI"] = dbUri(**kwargs) # use os.environ.update + app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False db.init_app(app) session = db.session - bp = '/api/1/' + bp = "/api/1/" def default_flow(endpoint, record_type, query_fun, json_fun, alt_query_fun=None): try: kwargs = getArgs(request, endpoint, dev=dev) except exc.UnknownArg as e: - return json.dumps({'error': e.args[0], 'http_response_status': 422}), 422 + return json.dumps({"error": e.args[0], "http_response_status": 422}), 422 except Exception as e: breakpoint() raise e - def gkw(k): return k in kwargs and kwargs[k] + def gkw(k): + return k in kwargs and kwargs[k] - if gkw('include-unused'): + if gkw("include-unused"): query_fun = alt_query_fun # FIXME record_type is actually determined entirely in query_fun right now @@ -620,7 +696,7 @@ def gkw(k): return k in kwargs and kwargs[k] breakpoint() raise e - if gkw('return-query'): + if gkw("return-query"): return query try: @@ -630,137 +706,140 @@ def gkw(k): return k in kwargs and kwargs[k] raise e try: - out = json_fun(record_type, res, prov=('prov' in kwargs and kwargs['prov'])) - resp = json.dumps(wrap_out(endpoint, kwargs, out), cls=JEncode), 200, {'Content-Type': 'application/json'} + out = json_fun(record_type, res, prov=("prov" in kwargs and kwargs["prov"])) + resp = json.dumps(wrap_out(endpoint, kwargs, out), cls=JEncode), 200, {"Content-Type": "application/json"} except Exception as e: breakpoint() raise e return resp - @app.route(f'{bp}/objects') + @app.route(f"{bp}/test") + def route_test_check(): + "objects with derived values that match all criteria" + return "testing-api" + + @app.route(f"{bp}/objects") def route_1_objects(): "objects with derived values that match all criteria" - return default_flow('objects', 'object', main_query, to_json) + return default_flow("objects", "object", main_query, to_json) - @app.route(f'{bp}/desc/inst') - @app.route(f'{bp}/descriptors/inst') - @app.route(f'{bp}/classes') + @app.route(f"{bp}/desc/inst") + @app.route(f"{bp}/descriptors/inst") + @app.route(f"{bp}/classes") def route_1_desc_inst(): def query(endpoint, kwargs): - return ('SELECT ' - - 'id.iri, ' - 'id.label, ' - 'idpar.label as subclassof' - + return ( + ( + "SELECT " + "id.iri, " + "id.label, " + "idpar.label as subclassof" """ FROM descriptors_inst AS id LEFT OUTER JOIN class_parent AS ip ON ip.id = id.id LEFT OUTER JOIN descriptors_inst AS idpar ON idpar.id = ip.parent -"""), {} +""" + ), + {}, + ) - return default_flow('desc/inst', 'desc-inst', main_query, to_json, alt_query_fun=query) + return default_flow("desc/inst", "desc-inst", main_query, to_json, alt_query_fun=query) - @app.route(f'{bp}/desc/cat') - @app.route(f'{bp}/descriptors/cat') - @app.route(f'{bp}/predicates') + @app.route(f"{bp}/desc/cat") + @app.route(f"{bp}/descriptors/cat") + @app.route(f"{bp}/predicates") def route_1_desc_cat(): def query(endpoint, kwargs): - return ('select ' - - 'cd.label, ' - 'cdid.label AS domain, ' - 'cd.range, ' - 'cd.description ' - - 'from descriptors_cat as cd ' - 'left outer join descriptors_inst as cdid on cdid.id = cd.domain' - ), {} - - return default_flow('desc/cat', 'desc-cat', main_query, to_json, alt_query_fun=query) # TODO likely need different args e.g. to filter by desc_inst - - @app.route(f'{bp}/desc/quant') - @app.route(f'{bp}/descriptors/quant') + return ( + "select " + "cd.label, " + "cdid.label AS domain, " + "cd.range, " + "cd.description " + "from descriptors_cat as cd " + "left outer join descriptors_inst as cdid on cdid.id = cd.domain" + ), {} + + return default_flow( + "desc/cat", "desc-cat", main_query, to_json, alt_query_fun=query + ) # TODO likely need different args e.g. to filter by desc_inst + + @app.route(f"{bp}/desc/quant") + @app.route(f"{bp}/descriptors/quant") def route_1_desc_quant(): def query(endpoint, kwargs): - return ('select ' - - 'qd.label, ' - 'id.label AS domain, ' - 'qd.shape, ' - 'qd.aggregation_type as agg_type, ' - 'a.label AS aspect, ' - 'u.label AS unit, ' - 'qd.description ' - - 'from descriptors_quant as qd ' - 'left outer join descriptors_inst as id on id.id = qd.domain ' - 'left outer join units as u on u.id = qd.unit ' - 'join aspects as a on a.id = qd.aspect' - ), {} - - return default_flow('desc/quant', 'desc-quant', main_query, to_json, alt_query_fun=query) # TODO likely need different args e.g. to filter by desc_inst - - @app.route(f'{bp}/values/inst') - @app.route(f'{bp}/instances') + return ( + "select " + "qd.label, " + "id.label AS domain, " + "qd.shape, " + "qd.aggregation_type as agg_type, " + "a.label AS aspect, " + "u.label AS unit, " + "qd.description " + "from descriptors_quant as qd " + "left outer join descriptors_inst as id on id.id = qd.domain " + "left outer join units as u on u.id = qd.unit " + "join aspects as a on a.id = qd.aspect" + ), {} + + return default_flow( + "desc/quant", "desc-quant", main_query, to_json, alt_query_fun=query + ) # TODO likely need different args e.g. to filter by desc_inst + + @app.route(f"{bp}/values/inst") + @app.route(f"{bp}/instances") def route_1_val_inst(): "instances associated with values that match all critiera" - return default_flow('values/inst', 'instance', main_query, to_json) + return default_flow("values/inst", "instance", main_query, to_json) - @app.route(f'{bp}/values') - @app.route(f'{bp}/values/cat-quant') + @app.route(f"{bp}/values") + @app.route(f"{bp}/values/cat-quant") def route_1_val_cat_quant(): - return default_flow('values/cat-quant', None, main_query, to_json) + return default_flow("values/cat-quant", None, main_query, to_json) - @app.route(f'{bp}/values/cat') + @app.route(f"{bp}/values/cat") def route_1_val_cat(): - return default_flow('values/cat', 'value-cat', main_query, to_json) + return default_flow("values/cat", "value-cat", main_query, to_json) - @app.route(f'{bp}/values/quant') + @app.route(f"{bp}/values/quant") def route_1_val_quant(): - return default_flow('values/quant', 'value-quant', main_query, to_json) + return default_flow("values/quant", "value-quant", main_query, to_json) - @app.route(f'{bp}/terms') - @app.route(f'{bp}/controlled-terms') + @app.route(f"{bp}/terms") + @app.route(f"{bp}/controlled-terms") def route_1_cterms(): def query(endpoint, kwargs): - return ('select ' - - 'ct.iri, ' - 'ct.label ' + return ("select " "ct.iri, " "ct.label " "from controlled_terms as ct"), {} - 'from controlled_terms as ct'), {} + return default_flow("terms", "term", main_query, to_json, alt_query_fun=query) - return default_flow('terms', 'term', main_query, to_json, alt_query_fun=query) - - @app.route(f'{bp}/units') + @app.route(f"{bp}/units") def route_1_units(): def query(endpoint, kwargs): - return ('select ' - - 'u.iri, ' - 'u.label ' - - 'from units as u'), {} + return ("select " "u.iri, " "u.label " "from units as u"), {} - return default_flow('units', 'unit', main_query, to_json, alt_query_fun=query) + return default_flow("units", "unit", main_query, to_json, alt_query_fun=query) - @app.route(f'{bp}/aspects') + @app.route(f"{bp}/aspects") def route_1_aspects(): def query(endpoint, kwargs): - return ('SELECT ' - - 'a.iri, ' - 'a.label, ' - 'aspar.label AS subclassof ' - + return ( + ( + "SELECT " + "a.iri, " + "a.label, " + "aspar.label AS subclassof " """ FROM aspects AS a LEFT OUTER JOIN aspect_parent AS ap ON ap.id = a.id LEFT OUTER JOIN aspects AS aspar ON aspar.id = ap.parent -"""), {} +""" + ), + {}, + ) - return default_flow('aspects', 'aspect', main_query, to_json, alt_query_fun=query) + return default_flow("aspects", "aspect", main_query, to_json, alt_query_fun=query) return app diff --git a/quantdb/router.py b/quantdb/router.py index a6b1d18..df75984 100644 --- a/quantdb/router.py +++ b/quantdb/router.py @@ -50,3 +50,174 @@ def get_example( result = mysql_db.execute(query) data = result.mappings().all() return data + + def default_flow(endpoint, record_type, query_fun, json_fun, alt_query_fun=None): + try: + kwargs = getArgs(request, endpoint, dev=dev) + except exc.UnknownArg as e: + return json.dumps({"error": e.args[0], "http_response_status": 422}), 422 + except Exception as e: + breakpoint() + raise e + + def gkw(k): + return k in kwargs and kwargs[k] + + if gkw("include-unused"): + query_fun = alt_query_fun + + # FIXME record_type is actually determined entirely in query_fun right now + try: + query, params = query_fun(endpoint, kwargs) + except Exception as e: + breakpoint() + raise e + + if gkw("return-query"): + return query + + try: + res = session.execute(sql_text(query), params) + except Exception as e: + breakpoint() + raise e + + try: + out = json_fun(record_type, res, prov=("prov" in kwargs and kwargs["prov"])) + resp = json.dumps(wrap_out(endpoint, kwargs, out), cls=JEncode), 200, {"Content-Type": "application/json"} + except Exception as e: + breakpoint() + raise e + + return resp + + +# @app.route(f"{bp}/test") +# def route_test_check(): +# "objects with derived values that match all criteria" +# return "testing-api" + +# @app.route(f"{bp}/objects") +# def route_1_objects(): +# "objects with derived values that match all criteria" +# return default_flow("objects", "object", main_query, to_json) + +# @app.route(f"{bp}/desc/inst") +# @app.route(f"{bp}/descriptors/inst") +# @app.route(f"{bp}/classes") +# def route_1_desc_inst(): +# def query(endpoint, kwargs): +# return ( +# ( +# "SELECT " +# "id.iri, " +# "id.label, " +# "idpar.label as subclassof" +# """ +# FROM descriptors_inst AS id +# LEFT OUTER JOIN class_parent AS ip ON ip.id = id.id +# LEFT OUTER JOIN descriptors_inst AS idpar ON idpar.id = ip.parent +# """ +# ), +# {}, +# ) + +# return default_flow("desc/inst", "desc-inst", main_query, to_json, alt_query_fun=query) + +# @app.route(f"{bp}/desc/cat") +# @app.route(f"{bp}/descriptors/cat") +# @app.route(f"{bp}/predicates") +# def route_1_desc_cat(): +# def query(endpoint, kwargs): +# return ( +# "select " +# "cd.label, " +# "cdid.label AS domain, " +# "cd.range, " +# "cd.description " +# "from descriptors_cat as cd " +# "left outer join descriptors_inst as cdid on cdid.id = cd.domain" +# ), {} + +# return default_flow( +# "desc/cat", "desc-cat", main_query, to_json, alt_query_fun=query +# ) # TODO likely need different args e.g. to filter by desc_inst + +# @app.route(f"{bp}/desc/quant") +# @app.route(f"{bp}/descriptors/quant") +# def route_1_desc_quant(): +# def query(endpoint, kwargs): +# return ( +# "select " +# "qd.label, " +# "id.label AS domain, " +# "qd.shape, " +# "qd.aggregation_type as agg_type, " +# "a.label AS aspect, " +# "u.label AS unit, " +# "qd.description " +# "from descriptors_quant as qd " +# "left outer join descriptors_inst as id on id.id = qd.domain " +# "left outer join units as u on u.id = qd.unit " +# "join aspects as a on a.id = qd.aspect" +# ), {} + +# return default_flow( +# "desc/quant", "desc-quant", main_query, to_json, alt_query_fun=query +# ) # TODO likely need different args e.g. to filter by desc_inst + +# @app.route(f"{bp}/values/inst") +# @app.route(f"{bp}/instances") +# def route_1_val_inst(): +# "instances associated with values that match all critiera" +# return default_flow("values/inst", "instance", main_query, to_json) + +# @app.route(f"{bp}/values") +# @app.route(f"{bp}/values/cat-quant") +# def route_1_val_cat_quant(): +# return default_flow("values/cat-quant", None, main_query, to_json) + +# @app.route(f"{bp}/values/cat") +# def route_1_val_cat(): +# return default_flow("values/cat", "value-cat", main_query, to_json) + +# @app.route(f"{bp}/values/quant") +# def route_1_val_quant(): +# return default_flow("values/quant", "value-quant", main_query, to_json) + +# @app.route(f"{bp}/terms") +# @app.route(f"{bp}/controlled-terms") +# def route_1_cterms(): +# def query(endpoint, kwargs): +# return ("select " "ct.iri, " "ct.label " "from controlled_terms as ct"), {} + +# return default_flow("terms", "term", main_query, to_json, alt_query_fun=query) + +# @app.route(f"{bp}/units") +# def route_1_units(): +# def query(endpoint, kwargs): +# return ("select " "u.iri, " "u.label " "from units as u"), {} + +# return default_flow("units", "unit", main_query, to_json, alt_query_fun=query) + +# @app.route(f"{bp}/aspects") +# def route_1_aspects(): +# def query(endpoint, kwargs): +# return ( +# ( +# "SELECT " +# "a.iri, " +# "a.label, " +# "aspar.label AS subclassof " +# """ +# FROM aspects AS a +# LEFT OUTER JOIN aspect_parent AS ap ON ap.id = a.id +# LEFT OUTER JOIN aspects AS aspar ON aspar.id = ap.parent +# """ +# ), +# {}, +# ) + +# return default_flow("aspects", "aspect", main_query, to_json, alt_query_fun=query) + +# return app From 2987252b583077772b213146464e7f7dc5bead0a Mon Sep 17 00:00:00 2001 From: tmsincomb Date: Tue, 21 May 2024 13:35:42 -0700 Subject: [PATCH 08/18] pre-commit pass --- .gitignore | 2 +- README.pdf | Bin 11284 -> 11273 bytes pyproject.toml | 2 +- quantdb/api.py | 11 +- quantdb/api_server.py | 9 +- quantdb/auth-config.py | 60 ++-- quantdb/config.py | 3 +- quantdb/exceptions.py | 4 +- quantdb/ingest.py | 617 ++++++++++++++++++++++++----------------- quantdb/main.py | 1 + quantdb/router.py | 228 +-------------- quantdb/utils.py | 33 +-- sql/postgres.sql | 1 - test/test_api.py | 158 +++++------ 14 files changed, 501 insertions(+), 628 deletions(-) diff --git a/.gitignore b/.gitignore index 74f9128..2d87f8c 100644 --- a/.gitignore +++ b/.gitignore @@ -161,4 +161,4 @@ cython_debug/ # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ -nb/ \ No newline at end of file +nb/ diff --git a/README.pdf b/README.pdf index eff2baa79326409fbcb2e138d0d691504f2854bd..0431664500efbcab3a942e6e80100ec14a0195da 100644 GIT binary patch delta 195 zcmbOd(HXJfi2d}') + # return int(f'{sam_ind}{seg_ind}{rest:0>2d}') return comps @@ -186,12 +188,12 @@ def pps(path_structure): p1 = sam_1, subject # child, parent to match db convention wasDerivedFrom p2 = segment, sam_1 return { - 'parents': (p1, p2), - 'subject': subject, - 'sample': segment, - 'modality': modality, + "parents": (p1, p2), + "subject": subject, + "sample": segment, + "modality": modality, # note that because we do not convert to a single value we cannot include raw_anat_index in the qdb but that's ok - 'raw_anat_index_v1': anat_index(segment), + "raw_anat_index_v1": anat_index(segment), } else: raise NotImplementedError(path_structure) @@ -199,124 +201,155 @@ def pps(path_structure): def ext(j): out = {} - out['dataset'] = j['dataset_id'] - out['object'] = j['remote_id'] - out['file_id'] = j['file_id'] if 'file_id' in j else int(j['uri_api'].rsplit('/')[-1]) # XXX old pathmeta schema that didn't include file id - ps = pathlib.Path(j['dataset_relative_path']).parts - [p for p in ps if p.startswith('sub-') or p.startswith('sam-')] + out["dataset"] = j["dataset_id"] + out["object"] = j["remote_id"] + out["file_id"] = ( + j["file_id"] if "file_id" in j else int(j["uri_api"].rsplit("/")[-1]) + ) # XXX old pathmeta schema that didn't include file id + ps = pathlib.Path(j["dataset_relative_path"]).parts + [p for p in ps if p.startswith("sub-") or p.startswith("sam-")] out.update(pps(ps)) return out class Queries: - def __init__(self, session): self.session = session def address_from_fadd_type_fadd(self, fadd_type, fadd): # FIXME multi etc. - res = [i for i, in self.session.execute(sql_text("select * from address_from_fadd_type_fadd(:fadd_type, :fadd)"), dict(fadd_type=fadd_type, fadd=fadd))] + res = [ + i + for i, in self.session.execute( + sql_text("select * from address_from_fadd_type_fadd(:fadd_type, :fadd)"), + dict(fadd_type=fadd_type, fadd=fadd), + ) + ] if res: return res[0] - def desc_inst_from_label(self, label): # FIXME multi etc. - res = [i for i, in self.session.execute(sql_text("select * from desc_inst_from_label(:label)"), dict(label=label))] + res = [ + i for i, in self.session.execute(sql_text("select * from desc_inst_from_label(:label)"), dict(label=label)) + ] if res: return res[0] - def desc_quant_from_label(self, label): # FIXME multi etc. - res = [i for i, in self.session.execute(sql_text("select * from desc_quant_from_label(:label)"), dict(label=label))] + res = [ + i for i, in self.session.execute(sql_text("select * from desc_quant_from_label(:label)"), dict(label=label)) + ] if res: return res[0] - def desc_cat_from_label_domain_label(self, label, domain_label): # FIXME multi etc. - res = [i for i, in self.session.execute(sql_text("select * from desc_cat_from_label_domain_label(:label, :domain_label)"), - dict(label=label, domain_label=domain_label))] + res = [ + i + for i, in self.session.execute( + sql_text("select * from desc_cat_from_label_domain_label(:label, :domain_label)"), + dict(label=label, domain_label=domain_label), + ) + ] if res: return res[0] - def cterm_from_label(self, label): # FIXME multi etc. res = [i for i, in self.session.execute(sql_text("select * from cterm_from_label(:label)"), dict(label=label))] if res: return res[0] - def insts_from_dataset_ids(self, dataset, ids): - return list(self.session.execute(sql_text("select * from insts_from_dataset_ids(:dataset, :ids)"), dict(dataset=dataset, ids=ids))) + return list( + self.session.execute( + sql_text("select * from insts_from_dataset_ids(:dataset, :ids)"), dict(dataset=dataset, ids=ids) + ) + ) class InternalIds: def __init__(self, queries): q = queries self._q = queries - - self.addr_suid = q.address_from_fadd_type_fadd('tabular-header', 'id_sub') - self.addr_said = q.address_from_fadd_type_fadd('tabular-header', 'id_sam') - self.addr_spec = q.address_from_fadd_type_fadd('tabular-header', 'species') - self.addr_saty = q.address_from_fadd_type_fadd('tabular-header', 'sample_type') - self.addr_tmod = q.address_from_fadd_type_fadd('tabular-header', 'modality') - #addr_trai = address_from_fadd_type_fadd('tabular-header', 'raw_anat_index') - #addr_tnai = address_from_fadd_type_fadd('tabular-header', 'norm_anat_index') - #addr_context = address_from_fadd_type_fadd('context', '#/path-metadata/{index of match remote_id}/dataset_relative_path') # XXX this doesn't do what we want, I think what we really would want in these contexts are objects_internal that reference the file system state for a given updated snapshot, that is the real "object" that corresponds to the path-metadata.json that we are working from + self.addr_suid = q.address_from_fadd_type_fadd("tabular-header", "id_sub") + self.addr_said = q.address_from_fadd_type_fadd("tabular-header", "id_sam") + self.addr_spec = q.address_from_fadd_type_fadd("tabular-header", "species") + self.addr_saty = q.address_from_fadd_type_fadd("tabular-header", "sample_type") - #addr_jpmod = address_from_fadd_type_fadd('json-path-with-types', '#/#int/modality') - #addr_jprai = address_from_fadd_type_fadd('json-path-with-types', '#/#int/anat_index') - #addr_jpnai = address_from_fadd_type_fadd('json-path-with-types', '#/#int/norm_anat_index') + self.addr_tmod = q.address_from_fadd_type_fadd("tabular-header", "modality") + # addr_trai = address_from_fadd_type_fadd('tabular-header', 'raw_anat_index') + # addr_tnai = address_from_fadd_type_fadd('tabular-header', 'norm_anat_index') + # addr_context = address_from_fadd_type_fadd('context', '#/path-metadata/{index of match remote_id}/dataset_relative_path') # XXX this doesn't do what we want, I think what we really would want in these contexts are objects_internal that reference the file system state for a given updated snapshot, that is the real "object" that corresponds to the path-metadata.json that we are working from - self.addr_jpdrp = q.address_from_fadd_type_fadd('json-path-with-types', '#/path-metadata/data/#int/dataset_relative_path') + # addr_jpmod = address_from_fadd_type_fadd('json-path-with-types', '#/#int/modality') + # addr_jprai = address_from_fadd_type_fadd('json-path-with-types', '#/#int/anat_index') + # addr_jpnai = address_from_fadd_type_fadd('json-path-with-types', '#/#int/norm_anat_index') - # XXX these are more accurate if opaque - self.addr_jpmod = q.address_from_fadd_type_fadd('json-path-with-types', '#/path-metadata/data/#int/dataset_relative_path#derive-modality') - #addr_jprai = address_from_fadd_type_fadd('json-path-with-types', '#/path-metadata/data/#int/dataset_relative_path#derive-raw-anat-index') - self.addr_jpnai = q.address_from_fadd_type_fadd('json-path-with-types', '#/path-metadata/data/#int/dataset_relative_path#derive-norm-anat-index-v1') - self.addr_jpnain = q.address_from_fadd_type_fadd('json-path-with-types', '#/path-metadata/data/#int/dataset_relative_path#derive-norm-anat-index-v1-min') - self.addr_jpnaix = q.address_from_fadd_type_fadd('json-path-with-types', '#/path-metadata/data/#int/dataset_relative_path#derive-norm-anat-index-v1-max') - self.addr_jpsuid = q.address_from_fadd_type_fadd('json-path-with-types', '#/path-metadata/data/#int/dataset_relative_path#derive-subject-id') - self.addr_jpsaid = q.address_from_fadd_type_fadd('json-path-with-types', '#/path-metadata/data/#int/dataset_relative_path#derive-sample-id') + self.addr_jpdrp = q.address_from_fadd_type_fadd( + "json-path-with-types", "#/path-metadata/data/#int/dataset_relative_path" + ) - self.addr_jpspec = q.address_from_fadd_type_fadd('json-path-with-types', '#/local/tom-made-it-up/species') - self.addr_jpsaty = q.address_from_fadd_type_fadd('json-path-with-types', '#/local/tom-made-it-up/sample_type') + # XXX these are more accurate if opaque + self.addr_jpmod = q.address_from_fadd_type_fadd( + "json-path-with-types", "#/path-metadata/data/#int/dataset_relative_path#derive-modality" + ) + # addr_jprai = address_from_fadd_type_fadd('json-path-with-types', '#/path-metadata/data/#int/dataset_relative_path#derive-raw-anat-index') + self.addr_jpnai = q.address_from_fadd_type_fadd( + "json-path-with-types", "#/path-metadata/data/#int/dataset_relative_path#derive-norm-anat-index-v1" + ) + self.addr_jpnain = q.address_from_fadd_type_fadd( + "json-path-with-types", "#/path-metadata/data/#int/dataset_relative_path#derive-norm-anat-index-v1-min" + ) + self.addr_jpnaix = q.address_from_fadd_type_fadd( + "json-path-with-types", "#/path-metadata/data/#int/dataset_relative_path#derive-norm-anat-index-v1-max" + ) + self.addr_jpsuid = q.address_from_fadd_type_fadd( + "json-path-with-types", "#/path-metadata/data/#int/dataset_relative_path#derive-subject-id" + ) + self.addr_jpsaid = q.address_from_fadd_type_fadd( + "json-path-with-types", "#/path-metadata/data/#int/dataset_relative_path#derive-sample-id" + ) + + self.addr_jpspec = q.address_from_fadd_type_fadd("json-path-with-types", "#/local/tom-made-it-up/species") + self.addr_jpsaty = q.address_from_fadd_type_fadd("json-path-with-types", "#/local/tom-made-it-up/sample_type") # future version when we actually have the metadata files - #addr_jpmod = address_from_fadd_type_fadd('json-path-with-types', '#/curation-export/manifest/#int/modality') - #addr_jprai = address_from_fadd_type_fadd('json-path-with-types', '#/curation-export/samples/#int/raw_anat_index') - #addr_jpnai = address_from_fadd_type_fadd('json-path-with-types', '#/curation-export/samples/#int/norm_anat_index') - #addr_jpsuid = address_from_fadd_type_fadd('json-path-with-types', '#/curation-export/subjects/#int/id_sub') - #addr_jpsaid = address_from_fadd_type_fadd('json-path-with-types', '#/curation-export/samples/#int/id_sam') - - self.addr_const_null = q.address_from_fadd_type_fadd('constant', None) - - #qd_rai = desc_quant_from_label('reva ft sample anatomical location distance index raw') - self.qd_nai = q.desc_quant_from_label('reva ft sample anatomical location distance index normalized v1') - self.qd_nain = q.desc_quant_from_label('reva ft sample anatomical location distance index normalized v1 min') - self.qd_naix = q.desc_quant_from_label('reva ft sample anatomical location distance index normalized v1 max') - - self.cd_mod = q.desc_cat_from_label_domain_label('hasDataAboutItModality', None) - self.cd_bot = q.desc_cat_from_label_domain_label('bottom', None) # we just need something we can reference that points to null so we can have a refernce to all the objects - - self.id_human = q.desc_inst_from_label('human') - self.id_nerve = q.desc_inst_from_label('nerve') - self.id_nerve_volume = q.desc_inst_from_label('nerve-volume') + # addr_jpmod = address_from_fadd_type_fadd('json-path-with-types', '#/curation-export/manifest/#int/modality') + # addr_jprai = address_from_fadd_type_fadd('json-path-with-types', '#/curation-export/samples/#int/raw_anat_index') + # addr_jpnai = address_from_fadd_type_fadd('json-path-with-types', '#/curation-export/samples/#int/norm_anat_index') + # addr_jpsuid = address_from_fadd_type_fadd('json-path-with-types', '#/curation-export/subjects/#int/id_sub') + # addr_jpsaid = address_from_fadd_type_fadd('json-path-with-types', '#/curation-export/samples/#int/id_sam') + + self.addr_const_null = q.address_from_fadd_type_fadd("constant", None) + + # qd_rai = desc_quant_from_label('reva ft sample anatomical location distance index raw') + self.qd_nai = q.desc_quant_from_label("reva ft sample anatomical location distance index normalized v1") + self.qd_nain = q.desc_quant_from_label("reva ft sample anatomical location distance index normalized v1 min") + self.qd_naix = q.desc_quant_from_label("reva ft sample anatomical location distance index normalized v1 max") + + self.cd_mod = q.desc_cat_from_label_domain_label("hasDataAboutItModality", None) + self.cd_bot = q.desc_cat_from_label_domain_label( + "bottom", None + ) # we just need something we can reference that points to null so we can have a refernce to all the objects + + self.id_human = q.desc_inst_from_label("human") + self.id_nerve = q.desc_inst_from_label("nerve") + self.id_nerve_volume = q.desc_inst_from_label("nerve-volume") self.luid = { - 'human': self.id_human, - 'nerve': self.id_nerve, - 'nerve-volume': self.id_nerve_volume, + "human": self.id_human, + "nerve": self.id_nerve, + "nerve-volume": self.id_nerve_volume, } - self.ct_mod = q.cterm_from_label('microct') # lol ct ct - self.ct_hack = q.cterm_from_label('hack-associate-some-value') + self.ct_mod = q.cterm_from_label("microct") # lol ct ct + self.ct_hack = q.cterm_from_label("hack-associate-some-value") self.luct = { - 'ct-hack': self.ct_hack, - 'microct': self.ct_mod, + "ct-hack": self.ct_hack, + "microct": self.ct_mod, } @@ -326,20 +359,30 @@ class Inserts: def ingest(dataset_uuid, extract_fun, session, commit=False, dev=False, values_args=None): - """ generic ingest workflow - this_dataset_updated_uuid might not be needed in future, - add a kwarg to control it maybe? + """generic ingest workflow + this_dataset_updated_uuid might not be needed in future, + add a kwarg to control it maybe? """ - ocdn = ' ON CONFLICT DO NOTHING' if dev else '' + ocdn = " ON CONFLICT DO NOTHING" if dev else "" if extract_fun is None and values_args is None: - raise TypeError('need one of extract_fun or values_args') - - (updated_transitive, values_objects, values_dataset_object, - make_values_instances, make_values_parents, - make_void, make_vocd, make_voqd, make_values_cat, make_values_quant - ) = extract_fun(dataset_uuid) if values_args is None else values_args + raise TypeError("need one of extract_fun or values_args") + + ( + updated_transitive, + values_objects, + values_dataset_object, + make_values_instances, + make_values_parents, + make_void, + make_vocd, + make_voqd, + make_values_cat, + make_values_quant, + ) = ( + extract_fun(dataset_uuid) if values_args is None else values_args + ) q = Queries(session) i = InternalIds(q) @@ -348,13 +391,21 @@ def ingest(dataset_uuid, extract_fun, session, commit=False, dev=False, values_a values_instances = make_values_instances(i) res0 = session.execute( - sql_text('INSERT INTO objects (id, id_type) VALUES (:id, :id_type) ON CONFLICT DO NOTHING'), - dict(id=dataset_uuid, id_type='dataset')) + sql_text("INSERT INTO objects (id, id_type) VALUES (:id, :id_type) ON CONFLICT DO NOTHING"), + dict(id=dataset_uuid, id_type="dataset"), + ) # oh dear https://stackoverflow.com/questions/34708509/how-to-use-returning-with-on-conflict-in-postgresql res1 = session.execute( - sql_text("WITH ins AS (INSERT INTO objects_internal (type, dataset, updated_transitive, label) VALUES ('path-metadata', :dataset, :updated_transitive, :label) ON CONFLICT DO NOTHING RETURNING id) SELECT id FROM ins UNION ALL SELECT id FROM objects_internal WHERE type = 'path-metadata' AND dataset = :dataset AND updated_transitive = :updated_transitive"), # TODO see whether we actually need union all here or whether union by itself is sufficient - dict(dataset=dataset_uuid, updated_transitive=updated_transitive, label=f'test-load-for-f001 {isoformat(updated_transitive)}')) + sql_text( + "WITH ins AS (INSERT INTO objects_internal (type, dataset, updated_transitive, label) VALUES ('path-metadata', :dataset, :updated_transitive, :label) ON CONFLICT DO NOTHING RETURNING id) SELECT id FROM ins UNION ALL SELECT id FROM objects_internal WHERE type = 'path-metadata' AND dataset = :dataset AND updated_transitive = :updated_transitive" + ), # TODO see whether we actually need union all here or whether union by itself is sufficient + dict( + dataset=dataset_uuid, + updated_transitive=updated_transitive, + label=f"test-load-for-f001 {isoformat(updated_transitive)}", + ), + ) # it is better to use this approach for all top down information # just assume that it is coming from some combination of the metadata files and the file system @@ -365,17 +416,23 @@ def ingest(dataset_uuid, extract_fun, session, commit=False, dev=False, values_a voqd = make_voqd(this_dataset_updated_uuid, i) res1_1 = session.execute( - sql_text('INSERT INTO objects (id, id_type, id_internal) VALUES (:id, :id_type, :id) ON CONFLICT DO NOTHING'), # FIXME bad ocdn here - dict(id=this_dataset_updated_uuid, id_type='quantdb')) + sql_text( + "INSERT INTO objects (id, id_type, id_internal) VALUES (:id, :id_type, :id) ON CONFLICT DO NOTHING" + ), # FIXME bad ocdn here + dict(id=this_dataset_updated_uuid, id_type="quantdb"), + ) vt, params = makeParamsValues(values_objects) - session.execute(sql_text(f'INSERT INTO objects (id, id_type, id_file) VALUES {vt}{ocdn}'), params) + session.execute(sql_text(f"INSERT INTO objects (id, id_type, id_file) VALUES {vt}{ocdn}"), params) vt, params = makeParamsValues(values_dataset_object) - session.execute(sql_text(f'INSERT INTO dataset_object (dataset, object) VALUES {vt}{ocdn}'), params) + session.execute(sql_text(f"INSERT INTO dataset_object (dataset, object) VALUES {vt}{ocdn}"), params) vt, params = makeParamsValues(values_instances) - session.execute(sql_text(f'INSERT INTO values_inst (dataset, id_formal, type, desc_inst, id_sub, id_sam) VALUES {vt}{ocdn}'), params) + session.execute( + sql_text(f"INSERT INTO values_inst (dataset, id_formal, type, desc_inst, id_sub, id_sam) VALUES {vt}{ocdn}"), + params, + ) # inserts that depend on instances having already been inserted ilt = q.insts_from_dataset_ids(dataset_uuid, [f for d, f, *rest in values_instances]) @@ -385,27 +442,38 @@ def ingest(dataset_uuid, extract_fun, session, commit=False, dev=False, values_a values_qv = make_values_quant(this_dataset_updated_uuid, i, luinst) vt, params = makeParamsValues(values_parents) - session.execute(sql_text(f'INSERT INTO instance_parent VALUES {vt}{ocdn}'), params) + session.execute(sql_text(f"INSERT INTO instance_parent VALUES {vt}{ocdn}"), params) vt, params = makeParamsValues(void) - session.execute(sql_text(f'INSERT INTO obj_desc_inst (object, desc_inst, addr_field, addr_desc_inst) VALUES {vt}{ocdn}'), params) + session.execute( + sql_text(f"INSERT INTO obj_desc_inst (object, desc_inst, addr_field, addr_desc_inst) VALUES {vt}{ocdn}"), params + ) vt, params = makeParamsValues(vocd) - session.execute(sql_text(f'INSERT INTO obj_desc_cat (object, desc_cat, addr_field) VALUES {vt}{ocdn}'), params) + session.execute(sql_text(f"INSERT INTO obj_desc_cat (object, desc_cat, addr_field) VALUES {vt}{ocdn}"), params) vt, params = makeParamsValues(voqd) - session.execute(sql_text(f'INSERT INTO obj_desc_quant (object, desc_quant, addr_field) VALUES {vt}{ocdn}'), params) + session.execute(sql_text(f"INSERT INTO obj_desc_quant (object, desc_quant, addr_field) VALUES {vt}{ocdn}"), params) vt, params = makeParamsValues(values_cv) - session.execute(sql_text(f'INSERT INTO values_cat (value_open, value_controlled, object, desc_inst, desc_cat, instance) VALUES {vt}{ocdn}'), params) + session.execute( + sql_text( + f"INSERT INTO values_cat (value_open, value_controlled, object, desc_inst, desc_cat, instance) VALUES {vt}{ocdn}" + ), + params, + ) vt, params, bindparams = makeParamsValues( # FIXME LOL the types spec here is atrocious ... but it does work ... # XXX and barring the unfortunate case, which we have now encountered where # now fixed in the local impl - values_qv, row_types=(None, None, None, None, None, JSONB)) + values_qv, + row_types=(None, None, None, None, None, JSONB), + ) - t = sql_text(f'INSERT INTO values_quant (value, object, desc_inst, desc_quant, instance, value_blob) VALUES {vt}{ocdn}') + t = sql_text( + f"INSERT INTO values_quant (value, object, desc_inst, desc_quant, instance, value_blob) VALUES {vt}{ocdn}" + ) tin = t.bindparams(*bindparams) session.execute(tin, params) @@ -415,11 +483,16 @@ def ingest(dataset_uuid, extract_fun, session, commit=False, dev=False, values_a def extract_reva_ft(dataset_uuid, source_local=False, visualize=False): if source_local: - with open(pathlib.Path(f'~/.local/share/sparcur/export/datasets/{dataset_uuid}/LATEST/path-metadata.json').expanduser(), 'rt') as f: + with open( + pathlib.Path( + f"~/.local/share/sparcur/export/datasets/{dataset_uuid}/LATEST/path-metadata.json" + ).expanduser(), + "rt", + ) as f: blob = json.load(f) else: - resp = requests.get(f'https://cassava.ucsd.edu/sparc/datasets/{dataset_uuid}/LATEST/path-metadata.json') + resp = requests.get(f"https://cassava.ucsd.edu/sparc/datasets/{dataset_uuid}/LATEST/path-metadata.json") try: blob = resp.json() @@ -427,117 +500,129 @@ def extract_reva_ft(dataset_uuid, source_local=False, visualize=False): breakpoint() raise e - for j in blob['data']: - j['type'] = 'pathmeta' + for j in blob["data"]: + j["type"] = "pathmeta" ir = fromJson(blob) - updated_transitive = max([i['timestamp_updated'] for i in ir['data'][1:]]) # 1: to skip the dataset object itself - - jpx = [r for r in ir['data'] if 'mimetype' in r and r['mimetype'] == 'image/jpx'] - + updated_transitive = max([i["timestamp_updated"] for i in ir["data"][1:]]) # 1: to skip the dataset object itself + jpx = [r for r in ir["data"] if "mimetype" in r and r["mimetype"] == "image/jpx"] exts = [ext(j) for j in jpx] - #hrm = sorted(exts, key=lambda j: j['raw_anat_index']) - #max_rai = max([e['raw_anat_index'] for e in exts]) - #import math - #log_max_rai = math.log10(max_rai) + # hrm = sorted(exts, key=lambda j: j['raw_anat_index']) + # max_rai = max([e['raw_anat_index'] for e in exts]) + # import math + # log_max_rai = math.log10(max_rai) # normalize the index by mapping distinct values to the integers - nondist = sorted([e['raw_anat_index_v1'] for e in exts]) - lin_distinct = {v:i for i, v in enumerate(sorted(set([e['raw_anat_index_v1'] for e in exts])))} + nondist = sorted([e["raw_anat_index_v1"] for e in exts]) + lin_distinct = {v: i for i, v in enumerate(sorted(set([e["raw_anat_index_v1"] for e in exts])))} max_distinct = len(lin_distinct) mdp1 = max_distinct + 0.1 # to simplify adding overlap dd = defaultdict(list) for e in exts: - #e['norm_anat_index'] = math.log10(e['raw_anat_index']) / log_max_rai - pos = lin_distinct[e['raw_anat_index_v1']] - e['norm_anat_index_v1'] = (pos + 0.55) / mdp1 - e['norm_anat_index_v1_min'] = pos / mdp1 - e['norm_anat_index_v1_max'] = (pos + 1.1) / mdp1 # ensure there is overlap between section for purposes of testing + # e['norm_anat_index'] = math.log10(e['raw_anat_index']) / log_max_rai + pos = lin_distinct[e["raw_anat_index_v1"]] + e["norm_anat_index_v1"] = (pos + 0.55) / mdp1 + e["norm_anat_index_v1_min"] = pos / mdp1 + e["norm_anat_index_v1_max"] = ( + pos + 1.1 + ) / mdp1 # ensure there is overlap between section for purposes of testing # TODO norm_anat_index_min # TODO norm_anat_index_max - dd[e['dataset'], e['sample']].append(e) + dd[e["dataset"], e["sample"]].append(e) inst_obj_index = dict(dd) - max_nai = max([e['norm_anat_index_v1'] for e in exts]) - min_nain = min([e['norm_anat_index_v1_min'] for e in exts]) - max_naix = max([e['norm_anat_index_v1_max'] for e in exts]) + max_nai = max([e["norm_anat_index_v1"] for e in exts]) + min_nain = min([e["norm_anat_index_v1_min"] for e in exts]) + max_naix = max([e["norm_anat_index_v1_max"] for e in exts]) if visualize: x = list(range(len(exts))) - #ry = sorted([e['raw_anat_index'] for e in exts]) - ny = sorted([e['norm_anat_index_v1'] for e in exts]) - nyn = sorted([e['norm_anat_index_v1_min'] for e in exts]) - nyx = sorted([e['norm_anat_index_v1_max'] for e in exts]) + # ry = sorted([e['raw_anat_index'] for e in exts]) + ny = sorted([e["norm_anat_index_v1"] for e in exts]) + nyn = sorted([e["norm_anat_index_v1_min"] for e in exts]) + nyx = sorted([e["norm_anat_index_v1_max"] for e in exts]) nnx = list(zip(nyn, nyx)) import pylab as plt import seaborn - #plt.figure() - #seaborn.scatterplot(x=x, y=ry) + + # plt.figure() + # seaborn.scatterplot(x=x, y=ry) plt.figure() - #end = 10 + # end = 10 end = len(x) seaborn.scatterplot(x=x[:end], y=ny[:end]) seaborn.scatterplot(x=x[:end], y=nyn[:end]) seaborn.scatterplot(x=x[:end], y=nyx[:end]) - datasets = {i.uuid: {'id_type': i.type} - for e in exts - if (i := e['dataset']) - } + datasets = {i.uuid: {"id_type": i.type} for e in exts if (i := e["dataset"])} - packages = {i.uuid: { - 'id_type': i.type, - 'id_file': e['file_id'], + packages = { + i.uuid: { + "id_type": i.type, + "id_file": e["file_id"], + } + for e in exts + if (i := e["object"]) } - for e in exts - if (i := e['object']) - } objects = {**datasets, **packages} - dataset_object = list(set((d.uuid, o.uuid) for e in exts - if (d := e['dataset']) and (o := e['object']) - )) - - subjects = {k: {'type': 'subject', - 'desc_inst': 'human', - 'id_sub': k[1], - } for k in sorted(set((e['dataset'], e['subject']) for e in exts))} - segments = {k[:2]: {'type': 'sample', # FIXME vs below ??? - 'desc_inst': 'nerve-volume', # FIXME should this be nerve-segment and then we use nerve-volume for the 1:1 with files? - 'id_sub': k[-1], - 'id_sam': k[1], - } for k in sorted(set((e['dataset'], e['sample'], e['subject']) for e in exts))} - parents = sorted(set((e['dataset'],) + p for e in exts for p in e['parents'])) - sam_other = {p[:2]:{'type': 'sample', 'desc_inst': 'nerve', 'id_sub': p[-1], 'id_sam': p[1]} for p in parents if p[:2] not in segments} + dataset_object = list(set((d.uuid, o.uuid) for e in exts if (d := e["dataset"]) and (o := e["object"]))) + + subjects = { + k: { + "type": "subject", + "desc_inst": "human", + "id_sub": k[1], + } + for k in sorted(set((e["dataset"], e["subject"]) for e in exts)) + } + segments = { + k[:2]: { + "type": "sample", # FIXME vs below ??? + "desc_inst": "nerve-volume", # FIXME should this be nerve-segment and then we use nerve-volume for the 1:1 with files? + "id_sub": k[-1], + "id_sam": k[1], + } + for k in sorted(set((e["dataset"], e["sample"], e["subject"]) for e in exts)) + } + parents = sorted(set((e["dataset"],) + p for e in exts for p in e["parents"])) + sam_other = { + p[:2]: {"type": "sample", "desc_inst": "nerve", "id_sub": p[-1], "id_sam": p[1]} + for p in parents + if p[:2] not in segments + } samples = {**segments, **sam_other} instances = {**subjects, **samples} values_objects = [ - (i, o['id_type'], o['id_file'] if 'id_file' in o else None) + (i, o["id_type"], o["id_file"] if "id_file" in o else None) for i, o in objects.items() - if o['id_type'] != 'dataset' # already did it above + if o["id_type"] != "dataset" # already did it above ] values_dataset_object = dataset_object def make_values_instances(i): values_instances = [ - (d.uuid, f, inst['type'], i.luid[inst['desc_inst']], - inst['id_sub'] if 'id_sub' in inst else None, - inst['id_sam'] if 'id_sam' in inst else None, + ( + d.uuid, + f, + inst["type"], + i.luid[inst["desc_inst"]], + inst["id_sub"] if "id_sub" in inst else None, + inst["id_sam"] if "id_sam" in inst else None, ) - for (d, f), inst in instances.items()] + for (d, f), inst in instances.items() + ] return values_instances def make_values_parents(luinst): - """ need the lookup for instances """ - values_parents = [ - (luinst[d.uuid, child], luinst[d.uuid, parent]) - for d, child, parent in parents] + """need the lookup for instances""" + values_parents = [(luinst[d.uuid, child], luinst[d.uuid, parent]) for d, child, parent in parents] return values_parents # XXX REMINDER an object descriptor pair can be associated with an arbitrary number of measured instances @@ -548,20 +633,25 @@ def make_values_parents(luinst): # XXX the external source is part of the issue I think def make_void(this_dataset_updated_uuid, i): void = [ # FIXME this is rather annoying because you have to list all expected types in advance, but I guess that is what we want - (this_dataset_updated_uuid, i.id_human, i.addr_jpsuid, i.addr_jpspec), (this_dataset_updated_uuid, i.id_nerve, i.addr_jpsaid, i.addr_jpsaty), (this_dataset_updated_uuid, i.id_nerve_volume, i.addr_jpsaid, i.addr_jpsaty), - # FIXME what about manifests? those link metadata as an extra hop ... everything meta related needs to come from combined object metadata ??? # that would certainly make more sense than the nonsense that is going on here, it would simplify the referencing for all the topdown # information that we have but it sort of obscures sources, however this _is_ contextual info ... sigh # XXX the other option would just be to just put the darned files in the instance measured table :/ because we do have data about them # annoying :/ - - #+ [(i, 'nerve-volume', addr_context) for i in packages] - ] + [(o, i.id_nerve_volume, i.addr_const_null, None) # XXX FIXME this is the only way I can think to do this right now ? - for o, b in objects.items() if b['id_type'] == 'package'] + # + [(i, 'nerve-volume', addr_context) for i in packages] + ] + [ + ( + o, + i.id_nerve_volume, + i.addr_const_null, + None, + ) # XXX FIXME this is the only way I can think to do this right now ? + for o, b in objects.items() + if b["id_type"] == "package" + ] return void @@ -570,8 +660,11 @@ def make_vocd(this_dataset_updated_uuid, i): # FIXME this reveals that there are cases where we may not have void for a single file or that the id comes from context and is not embedded # figuring out how to turn that around is going to take a bit of thinking (this_dataset_updated_uuid, i.cd_mod, i.addr_jpmod), - ] + [(o, i.cd_bot, i.addr_const_null) # XXX FIXME this is the only way I can think to do this right now ? - for o, b in objects.items() if b['id_type'] == 'package'] + ] + [ + (o, i.cd_bot, i.addr_const_null) # XXX FIXME this is the only way I can think to do this right now ? + for o, b in objects.items() + if b["id_type"] == "package" + ] return vocd @@ -579,35 +672,38 @@ def make_voqd(this_dataset_updated_uuid, i): voqd = [ # FIXME this isn't quite right, we should just do it to the segments and pretend it is from the samples file I think? # FIXME addr_aspect and addr_unit are ... implicit, there is no conditional dispatch but the choice of the constant quantative descriptor comes from ... tgbugs? or what? I think we just leave it as null because it is a constant across all fields, but maybe we just use constant null? but why bother in that case? (this_dataset_updated_uuid, i.qd_nai, i.addr_jpnai), - (this_dataset_updated_uuid, i.qd_nain, i.addr_jpnain), # XXX FIXME is this really an aggregation type in this case? I guess it technically if the sample space is over all the points inside the segment or something + ( + this_dataset_updated_uuid, + i.qd_nain, + i.addr_jpnain, + ), # XXX FIXME is this really an aggregation type in this case? I guess it technically if the sample space is over all the points inside the segment or something (this_dataset_updated_uuid, i.qd_naix, i.addr_jpnaix), ] return voqd def make_values_cat(this_dataset_updated_uuid, i, luinst): - #obj_index = {e['object']: e for e in exts} + # obj_index = {e['object']: e for e in exts} values_cv = [ # value_open, value_controlled, object, desc_inst, desc_cat - (e[k], - i.luct[e[k]], - this_dataset_updated_uuid, - #e['object'].uuid, # FIXME still not right this comes from the updated latest - i.id_nerve_volume, - cd, # if we mess this up the fk ok obj_desc_cat will catch it :) - luinst[e['dataset'].uuid, e['sample']], # get us the instance + ( + e[k], + i.luct[e[k]], + this_dataset_updated_uuid, + # e['object'].uuid, # FIXME still not right this comes from the updated latest + i.id_nerve_volume, + cd, # if we mess this up the fk ok obj_desc_cat will catch it :) + luinst[e["dataset"].uuid, e["sample"]], # get us the instance ) for e in exts - for k, cd in ( - ('modality', i.cd_mod), - ) - + for k, cd in (("modality", i.cd_mod),) ] + [ - (None, - i.ct_hack, - e['object'].uuid, - i.id_nerve_volume, - i.cd_bot, # if we mess this up the fk ok obj_desc_cat will catch it :) - luinst[e['dataset'].uuid, e['sample']], # get us the instance + ( + None, + i.ct_hack, + e["object"].uuid, + i.id_nerve_volume, + i.cd_bot, # if we mess this up the fk ok obj_desc_cat will catch it :) + luinst[e["dataset"].uuid, e["sample"]], # get us the instance ) for e in exts ] @@ -616,41 +712,48 @@ def make_values_cat(this_dataset_updated_uuid, i, luinst): def make_values_quant(this_dataset_updated_uuid, i, luinst): values_qv = [ # value, object, desc_inst, desc_quant, inst, value_blob - (e[k], - #e['object'].uuid, # FIXME TODO we could fill this here but we choose to use this_dataset_updated_uuid instead I think - this_dataset_updated_uuid, - i.id_nerve_volume, - qd, # if we mess this up the fk ok obj_desc_cat will catch it :) - luinst[e['dataset'].uuid, e['sample']], # get us the instance - e[k], + ( + e[k], + # e['object'].uuid, # FIXME TODO we could fill this here but we choose to use this_dataset_updated_uuid instead I think + this_dataset_updated_uuid, + i.id_nerve_volume, + qd, # if we mess this up the fk ok obj_desc_cat will catch it :) + luinst[e["dataset"].uuid, e["sample"]], # get us the instance + e[k], ) for e in exts for k, qd in ( - #('raw_anat_index', qd_rai), # XXX this is a bad place to store object -> field -> qd mappings also risks mismatch on address - ('norm_anat_index_v1', i.qd_nai), - ('norm_anat_index_v1_min', i.qd_nain), - ('norm_anat_index_v1_max', i.qd_naix), + # ('raw_anat_index', qd_rai), # XXX this is a bad place to store object -> field -> qd mappings also risks mismatch on address + ("norm_anat_index_v1", i.qd_nai), + ("norm_anat_index_v1_min", i.qd_nain), + ("norm_anat_index_v1_max", i.qd_naix), ) ] return values_qv - return (updated_transitive, values_objects, values_dataset_object, - make_values_instances, make_values_parents, - make_void, make_vocd, make_voqd, - make_values_cat, make_values_quant, - ) + return ( + updated_transitive, + values_objects, + values_dataset_object, + make_values_instances, + make_values_parents, + make_void, + make_vocd, + make_voqd, + make_values_cat, + make_values_quant, + ) # this is where things get annoying with needing selects on instance measured def ingest_reva_ft_all(session, source_local=False, do_insert=True, batch=False, commit=False, dev=False): - dataset_uuids = ( - 'aa43eda8-b29a-4c25-9840-ecbd57598afc', # f001 + "aa43eda8-b29a-4c25-9840-ecbd57598afc", # f001 # the rest have uuid1 issues :/ all in the undefined folder it seems, might be able to fix with a reupload - 'bc4cc558-727c-4691-ae6d-498b57a10085', # f002 # XXX has a uuid1 so breaking in prod right now have to push the new pipelines - 'ec6ad74e-7b59-409b-8fc7-a304319b6faf', # f003 # also uuid1 issue - 'a8b2bdc7-54df-46a3-810e-83cdf33cfc3a', # f004 - '04a5fed9-7ba6-4292-b1a6-9cab5c38895f', # f005 + "bc4cc558-727c-4691-ae6d-498b57a10085", # f002 # XXX has a uuid1 so breaking in prod right now have to push the new pipelines + "ec6ad74e-7b59-409b-8fc7-a304319b6faf", # f003 # also uuid1 issue + "a8b2bdc7-54df-46a3-810e-83cdf33cfc3a", # f004 + "04a5fed9-7ba6-4292-b1a6-9cab5c38895f", # f005 ) batched = [] @@ -670,9 +773,9 @@ def ingest_reva_ft_all(session, source_local=False, do_insert=True, batch=False, def main(source_local=False, commit=False, echo=True): from quantdb.config import auth - dbkwargs = {k:auth.get(f'db-{k}') # TODO integrate with cli options - for k in ('user', 'host', 'port', 'database')} - dbkwargs['dbuser'] = dbkwargs.pop('user') + + dbkwargs = {k: auth.get(f"db-{k}") for k in ("user", "host", "port", "database")} # TODO integrate with cli options + dbkwargs["dbuser"] = dbkwargs.pop("user") engine = create_engine(dbUri(**dbkwargs)) engine.echo = echo session = Session(engine) @@ -689,5 +792,5 @@ def main(source_local=False, commit=False, echo=True): engine.dispose() -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/quantdb/main.py b/quantdb/main.py index ad52ff0..deb5246 100644 --- a/quantdb/main.py +++ b/quantdb/main.py @@ -1,5 +1,6 @@ from fastapi import FastAPI from fastapi.middleware.wsgi import WSGIMiddleware + from quantdb.api_server import app as flask_app app = FastAPI() diff --git a/quantdb/router.py b/quantdb/router.py index df75984..0d2913c 100644 --- a/quantdb/router.py +++ b/quantdb/router.py @@ -1,223 +1,21 @@ -from __future__ import annotations +from typing import Literal -from functools import lru_cache -from typing import Any, Generator +import uvicorn +from fastapi import FastAPI +from fastapi.middleware.wsgi import WSGIMiddleware +from fastapi.staticfiles import StaticFiles -from fastapi import Depends, FastAPI -from sqlalchemy import text -from sqlalchemy.orm import Session # type: ignore - -from quantdb.config import Settings - -from quantdb import mysql_app +from quantdb.api_server import app as quantdb_flask_app app = FastAPI() +app.mount("/quantdb", WSGIMiddleware(quantdb_flask_app)) -@lru_cache() -def get_settings(): - """ - Get settings from mongodb - alternative for test and production credientials - - Returns - ------- - Settings - pydantic BaseSettings model with MongoDB credentials - """ - return Settings() - - -def get_mysql_db() -> Generator[Session, Any, None]: - db = mysql_app.database.SessionLocal() - try: - yield db - finally: - db.close() - - -@app.get( - "/example", - status_code=200, -) -def get_example( - mysql_db: Session = Depends(get_mysql_db), # type: ignore -): - query = text( - """ - select * from limit 10 - """ - ) - result = mysql_db.execute(query) - data = result.mappings().all() - return data - - def default_flow(endpoint, record_type, query_fun, json_fun, alt_query_fun=None): - try: - kwargs = getArgs(request, endpoint, dev=dev) - except exc.UnknownArg as e: - return json.dumps({"error": e.args[0], "http_response_status": 422}), 422 - except Exception as e: - breakpoint() - raise e - - def gkw(k): - return k in kwargs and kwargs[k] - - if gkw("include-unused"): - query_fun = alt_query_fun - - # FIXME record_type is actually determined entirely in query_fun right now - try: - query, params = query_fun(endpoint, kwargs) - except Exception as e: - breakpoint() - raise e - - if gkw("return-query"): - return query - - try: - res = session.execute(sql_text(query), params) - except Exception as e: - breakpoint() - raise e - - try: - out = json_fun(record_type, res, prov=("prov" in kwargs and kwargs["prov"])) - resp = json.dumps(wrap_out(endpoint, kwargs, out), cls=JEncode), 200, {"Content-Type": "application/json"} - except Exception as e: - breakpoint() - raise e - - return resp - - -# @app.route(f"{bp}/test") -# def route_test_check(): -# "objects with derived values that match all criteria" -# return "testing-api" - -# @app.route(f"{bp}/objects") -# def route_1_objects(): -# "objects with derived values that match all criteria" -# return default_flow("objects", "object", main_query, to_json) - -# @app.route(f"{bp}/desc/inst") -# @app.route(f"{bp}/descriptors/inst") -# @app.route(f"{bp}/classes") -# def route_1_desc_inst(): -# def query(endpoint, kwargs): -# return ( -# ( -# "SELECT " -# "id.iri, " -# "id.label, " -# "idpar.label as subclassof" -# """ -# FROM descriptors_inst AS id -# LEFT OUTER JOIN class_parent AS ip ON ip.id = id.id -# LEFT OUTER JOIN descriptors_inst AS idpar ON idpar.id = ip.parent -# """ -# ), -# {}, -# ) - -# return default_flow("desc/inst", "desc-inst", main_query, to_json, alt_query_fun=query) - -# @app.route(f"{bp}/desc/cat") -# @app.route(f"{bp}/descriptors/cat") -# @app.route(f"{bp}/predicates") -# def route_1_desc_cat(): -# def query(endpoint, kwargs): -# return ( -# "select " -# "cd.label, " -# "cdid.label AS domain, " -# "cd.range, " -# "cd.description " -# "from descriptors_cat as cd " -# "left outer join descriptors_inst as cdid on cdid.id = cd.domain" -# ), {} - -# return default_flow( -# "desc/cat", "desc-cat", main_query, to_json, alt_query_fun=query -# ) # TODO likely need different args e.g. to filter by desc_inst - -# @app.route(f"{bp}/desc/quant") -# @app.route(f"{bp}/descriptors/quant") -# def route_1_desc_quant(): -# def query(endpoint, kwargs): -# return ( -# "select " -# "qd.label, " -# "id.label AS domain, " -# "qd.shape, " -# "qd.aggregation_type as agg_type, " -# "a.label AS aspect, " -# "u.label AS unit, " -# "qd.description " -# "from descriptors_quant as qd " -# "left outer join descriptors_inst as id on id.id = qd.domain " -# "left outer join units as u on u.id = qd.unit " -# "join aspects as a on a.id = qd.aspect" -# ), {} - -# return default_flow( -# "desc/quant", "desc-quant", main_query, to_json, alt_query_fun=query -# ) # TODO likely need different args e.g. to filter by desc_inst - -# @app.route(f"{bp}/values/inst") -# @app.route(f"{bp}/instances") -# def route_1_val_inst(): -# "instances associated with values that match all critiera" -# return default_flow("values/inst", "instance", main_query, to_json) - -# @app.route(f"{bp}/values") -# @app.route(f"{bp}/values/cat-quant") -# def route_1_val_cat_quant(): -# return default_flow("values/cat-quant", None, main_query, to_json) - -# @app.route(f"{bp}/values/cat") -# def route_1_val_cat(): -# return default_flow("values/cat", "value-cat", main_query, to_json) - -# @app.route(f"{bp}/values/quant") -# def route_1_val_quant(): -# return default_flow("values/quant", "value-quant", main_query, to_json) - -# @app.route(f"{bp}/terms") -# @app.route(f"{bp}/controlled-terms") -# def route_1_cterms(): -# def query(endpoint, kwargs): -# return ("select " "ct.iri, " "ct.label " "from controlled_terms as ct"), {} - -# return default_flow("terms", "term", main_query, to_json, alt_query_fun=query) - -# @app.route(f"{bp}/units") -# def route_1_units(): -# def query(endpoint, kwargs): -# return ("select " "u.iri, " "u.label " "from units as u"), {} - -# return default_flow("units", "unit", main_query, to_json, alt_query_fun=query) - -# @app.route(f"{bp}/aspects") -# def route_1_aspects(): -# def query(endpoint, kwargs): -# return ( -# ( -# "SELECT " -# "a.iri, " -# "a.label, " -# "aspar.label AS subclassof " -# """ -# FROM aspects AS a -# LEFT OUTER JOIN aspect_parent AS ap ON ap.id = a.id -# LEFT OUTER JOIN aspects AS aspar ON aspar.id = ap.parent -# """ -# ), -# {}, -# ) +# Root URL +@app.get("/") +def index() -> Literal["Hello"]: + return "Hello" -# return default_flow("aspects", "aspect", main_query, to_json, alt_query_fun=query) -# return app +if __name__ == "__main__": + uvicorn.run("router:app", host="localhost", port=8000, reload=True) diff --git a/quantdb/utils.py b/quantdb/utils.py index 169a5c1..7619e64 100644 --- a/quantdb/utils.py +++ b/quantdb/utils.py @@ -1,6 +1,6 @@ +import logging import os import sys -import logging from datetime import datetime @@ -12,46 +12,39 @@ def makeSimpleLogger(name, level=logging.INFO): logger.setLevel(level) ch = logging.StreamHandler() # FileHander goes to disk - fmt = ('[%(asctime)s] - %(levelname)8s - ' - '%(name)14s - ' - '%(filename)16s:%(lineno)-4d - ' - '%(message)s') + fmt = "[%(asctime)s] - %(levelname)8s - " "%(name)14s - " "%(filename)16s:%(lineno)-4d - " "%(message)s" formatter = logging.Formatter(fmt) ch.setFormatter(formatter) logger.addHandler(ch) return logger -log = makeSimpleLogger('quantdb') -logd = log.getChild('data') +log = makeSimpleLogger("quantdb") +logd = log.getChild("data") # from pyontutils.utils_fast import setPS1 def setPS1(script__file__): try: - text = 'Running ' + os.path.basename(script__file__) - os.sys.stdout.write('\x1b]2;{}\x07\n'.format(text)) + text = "Running " + os.path.basename(script__file__) + os.sys.stdout.write("\x1b]2;{}\x07\n".format(text)) except AttributeError as e: log.exception(e) def dbUri(dbuser, host, port, database): - if hasattr(sys, 'pypy_version_info'): - dialect = 'psycopg2cffi' + if hasattr(sys, "pypy_version_info"): + dialect = "psycopg2cffi" else: - dialect = 'psycopg2' - return f'postgresql+{dialect}://{dbuser}@{host}:{port}/{database}' + dialect = "psycopg2" + return f"postgresql+{dialect}://{dbuser}@{host}:{port}/{database}" # from pyontutils.utils_fast import isoformat -def isoformat(datetime_instance, timespec='auto'): +def isoformat(datetime_instance, timespec="auto"): kwargs = {} if isinstance(datetime_instance, datetime): # don't pass timespec if type is not date not datetime - kwargs['timespec'] = timespec - - return (datetime_instance - .isoformat(**kwargs) - .replace('.', ',') - .replace('+00:00', 'Z')) + kwargs["timespec"] = timespec + return datetime_instance.isoformat(**kwargs).replace(".", ",").replace("+00:00", "Z") diff --git a/sql/postgres.sql b/sql/postgres.sql index dbbe6ce..f556ff3 100644 --- a/sql/postgres.sql +++ b/sql/postgres.sql @@ -35,4 +35,3 @@ ALTER ROLE "quantdb-test-admin" SET search_path = quantdb, public; ALTER ROLE "quantdb-test-user" SET search_path = quantdb, public; ALTER ROLE "quantdb-admin" SET search_path = quantdb, public; ALTER ROLE "quantdb-user" SET search_path = quantdb, public; - diff --git a/test/test_api.py b/test/test_api.py index 88ef2bf..f1a97a9 100644 --- a/test/test_api.py +++ b/test/test_api.py @@ -1,6 +1,8 @@ import json import pprint + from flask_sqlalchemy import SQLAlchemy + from quantdb.api import make_app from quantdb.utils import log @@ -11,99 +13,80 @@ def test(): client = app.test_client() runner = app.test_cli_runner() - dataset_uuid = 'aa43eda8-b29a-4c25-9840-ecbd57598afc' - some_object = '414886a9-9ec7-447e-b4d8-3ae42fda93b7' # XXX FAKE - actual_package_uuid = '15bcbcd5-b054-40ef-9b5c-6a260d441621' - base = 'http://localhost:8989/api/1/' + dataset_uuid = "aa43eda8-b29a-4c25-9840-ecbd57598afc" + some_object = "414886a9-9ec7-447e-b4d8-3ae42fda93b7" # XXX FAKE + actual_package_uuid = "15bcbcd5-b054-40ef-9b5c-6a260d441621" + base = "http://localhost:8989/api/1/" urls = ( - f'{base}values/inst', - f'{base}values/inst?dataset={dataset_uuid}', - f'{base}values/inst?dataset={dataset_uuid}&union-cat-quant=true', - f'{base}values/inst?dataset={dataset_uuid}&aspect=distance&aspect=time', - f'{base}values/inst?dataset={dataset_uuid}&aspect=distance&value-quant-min=0.5', - f'{base}values/inst?desc-inst=nerve-volume', - - f'{base}objects?dataset={dataset_uuid}', - f'{base}objects?dataset={dataset_uuid}&aspect=distance', - f'{base}objects?dataset={dataset_uuid}&aspect=distance&value-quant-min=0.5', # expect nothing - f'{base}objects?dataset={dataset_uuid}&aspect=distance&value-quant-min=0.5&union-cat-quant=true', - - f'{base}objects?dataset={dataset_uuid}&subject=sub-f001', - f'{base}objects?subject=sub-f001', - f'{base}objects?subject=sub-f001&union-cat-quant=true', - f'{base}objects?subject=sub-f001&subject=sub-f002&subject=sub-f003&subject=sub-f004&subject=sub-f005', - f'{base}objects?subject=sub-f001&subject=sub-f002&subject=sub-f003&subject=sub-f004&subject=sub-f005&union-cat-quant=true', - f'{base}objects?subject=sub-f001&desc-cat=none&value-quant-min=0.5&union-cat-quant=true', - f'{base}objects?subject=sub-f001&desc-cat=none&aspect=distance&value-quant-min=0.5&union-cat-quant=true', - f'{base}objects?subject=sub-f001&aspect=distance&value-quant-min=0.5&union-cat-quant=true', - f'{base}objects?aspect=distance&value-quant-min=0.5&union-cat-quant=true', - f'{base}objects?desc-cat=none&aspect=distance&value-quant-min=0.5&union-cat-quant=true', - f'{base}objects?desc-cat=none&aspect=distance&value-quant-min=0.5', - f'{base}objects?aspect=distance&value-quant-min=0.5', - f'{base}objects?aspect=distance&value-quant-min=0.5&source-only=true', - f'{base}objects?desc-inst=nerve-volume&aspect=distance&value-quant-min=0.5&source-only=true', - + f"{base}values/inst", + f"{base}values/inst?dataset={dataset_uuid}", + f"{base}values/inst?dataset={dataset_uuid}&union-cat-quant=true", + f"{base}values/inst?dataset={dataset_uuid}&aspect=distance&aspect=time", + f"{base}values/inst?dataset={dataset_uuid}&aspect=distance&value-quant-min=0.5", + f"{base}values/inst?desc-inst=nerve-volume", + f"{base}objects?dataset={dataset_uuid}", + f"{base}objects?dataset={dataset_uuid}&aspect=distance", + f"{base}objects?dataset={dataset_uuid}&aspect=distance&value-quant-min=0.5", # expect nothing + f"{base}objects?dataset={dataset_uuid}&aspect=distance&value-quant-min=0.5&union-cat-quant=true", + f"{base}objects?dataset={dataset_uuid}&subject=sub-f001", + f"{base}objects?subject=sub-f001", + f"{base}objects?subject=sub-f001&union-cat-quant=true", + f"{base}objects?subject=sub-f001&subject=sub-f002&subject=sub-f003&subject=sub-f004&subject=sub-f005", + f"{base}objects?subject=sub-f001&subject=sub-f002&subject=sub-f003&subject=sub-f004&subject=sub-f005&union-cat-quant=true", + f"{base}objects?subject=sub-f001&desc-cat=none&value-quant-min=0.5&union-cat-quant=true", + f"{base}objects?subject=sub-f001&desc-cat=none&aspect=distance&value-quant-min=0.5&union-cat-quant=true", + f"{base}objects?subject=sub-f001&aspect=distance&value-quant-min=0.5&union-cat-quant=true", + f"{base}objects?aspect=distance&value-quant-min=0.5&union-cat-quant=true", + f"{base}objects?desc-cat=none&aspect=distance&value-quant-min=0.5&union-cat-quant=true", + f"{base}objects?desc-cat=none&aspect=distance&value-quant-min=0.5", + f"{base}objects?aspect=distance&value-quant-min=0.5", + f"{base}objects?aspect=distance&value-quant-min=0.5&source-only=true", + f"{base}objects?desc-inst=nerve-volume&aspect=distance&value-quant-min=0.5&source-only=true", # values-quant - f'{base}values/quant?dataset={dataset_uuid}&aspect=distance', - f'{base}values/quant?object={actual_package_uuid}&aspect=distance', - f'{base}values/quant?aspect=distance', - f'{base}values/quant?aspect=distance-via-reva-ft-sample-id-normalized-v1', - + f"{base}values/quant?dataset={dataset_uuid}&aspect=distance", + f"{base}values/quant?object={actual_package_uuid}&aspect=distance", + f"{base}values/quant?aspect=distance", + f"{base}values/quant?aspect=distance-via-reva-ft-sample-id-normalized-v1", # values-cat - f'{base}values/cat?object={actual_package_uuid}', - f'{base}values/cat?object={actual_package_uuid}&union-cat-quant=true', # shouldn't need it in this case - - f'{base}values/cat-quant?object={actual_package_uuid}', - f'{base}values/cat-quant?object={actual_package_uuid}&union-cat-quant=true', - + f"{base}values/cat?object={actual_package_uuid}", + f"{base}values/cat?object={actual_package_uuid}&union-cat-quant=true", # shouldn't need it in this case + f"{base}values/cat-quant?object={actual_package_uuid}", + f"{base}values/cat-quant?object={actual_package_uuid}&union-cat-quant=true", # values-cat-quant - f'{base}values?dataset={dataset_uuid}&aspect=distance&value-quant-min=0.5', - f'{base}values?dataset={dataset_uuid}&aspect=distance&value-quant-min=0.5&union-cat-quant=true', - - f'{base}values?object={actual_package_uuid}', - f'{base}values?object={actual_package_uuid}&union-cat-quant=true', - - f'{base}values/inst?object={actual_package_uuid}', - f'{base}values/inst?object={actual_package_uuid}&union-cat-quant=true', - + f"{base}values?dataset={dataset_uuid}&aspect=distance&value-quant-min=0.5", + f"{base}values?dataset={dataset_uuid}&aspect=distance&value-quant-min=0.5&union-cat-quant=true", + f"{base}values?object={actual_package_uuid}", + f"{base}values?object={actual_package_uuid}&union-cat-quant=true", + f"{base}values/inst?object={actual_package_uuid}", + f"{base}values/inst?object={actual_package_uuid}&union-cat-quant=true", # prov - f'{base}values/inst?prov=true', - - f'{base}values/quant?aspect=distance&prov=true', - - f'{base}values/cat?object={actual_package_uuid}', - f'{base}values/cat?object={actual_package_uuid}&prov=true', # FIXME somehow this has a 3x increase in records, and non-distinct - - f'{base}values/cat-quant?object={actual_package_uuid}&union-cat-quant=true', - f'{base}values/cat-quant?object={actual_package_uuid}&union-cat-quant=true&prov=true', - - f'{base}values/cat-quant', - f'{base}values/cat-quant?prov=true', - - f'{base}values/cat-quant?union-cat-quant=true', - f'{base}values/cat-quant?union-cat-quant=true&prov=true', - + f"{base}values/inst?prov=true", + f"{base}values/quant?aspect=distance&prov=true", + f"{base}values/cat?object={actual_package_uuid}", + f"{base}values/cat?object={actual_package_uuid}&prov=true", # FIXME somehow this has a 3x increase in records, and non-distinct + f"{base}values/cat-quant?object={actual_package_uuid}&union-cat-quant=true", + f"{base}values/cat-quant?object={actual_package_uuid}&union-cat-quant=true&prov=true", + f"{base}values/cat-quant", + f"{base}values/cat-quant?prov=true", + f"{base}values/cat-quant?union-cat-quant=true", + f"{base}values/cat-quant?union-cat-quant=true&prov=true", # desc - f'{base}desc/inst', - f'{base}desc/cat', - f'{base}desc/quant', - - f'{base}desc/inst?include-unused=true', - f'{base}desc/cat?include-unused=true', - f'{base}desc/quant?include-unused=true', - + f"{base}desc/inst", + f"{base}desc/cat", + f"{base}desc/quant", + f"{base}desc/inst?include-unused=true", + f"{base}desc/cat?include-unused=true", + f"{base}desc/quant?include-unused=true", # descriptor values - f'{base}terms', - f'{base}aspects', - f'{base}units', - - f'{base}terms?include-unused=true', - f'{base}aspects?include-unused=true', - f'{base}units?include-unused=true', + f"{base}terms", + f"{base}aspects", + f"{base}units", + f"{base}terms?include-unused=true", + f"{base}aspects?include-unused=true", + f"{base}units?include-unused=true", # TODO maybe shapes here as well? - ) - #log.setLevel(9) + # log.setLevel(9) resps = [] for url in urls: log.debug(url) @@ -115,8 +98,7 @@ def test(): pprint.pprint(resps, width=120) # (i := 6, resps[i], urls[i]) - #q = client.get(f'{base}values/quant?dataset={dataset_uuid}&aspect=distance&return-query=true').data.decode() - #q = client.get(f'{base}values/cat?object={actual_package_uuid}&prov=true&return-query=true').data.decode() - #print(q) + # q = client.get(f'{base}values/quant?dataset={dataset_uuid}&aspect=distance&return-query=true').data.decode() + # q = client.get(f'{base}values/cat?object={actual_package_uuid}&prov=true&return-query=true').data.decode() + # print(q) breakpoint() - From d2df004fb7c6e202a805a365cdb2765332b06593 Mon Sep 17 00:00:00 2001 From: EC2 Default User Date: Tue, 25 Jun 2024 19:51:13 +0000 Subject: [PATCH 09/18] pydantic not needed --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a216a3b..40438fe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "hatchling.build" [project] dependencies = [ "sqlalchemy ~= 2.0.29", - "pydantic ~= 2.6.4", + # "pydantic ~= 2.6.4", "fastapi ~= 0.110.1", "uvicorn ~= 0.29.0", "pydantic-settings ~= 2.2.1", From a6c144df8f637428212882e6f44f228e327c4dca Mon Sep 17 00:00:00 2001 From: Troy Sincomb Date: Mon, 28 Apr 2025 13:38:25 -0700 Subject: [PATCH 10/18] pre-commit try --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6ce8cf5..2fa1306 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,6 +21,6 @@ repos: exclude: | (?x)^( quantdb/api.py| - quantdb/ingest.py| + quantdb/ingest.py|ls sql;/.* )$ From dc0776f7e5e7a2f06fceb0e384969b76bfac6ebc Mon Sep 17 00:00:00 2001 From: Troy Sincomb Date: Mon, 28 Apr 2025 13:40:24 -0700 Subject: [PATCH 11/18] accepted all incoming changes for hangnail merge --- .pre-commit-config.yaml | 2 +- quantdb/ingest.py | 360 ---------------------------------------- 2 files changed, 1 insertion(+), 361 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2fa1306..6ce8cf5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,6 +21,6 @@ repos: exclude: | (?x)^( quantdb/api.py| - quantdb/ingest.py|ls + quantdb/ingest.py| sql;/.* )$ diff --git a/quantdb/ingest.py b/quantdb/ingest.py index 6c52f19..b3f514d 100644 --- a/quantdb/ingest.py +++ b/quantdb/ingest.py @@ -5,14 +5,8 @@ import requests from sparcur import objects as sparcur_objects # register pathmeta type - -<<<<<<< HEAD -# FIXME sparcur dependencies, or keep ingest separate -======= from sparcur.paths import Path from sparcur.utils import PennsieveId as RemoteId - ->>>>>>> master from sparcur.utils import fromJson from sqlalchemy import create_engine from sqlalchemy.dialects.postgresql import JSONB @@ -22,20 +16,12 @@ from quantdb.utils import dbUri, isoformat, log -<<<<<<< HEAD -======= - # FIXME sparcur dependencies, or keep ingest separate ->>>>>>> master ######### start database interaction section -<<<<<<< HEAD -log = log.getChild("ingest") -======= log = log.getChild('ingest') ->>>>>>> master try: if get_ipython().__class__.__name__ == "ZMQInteractiveShell": @@ -115,13 +101,8 @@ def makeParamsValues(*value_sets, constants=tuple(), types=tuple(), row_types=tu else: proto_params = [(tuple(getname(value) for value in row), row) for row in values] -<<<<<<< HEAD - values_template = ", ".join( - "(" + ", ".join(constants + tuple(":" + name for name in names)) + ")" for names, _ in proto_params -======= values_template = ', '.join( '(' + ', '.join(constants + tuple(':' + name for name in names)) + ')' for names, _ in proto_params ->>>>>>> master ) yield values_template if row_types: @@ -157,19 +138,11 @@ def makeParamsValues(*value_sets, constants=tuple(), types=tuple(), row_types=tu a abdominal """ sam_ordering = { -<<<<<<< HEAD - "l": 0, # left - "r": 0, # right - "c": 0, # cardiac safe to keep at zero since the c index usually come after t - "a": 1, # anterior abdominal - "p": 1, # posterior abdominal -======= 'l': 0, # left 'r': 0, # right 'c': 1, # central +cardiac safe to keep at zero since the c index usually come after t+ FIXME means central so it is where both sides merge so have to redo the ordering which will force a v2 'a': 2, # anterior 'p': 2, # posterior ->>>>>>> master } seg_ordering = { "c": 0, # cervical @@ -192,11 +165,7 @@ def anat_index(sample): seg_ind = v break else: -<<<<<<< HEAD - if sam_id == "c": -======= if sam_id == 'c': ->>>>>>> master # print('c sample', sample) # rest = int(''.join(_ for _ in seg_id if _.isdigit())) rest = int(seg_id[:-1]) @@ -219,18 +188,6 @@ def pps(path_structure): if len(path_structure) == 6: # FIXME utter hack top, subject, sam_1, segment, modality, file = path_structure -<<<<<<< HEAD - p1 = sam_1, subject # child, parent to match db convention wasDerivedFrom - p2 = segment, sam_1 - return { - "parents": (p1, p2), - "subject": subject, - "sample": segment, - "modality": modality, - # note that because we do not convert to a single value we cannot include raw_anat_index in the qdb but that's ok - "raw_anat_index_v1": anat_index(segment), - } -======= elif len(path_structure) == 5: top, subject, sam_1, segment, file = path_structure modality = None # FIXME from metadata sample id @@ -238,7 +195,6 @@ def pps(path_structure): modality = 'microct' else: raise NotImplementedError(path_structure) ->>>>>>> master else: raise NotImplementedError(path_structure) @@ -273,19 +229,6 @@ def pps123(path_structure): def ext_pmeta(j, _pps=pps): out = {} -<<<<<<< HEAD - out["dataset"] = j["dataset_id"] - out["object"] = j["remote_id"] - out["file_id"] = ( - j["file_id"] if "file_id" in j else int(j["uri_api"].rsplit("/")[-1]) - ) # XXX old pathmeta schema that didn't include file id - ps = pathlib.Path(j["dataset_relative_path"]).parts - [p for p in ps if p.startswith("sub-") or p.startswith("sam-")] - out.update(pps(ps)) - return out - - -======= out['dataset'] = j['dataset_id'] out['object'] = j['remote_id'] out['file_id'] = ( @@ -301,52 +244,12 @@ def ext_pmeta123(j): return ext_pmeta(j, _pps=pps123) ->>>>>>> master class Queries: def __init__(self, session): self.session = session def address_from_fadd_type_fadd(self, fadd_type, fadd): # FIXME multi etc. -<<<<<<< HEAD - res = [ - i - for i, in self.session.execute( - sql_text("select * from address_from_fadd_type_fadd(:fadd_type, :fadd)"), - dict(fadd_type=fadd_type, fadd=fadd), - ) - ] - if res: - return res[0] - - def desc_inst_from_label(self, label): - # FIXME multi etc. - res = [ - i for i, in self.session.execute(sql_text("select * from desc_inst_from_label(:label)"), dict(label=label)) - ] - if res: - return res[0] - - def desc_quant_from_label(self, label): - # FIXME multi etc. - res = [ - i for i, in self.session.execute(sql_text("select * from desc_quant_from_label(:label)"), dict(label=label)) - ] - if res: - return res[0] - - def desc_cat_from_label_domain_label(self, label, domain_label): - # FIXME multi etc. - res = [ - i - for i, in self.session.execute( - sql_text("select * from desc_cat_from_label_domain_label(:label, :domain_label)"), - dict(label=label, domain_label=domain_label), - ) - ] - if res: - return res[0] -======= params = dict(fadd_type=fadd_type, fadd=fadd) res = [ i @@ -398,7 +301,6 @@ def desc_cat_from_label_domain_label(self, label, domain_label): raise ValueError(f'needed a result here {params}') else: return out ->>>>>>> master def cterm_from_label(self, label): # FIXME multi etc. @@ -411,12 +313,6 @@ def cterm_from_label(self, label): else: return out -<<<<<<< HEAD - def insts_from_dataset_ids(self, dataset, ids): - return list( - self.session.execute( - sql_text("select * from insts_from_dataset_ids(:dataset, :ids)"), dict(dataset=dataset, ids=ids) -======= def insts_from_dataset(self, dataset): return list(self.session.execute(sql_text('select * from insts_from_dataset(:dataset)'), dict(dataset=dataset))) @@ -424,7 +320,6 @@ def insts_from_dataset_ids(self, dataset, ids): return list( self.session.execute( sql_text('select * from insts_from_dataset_ids(:dataset, :ids)'), dict(dataset=dataset, ids=ids) ->>>>>>> master ) ) @@ -433,19 +328,6 @@ class InternalIds: def __init__(self, queries): q = queries self._q = queries -<<<<<<< HEAD - - self.addr_suid = q.address_from_fadd_type_fadd("tabular-header", "id_sub") - self.addr_said = q.address_from_fadd_type_fadd("tabular-header", "id_sam") - self.addr_spec = q.address_from_fadd_type_fadd("tabular-header", "species") - self.addr_saty = q.address_from_fadd_type_fadd("tabular-header", "sample_type") - - self.addr_tmod = q.address_from_fadd_type_fadd("tabular-header", "modality") - # addr_trai = address_from_fadd_type_fadd('tabular-header', 'raw_anat_index') - # addr_tnai = address_from_fadd_type_fadd('tabular-header', 'norm_anat_index') - # addr_context = address_from_fadd_type_fadd('context', '#/path-metadata/{index of match remote_id}/dataset_relative_path') # XXX this doesn't do what we want, I think what we really would want in these contexts are objects_internal that reference the file system state for a given updated snapshot, that is the real "object" that corresponds to the path-metadata.json that we are working from - -======= self.addr_suid = q.address_from_fadd_type_fadd('tabular-header', 'id_sub') self.addr_said = q.address_from_fadd_type_fadd('tabular-header', 'id_sam') @@ -472,40 +354,16 @@ def __init__(self, queries): # addr_tnai = address_from_fadd_type_fadd('tabular-header', 'norm_anat_index') # addr_context = address_from_fadd_type_fadd('context', '#/path-metadata/{index of match remote_id}/dataset_relative_path') # XXX this doesn't do what we want, I think what we really would want in these contexts are objects_internal that reference the file system state for a given updated snapshot, that is the real "object" that corresponds to the path-metadata.json that we are working from ->>>>>>> master # addr_jpmod = address_from_fadd_type_fadd('json-path-with-types', '#/#int/modality') # addr_jprai = address_from_fadd_type_fadd('json-path-with-types', '#/#int/anat_index') # addr_jpnai = address_from_fadd_type_fadd('json-path-with-types', '#/#int/norm_anat_index') self.addr_jpdrp = q.address_from_fadd_type_fadd( -<<<<<<< HEAD - "json-path-with-types", "#/path-metadata/data/#int/dataset_relative_path" -======= 'json-path-with-types', '#/path-metadata/data/#int/dataset_relative_path' ->>>>>>> master ) # XXX these are more accurate if opaque self.addr_jpmod = q.address_from_fadd_type_fadd( -<<<<<<< HEAD - "json-path-with-types", "#/path-metadata/data/#int/dataset_relative_path#derive-modality" - ) - # addr_jprai = address_from_fadd_type_fadd('json-path-with-types', '#/path-metadata/data/#int/dataset_relative_path#derive-raw-anat-index') - self.addr_jpnai = q.address_from_fadd_type_fadd( - "json-path-with-types", "#/path-metadata/data/#int/dataset_relative_path#derive-norm-anat-index-v1" - ) - self.addr_jpnain = q.address_from_fadd_type_fadd( - "json-path-with-types", "#/path-metadata/data/#int/dataset_relative_path#derive-norm-anat-index-v1-min" - ) - self.addr_jpnaix = q.address_from_fadd_type_fadd( - "json-path-with-types", "#/path-metadata/data/#int/dataset_relative_path#derive-norm-anat-index-v1-max" - ) - self.addr_jpsuid = q.address_from_fadd_type_fadd( - "json-path-with-types", "#/path-metadata/data/#int/dataset_relative_path#derive-subject-id" - ) - self.addr_jpsaid = q.address_from_fadd_type_fadd( - "json-path-with-types", "#/path-metadata/data/#int/dataset_relative_path#derive-sample-id" -======= 'json-path-with-types', '#/path-metadata/data/#int/dataset_relative_path#derive-modality' ) # addr_jprai = address_from_fadd_type_fadd('json-path-with-types', '#/path-metadata/data/#int/dataset_relative_path#derive-raw-anat-index') @@ -532,7 +390,6 @@ def __init__(self, queries): ) self.addr_jpsaid = q.address_from_fadd_type_fadd( 'json-path-with-types', '#/path-metadata/data/#int/dataset_relative_path#derive-sample-id' ->>>>>>> master ) self.addr_jpspec = q.address_from_fadd_type_fadd("json-path-with-types", "#/local/tom-made-it-up/species") @@ -547,25 +404,6 @@ def __init__(self, queries): self.addr_const_null = q.address_from_fadd_type_fadd("constant", None) -<<<<<<< HEAD - # qd_rai = desc_quant_from_label('reva ft sample anatomical location distance index raw') - self.qd_nai = q.desc_quant_from_label("reva ft sample anatomical location distance index normalized v1") - self.qd_nain = q.desc_quant_from_label("reva ft sample anatomical location distance index normalized v1 min") - self.qd_naix = q.desc_quant_from_label("reva ft sample anatomical location distance index normalized v1 max") - - self.cd_mod = q.desc_cat_from_label_domain_label("hasDataAboutItModality", None) - self.cd_bot = q.desc_cat_from_label_domain_label( - "bottom", None - ) # we just need something we can reference that points to null so we can have a refernce to all the objects - - self.id_human = q.desc_inst_from_label("human") - self.id_nerve = q.desc_inst_from_label("nerve") - self.id_nerve_volume = q.desc_inst_from_label("nerve-volume") - self.luid = { - "human": self.id_human, - "nerve": self.id_nerve, - "nerve-volume": self.id_nerve_volume, -======= self.qd_nvlai1 = q.desc_quant_from_label('vagus level anatomical location distance index normalized v1') self.qd_nvlain1 = q.desc_quant_from_label('vagus level anatomical location distance index normalized v1 min') self.qd_nvlaix1 = q.desc_quant_from_label('vagus level anatomical location distance index normalized v1 max') @@ -600,7 +438,6 @@ def __init__(self, queries): 'nerve-volume': self.id_nerve_volume, 'nerve-cross-section': self.id_nerve_cross_section, 'fascicle-cross-section': self.id_fascicle_cross_section, ->>>>>>> master } self.ct_mod = q.cterm_from_label("microct") # lol ct ct @@ -616,11 +453,7 @@ class Inserts: pass -<<<<<<< HEAD -def ingest(dataset_uuid, extract_fun, session, commit=False, dev=False, values_args=None): -======= def ingest(dataset_uuid, extract_fun, session, commit=False, dev=False, values_args=None, **kwargs): ->>>>>>> master """generic ingest workflow this_dataset_updated_uuid might not be needed in future, add a kwarg to control it maybe? @@ -643,11 +476,7 @@ def ingest(dataset_uuid, extract_fun, session, commit=False, dev=False, values_a make_values_cat, make_values_quant, ) = ( -<<<<<<< HEAD - extract_fun(dataset_uuid) if values_args is None else values_args -======= extract_fun(dataset_uuid, **kwargs) if values_args is None else values_args ->>>>>>> master ) q = Queries(session) @@ -657,23 +486,6 @@ def ingest(dataset_uuid, extract_fun, session, commit=False, dev=False, values_a values_instances = make_values_instances(i) res0 = session.execute( -<<<<<<< HEAD - sql_text("INSERT INTO objects (id, id_type) VALUES (:id, :id_type) ON CONFLICT DO NOTHING"), - dict(id=dataset_uuid, id_type="dataset"), - ) - - # oh dear https://stackoverflow.com/questions/34708509/how-to-use-returning-with-on-conflict-in-postgresql - res1 = session.execute( - sql_text( - "WITH ins AS (INSERT INTO objects_internal (type, dataset, updated_transitive, label) VALUES ('path-metadata', :dataset, :updated_transitive, :label) ON CONFLICT DO NOTHING RETURNING id) SELECT id FROM ins UNION ALL SELECT id FROM objects_internal WHERE type = 'path-metadata' AND dataset = :dataset AND updated_transitive = :updated_transitive" - ), # TODO see whether we actually need union all here or whether union by itself is sufficient - dict( - dataset=dataset_uuid, - updated_transitive=updated_transitive, - label=f"test-load-for-f001 {isoformat(updated_transitive)}", - ), - ) -======= sql_text('INSERT INTO objects (id, id_type) VALUES (:id, :id_type) ON CONFLICT DO NOTHING'), dict(id=dataset_uuid, id_type='dataset'), ) @@ -697,20 +509,11 @@ def ingest(dataset_uuid, extract_fun, session, commit=False, dev=False, values_a this_dataset_updated_uuid = [_ for _, in res1][0] else: this_dataset_updated_uuid = None ->>>>>>> master void = make_void(this_dataset_updated_uuid, i) vocd = make_vocd(this_dataset_updated_uuid, i) voqd = make_voqd(this_dataset_updated_uuid, i) -<<<<<<< HEAD - res1_1 = session.execute( - sql_text( - "INSERT INTO objects (id, id_type, id_internal) VALUES (:id, :id_type, :id) ON CONFLICT DO NOTHING" - ), # FIXME bad ocdn here - dict(id=this_dataset_updated_uuid, id_type="quantdb"), - ) -======= if updated_transitive: res1_1 = session.execute( sql_text( @@ -718,7 +521,6 @@ def ingest(dataset_uuid, extract_fun, session, commit=False, dev=False, values_a ), # FIXME bad ocdn here dict(id=this_dataset_updated_uuid, id_type='quantdb'), ) ->>>>>>> master vt, params = makeParamsValues(values_objects) session.execute(sql_text(f"INSERT INTO objects (id, id_type, id_file) VALUES {vt}{ocdn}"), params) @@ -728,11 +530,7 @@ def ingest(dataset_uuid, extract_fun, session, commit=False, dev=False, values_a vt, params = makeParamsValues(values_instances) session.execute( -<<<<<<< HEAD - sql_text(f"INSERT INTO values_inst (dataset, id_formal, type, desc_inst, id_sub, id_sam) VALUES {vt}{ocdn}"), -======= sql_text(f'INSERT INTO values_inst (dataset, id_formal, type, desc_inst, id_sub, id_sam) VALUES {vt}{ocdn}'), ->>>>>>> master params, ) @@ -751,38 +549,6 @@ def ingest(dataset_uuid, extract_fun, session, commit=False, dev=False, values_a vt, params = makeParamsValues(void) session.execute( -<<<<<<< HEAD - sql_text(f"INSERT INTO obj_desc_inst (object, desc_inst, addr_field, addr_desc_inst) VALUES {vt}{ocdn}"), params - ) - - vt, params = makeParamsValues(vocd) - session.execute(sql_text(f"INSERT INTO obj_desc_cat (object, desc_cat, addr_field) VALUES {vt}{ocdn}"), params) - - vt, params = makeParamsValues(voqd) - session.execute(sql_text(f"INSERT INTO obj_desc_quant (object, desc_quant, addr_field) VALUES {vt}{ocdn}"), params) - - vt, params = makeParamsValues(values_cv) - session.execute( - sql_text( - f"INSERT INTO values_cat (value_open, value_controlled, object, desc_inst, desc_cat, instance) VALUES {vt}{ocdn}" - ), - params, - ) - - vt, params, bindparams = makeParamsValues( - # FIXME LOL the types spec here is atrocious ... but it does work ... - # XXX and barring the unfortunate case, which we have now encountered where - # now fixed in the local impl - values_qv, - row_types=(None, None, None, None, None, JSONB), - ) - - t = sql_text( - f"INSERT INTO values_quant (value, object, desc_inst, desc_quant, instance, value_blob) VALUES {vt}{ocdn}" - ) - tin = t.bindparams(*bindparams) - session.execute(tin, params) -======= sql_text(f'INSERT INTO obj_desc_inst (object, desc_inst, addr_field, addr_desc_inst) VALUES {vt}{ocdn}'), params ) @@ -819,7 +585,6 @@ def ingest(dataset_uuid, extract_fun, session, commit=False, dev=False, values_a ) tin = t.bindparams(*bindparams) session.execute(tin, params) ->>>>>>> master if commit: session.commit() @@ -829,15 +594,9 @@ def extract_reva_ft(dataset_uuid, source_local=False, visualize=True): if source_local: with open( pathlib.Path( -<<<<<<< HEAD - f"~/.local/share/sparcur/export/datasets/{dataset_uuid}/LATEST/path-metadata.json" - ).expanduser(), - "rt", -======= f'~/.local/share/sparcur/export/datasets/{dataset_uuid}/LATEST/path-metadata.json' ).expanduser(), 'rt', ->>>>>>> master ) as f: blob = json.load(f) @@ -855,49 +614,29 @@ def extract_reva_ft(dataset_uuid, source_local=False, visualize=True): ir = fromJson(blob) -<<<<<<< HEAD - updated_transitive = max([i["timestamp_updated"] for i in ir["data"][1:]]) # 1: to skip the dataset object itself - - jpx = [r for r in ir["data"] if "mimetype" in r and r["mimetype"] == "image/jpx"] - - exts = [ext(j) for j in jpx] -======= updated_transitive = max([i['timestamp_updated'] for i in ir['data'][1:]]) # 1: to skip the dataset object itself jpx = [r for r in ir['data'] if 'mimetype' in r and r['mimetype'] == 'image/jpx'] exts = [ext_pmeta(j) for j in jpx] ->>>>>>> master # hrm = sorted(exts, key=lambda j: j['raw_anat_index']) # max_rai = max([e['raw_anat_index'] for e in exts]) # import math # log_max_rai = math.log10(max_rai) # normalize the index by mapping distinct values to the integers -<<<<<<< HEAD - nondist = sorted([e["raw_anat_index_v1"] for e in exts]) - lin_distinct = {v: i for i, v in enumerate(sorted(set([e["raw_anat_index_v1"] for e in exts])))} -======= nondist = sorted([e['raw_anat_index_v2'] for e in exts]) lin_distinct = {v: i for i, v in enumerate(sorted(set([e['raw_anat_index_v2'] for e in exts])))} ->>>>>>> master max_distinct = len(lin_distinct) mdp1 = max_distinct + 0.1 # to simplify adding overlap dd = defaultdict(list) for e in exts: # e['norm_anat_index'] = math.log10(e['raw_anat_index']) / log_max_rai -<<<<<<< HEAD - pos = lin_distinct[e["raw_anat_index_v1"]] - e["norm_anat_index_v1"] = (pos + 0.55) / mdp1 - e["norm_anat_index_v1_min"] = pos / mdp1 - e["norm_anat_index_v1_max"] = ( -======= pos = lin_distinct[e['raw_anat_index_v2']] e['norm_anat_index_v2'] = (pos + 0.55) / mdp1 e['norm_anat_index_v2_min'] = pos / mdp1 e['norm_anat_index_v2_max'] = ( ->>>>>>> master pos + 1.1 ) / mdp1 # ensure there is overlap between section for purposes of testing # TODO norm_anat_index_min @@ -905,15 +644,9 @@ def extract_reva_ft(dataset_uuid, source_local=False, visualize=True): dd[e["dataset"], e["sample"]].append(e) inst_obj_index = dict(dd) -<<<<<<< HEAD - max_nai = max([e["norm_anat_index_v1"] for e in exts]) - min_nain = min([e["norm_anat_index_v1_min"] for e in exts]) - max_naix = max([e["norm_anat_index_v1_max"] for e in exts]) -======= max_nai = max([e['norm_anat_index_v2'] for e in exts]) min_nain = min([e['norm_anat_index_v2_min'] for e in exts]) max_naix = max([e['norm_anat_index_v2_max'] for e in exts]) ->>>>>>> master if visualize: mexts = [] @@ -927,16 +660,10 @@ def extract_reva_ft(dataset_uuid, source_local=False, visualize=True): exts = mexts x = list(range(len(exts))) # ry = sorted([e['raw_anat_index'] for e in exts]) -<<<<<<< HEAD - ny = sorted([e["norm_anat_index_v1"] for e in exts]) - nyn = sorted([e["norm_anat_index_v1_min"] for e in exts]) - nyx = sorted([e["norm_anat_index_v1_max"] for e in exts]) -======= idy = [b for a, b in sorted([(e['norm_anat_index_v2'], e['sample']) for e in exts])] ny = sorted([e['norm_anat_index_v2'] for e in exts]) nyn = sorted([e['norm_anat_index_v2_min'] for e in exts]) nyx = sorted([e['norm_anat_index_v2_max'] for e in exts]) ->>>>>>> master nnx = list(zip(nyn, nyx)) import pylab as plt import seaborn @@ -960,42 +687,6 @@ def extract_reva_ft(dataset_uuid, source_local=False, visualize=True): plt.savefig(f'ft-norm-anat-index-v2-{_sid}.png') exts = _exts -<<<<<<< HEAD - datasets = {i.uuid: {"id_type": i.type} for e in exts if (i := e["dataset"])} - - packages = { - i.uuid: { - "id_type": i.type, - "id_file": e["file_id"], - } - for e in exts - if (i := e["object"]) - } - - objects = {**datasets, **packages} - dataset_object = list(set((d.uuid, o.uuid) for e in exts if (d := e["dataset"]) and (o := e["object"]))) - - subjects = { - k: { - "type": "subject", - "desc_inst": "human", - "id_sub": k[1], - } - for k in sorted(set((e["dataset"], e["subject"]) for e in exts)) - } - segments = { - k[:2]: { - "type": "sample", # FIXME vs below ??? - "desc_inst": "nerve-volume", # FIXME should this be nerve-segment and then we use nerve-volume for the 1:1 with files? - "id_sub": k[-1], - "id_sam": k[1], - } - for k in sorted(set((e["dataset"], e["sample"], e["subject"]) for e in exts)) - } - parents = sorted(set((e["dataset"],) + p for e in exts for p in e["parents"])) - sam_other = { - p[:2]: {"type": "sample", "desc_inst": "nerve", "id_sub": p[-1], "id_sam": p[1]} -======= datasets = {i.uuid: {'id_type': i.type} for e in exts if (i := e['dataset'])} packages = { @@ -1030,7 +721,6 @@ def extract_reva_ft(dataset_uuid, source_local=False, visualize=True): parents = sorted(set((e['dataset'],) + p for e in exts for p in e['parents'])) sam_other = { p[:2]: {'type': 'sample', 'desc_inst': 'nerve', 'id_sub': p[-1], 'id_sam': p[1]} ->>>>>>> master for p in parents if p[:2] not in segments } @@ -1049,17 +739,10 @@ def make_values_instances(i): ( d.uuid, f, -<<<<<<< HEAD - inst["type"], - i.luid[inst["desc_inst"]], - inst["id_sub"] if "id_sub" in inst else None, - inst["id_sam"] if "id_sam" in inst else None, -======= inst['type'], i.luid[inst['desc_inst']], inst['id_sub'] if 'id_sub' in inst else None, inst['id_sam'] if 'id_sam' in inst else None, ->>>>>>> master ) for (d, f), inst in instances.items() ] @@ -1096,11 +779,7 @@ def make_void(this_dataset_updated_uuid, i): None, ) # XXX FIXME this is the only way I can think to do this right now ? for o, b in objects.items() -<<<<<<< HEAD - if b["id_type"] == "package" -======= if b['id_type'] == 'package' ->>>>>>> master ] return void @@ -1111,15 +790,9 @@ def make_vocd(this_dataset_updated_uuid, i): # figuring out how to turn that around is going to take a bit of thinking (this_dataset_updated_uuid, i.cd_mod, i.addr_jpmod), ] + [ -<<<<<<< HEAD - (o, i.cd_bot, i.addr_const_null) # XXX FIXME this is the only way I can think to do this right now ? - for o, b in objects.items() - if b["id_type"] == "package" -======= (o, i.cd_obj, i.addr_const_null) # XXX FIXME this is the only way I can think to do this right now ? for o, b in objects.items() if b['id_type'] == 'package' ->>>>>>> master ] return vocd @@ -1148,32 +821,18 @@ def make_values_cat(this_dataset_updated_uuid, i, luinst): # e['object'].uuid, # FIXME still not right this comes from the updated latest i.id_nerve_volume, cd, # if we mess this up the fk ok obj_desc_cat will catch it :) -<<<<<<< HEAD - luinst[e["dataset"].uuid, e["sample"]], # get us the instance - ) - for e in exts - for k, cd in (("modality", i.cd_mod),) -======= luinst[e['dataset'].uuid, e['sample']], # get us the instance ) for e in exts for k, cd in (('modality', i.cd_mod),) ->>>>>>> master ] + [ ( None, i.ct_hack, -<<<<<<< HEAD - e["object"].uuid, - i.id_nerve_volume, - i.cd_bot, # if we mess this up the fk ok obj_desc_cat will catch it :) - luinst[e["dataset"].uuid, e["sample"]], # get us the instance -======= e['object'].uuid, i.id_nerve_volume, i.cd_obj, # if we mess this up the fk ok obj_desc_cat will catch it :) luinst[e['dataset'].uuid, e['sample']], # get us the instance ->>>>>>> master ) for e in exts ] @@ -1188,25 +847,15 @@ def make_values_quant(this_dataset_updated_uuid, i, luinst): this_dataset_updated_uuid, i.id_nerve_volume, qd, # if we mess this up the fk ok obj_desc_cat will catch it :) -<<<<<<< HEAD - luinst[e["dataset"].uuid, e["sample"]], # get us the instance -======= luinst[e['dataset'].uuid, e['sample']], # get us the instance ->>>>>>> master e[k], ) for e in exts for k, qd in ( # ('raw_anat_index', qd_rai), # XXX this is a bad place to store object -> field -> qd mappings also risks mismatch on address -<<<<<<< HEAD - ("norm_anat_index_v1", i.qd_nai), - ("norm_anat_index_v1_min", i.qd_nain), - ("norm_anat_index_v1_max", i.qd_naix), -======= ('norm_anat_index_v2', i.qd_nai), ('norm_anat_index_v2_min', i.qd_nain), ('norm_anat_index_v2_max', i.qd_naix), ->>>>>>> master ) ] return values_qv @@ -1223,10 +872,6 @@ def make_values_quant(this_dataset_updated_uuid, i, luinst): make_values_cat, make_values_quant, ) -<<<<<<< HEAD -======= - ->>>>>>> master # this is where things get annoying with needing selects on instance measured @@ -1696,13 +1341,8 @@ def ingest_reva_ft_all(session, source_local=False, do_insert=True, batch=False, def main(source_local=False, commit=False, echo=True): from quantdb.config import auth -<<<<<<< HEAD - dbkwargs = {k: auth.get(f"db-{k}") for k in ("user", "host", "port", "database")} # TODO integrate with cli options - dbkwargs["dbuser"] = dbkwargs.pop("user") -======= dbkwargs = {k: auth.get(f'db-{k}') for k in ('user', 'host', 'port', 'database')} # TODO integrate with cli options dbkwargs['dbuser'] = dbkwargs.pop('user') ->>>>>>> master engine = create_engine(dbUri(**dbkwargs)) engine.echo = echo session = Session(engine) From 81487b7e936071a4df10589f59789a41fdc818fa Mon Sep 17 00:00:00 2001 From: Troy Sincomb Date: Mon, 28 Apr 2025 14:31:41 -0700 Subject: [PATCH 12/18] added fastapi postgresql folder --- quantdb/postgresql_app/__init__.py | 5 +++++ quantdb/postgresql_app/database.py | 17 +++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 quantdb/postgresql_app/__init__.py create mode 100644 quantdb/postgresql_app/database.py diff --git a/quantdb/postgresql_app/__init__.py b/quantdb/postgresql_app/__init__.py new file mode 100644 index 0000000..e73dd2a --- /dev/null +++ b/quantdb/postgresql_app/__init__.py @@ -0,0 +1,5 @@ +from __future__ import annotations + +from . import database + +__all__ = ['database'] diff --git a/quantdb/postgresql_app/database.py b/quantdb/postgresql_app/database.py new file mode 100644 index 0000000..411d706 --- /dev/null +++ b/quantdb/postgresql_app/database.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from typing import Any + +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker + +from quantdb.config import Settings + +# TODO: get new setting env setup first +# SQLALCHEMY_DATABASE_URL = Settings().MYSQL_URL + +# engine = create_engine(SQLALCHEMY_DATABASE_URL) +# SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +# Base: Any = declarative_base() From 542f773ea21fc159bd05c886d3dd50a5c8e488a4 Mon Sep 17 00:00:00 2001 From: Troy Sincomb Date: Mon, 28 Apr 2025 14:40:35 -0700 Subject: [PATCH 13/18] remove ingest from blue ignore and found internal bug on why api was formating --- .pre-commit-config.yaml | 1 - quantdb/api.py | 602 +++++++++++++++++----------------- quantdb/ingest.py | 66 ++-- quantdb/mysql_app/__init__.py | 5 - quantdb/mysql_app/database.py | 17 - 5 files changed, 330 insertions(+), 361 deletions(-) delete mode 100644 quantdb/mysql_app/__init__.py delete mode 100644 quantdb/mysql_app/database.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6ce8cf5..c1fdb6e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,6 +21,5 @@ repos: exclude: | (?x)^( quantdb/api.py| - quantdb/ingest.py| sql;/.* )$ diff --git a/quantdb/api.py b/quantdb/api.py index b66bd3d..cec693f 100644 --- a/quantdb/api.py +++ b/quantdb/api.py @@ -28,6 +28,7 @@ def default(self, obj): url_sql_where = ( # TODO arity spec here + # dupes overwrite params but that is ok, this way we get the correct table alias for both cases ('object', 'object', 'cv.object = any(:object)', 'cat'), # XXX should not use this outside values/ unless we left outer due to intersect ? ('object', 'object', 'qv.object = any(:object)', 'quant'), # XXX should not use this outside values/ unless we left outer due to intersect ? @@ -63,7 +64,7 @@ def get_where(kwargs): for u, s, w, t in url_sql_where: if u in kwargs and kwargs[u]: params[s] = kwargs[u] - if t == "cat": + if t == 'cat': _where_cat.append(w) elif t == 'quant': # do not include value-quant if value-quant-margin is provided @@ -77,23 +78,23 @@ def get_where(kwargs): _where_cat.append(w) _where_quant.append(w) else: - raise ValueError("wat") + raise ValueError('wat') - where_cat = " AND ".join(_where_cat) - where_quant = " AND ".join(_where_quant) - log.log(9, f"\nwhere-quant\n{where_quant}\nwhere-quant") + where_cat = ' AND '.join(_where_cat) + where_quant = ' AND '.join(_where_quant) + log.log(9, f'\nwhere-quant\n{where_quant}\nwhere-quant') return where_cat, where_quant, params def main_query(endpoint, kwargs): ep_select = { #'instances': 'im.dataset, im.id_formal, im.id_sam, im.id_sub, id.label', - "values/inst": ( - "im.dataset, " - "im.id_formal AS inst, " - "im.id_sam AS sample, " - "im.id_sub AS subject, " - "id.label AS desc_inst" + 'values/inst': ( + 'im.dataset, ' + 'im.id_formal AS inst, ' + 'im.id_sam AS sample, ' + 'im.id_sub AS subject, ' + 'id.label AS desc_inst' ), 'objects': ( # TODO probably some path metadata file type, etc. too 'im.dataset, ' @@ -102,86 +103,97 @@ def main_query(endpoint, kwargs): 'o.id_file, ' # beware that there might be more than one id_file if a package is multi-file, but we usually ban those 'oi.updated_transitive' ), - "values/cat": ( - "im.dataset, " - "im.id_formal AS inst, " - "id.label AS desc_inst, " - "cdid.label AS domain, " - "cd.range, " - "cd.label AS desc_cat, " - "cv.value_open, " - "ct.label AS value_controlled" # TODO and where did it come from TODO iri + 'values/cat': ( + 'im.dataset, ' + 'im.id_formal AS inst, ' + 'id.label AS desc_inst, ' + 'cdid.label AS domain, ' + 'cd.range, ' + 'cd.label AS desc_cat, ' + 'cv.value_open, ' + 'ct.label AS value_controlled' # TODO and where did it come from TODO iri ), # TODO will want/need to return the shape of the value for these as well since that will be needed to correctly interpret the contents of the value field in the future - "values/quant": ( - "im.dataset, " - "im.id_formal AS inst, " - "id.label AS desc_inst, " - "qd.aggregation_type AS agg_type, " - "a.label AS aspect, " - "u.label AS unit, qv.value" # TODO and where did it come from + 'values/quant': ( + 'im.dataset, ' + 'im.id_formal AS inst, ' + 'id.label AS desc_inst, ' + 'qd.aggregation_type AS agg_type, ' + 'a.label AS aspect, ' + 'u.label AS unit, qv.value' # TODO and where did it come from ), - "values/cat-quant": ( + 'values/cat-quant': ( ( "'value-cat' AS type, " - "im.dataset, " - "im.id_formal AS inst, " - "id.label AS desc_inst, " - "cdid.label AS domain, " - "cd.range, " - "NULL::quant_agg_type as agg_type, " # have to annoate the nulls because distinct causes type inference to fail ??? - "cd.label AS pred_or_asp, " - "cv.value_open AS vo_or_unit, " - "ct.label AS value_controlled, " - "NULL::numeric AS value" - ), - ( + 'im.dataset, ' + 'im.id_formal AS inst, ' + 'id.label AS desc_inst, ' + 'cdid.label AS domain, ' + 'cd.range, ' + 'NULL::quant_agg_type as agg_type, ' # have to annoate the nulls because distinct causes type inference to fail ??? + 'cd.label AS pred_or_asp, ' + 'cv.value_open AS vo_or_unit, ' + 'ct.label AS value_controlled, ' + 'NULL::numeric AS value') + , ( "'value-quant' AS type, im.dataset, " - "im.id_formal AS inst, id.label AS desc_inst, " - "NULL AS domain, " - "NULL::cat_range_type AS range, " - "qd.aggregation_type AS agg_type, " - "a.label AS aspect, " - "u.label AS unit, " - "NULL AS vc, qv.value" - ), + 'im.id_formal AS inst, id.label AS desc_inst, ' + 'NULL AS domain, ' + 'NULL::cat_range_type AS range, ' + 'qd.aggregation_type AS agg_type, ' + 'a.label AS aspect, ' + 'u.label AS unit, ' + 'NULL AS vc, qv.value' + )), + 'desc/inst': ( + 'id.iri, ' + 'id.label, ' + 'idpar.label as subclassof ' ), - "desc/inst": ("id.iri, " "id.label, " "idpar.label as subclassof "), - "desc/cat": ("cd.label, " "cdid.label AS domain, " "cd.range, " "cd.description "), - "desc/quant": ( - "qd.label, " - "id.label AS domain, " - "qd.shape, " - "qd.aggregation_type as agg_type, " - "a.label AS aspect, " - "u.label AS unit, " - "qd.description " + 'desc/cat': ( + 'cd.label, ' + 'cdid.label AS domain, ' + 'cd.range, ' + 'cd.description ' + ), + 'desc/quant': ( + 'qd.label, ' + 'id.label AS domain, ' + 'qd.shape, ' + 'qd.aggregation_type as agg_type, ' + 'a.label AS aspect, ' + 'u.label AS unit, ' + 'qd.description ' + ), + 'terms': ( + 'ct.iri, ' + 'ct.label ' + ), + 'units': ( + 'u.iri, ' + 'u.label ' + ), + 'aspects': ( + 'a.iri, ' + 'a.label, ' + 'aspar.label as subclassof ' ), - "terms": ("ct.iri, " "ct.label "), - "units": ("u.iri, " "u.label "), - "aspects": ("a.iri, " "a.label, " "aspar.label as subclassof "), }[endpoint] # FIXME move extra and select out and pass then in in as arguments ? or retain control here? - def gkw(k): - return k in kwargs and kwargs[k] + def gkw(k): return k in kwargs and kwargs[k] class sn: # select needs - objects = endpoint == "objects" - desc_inst = endpoint not in ( - "objects", - "terms", - "units", - "aspects", - ) - desc_cat = endpoint in ("values/cat", "values/cat-quant", "desc/cat") - value_cat = endpoint in ("values/cat", "values/cat-quant", "terms") - aspect = endpoint in ("values/quant", "values/cat-quant", "desc/quant", "aspects") - unit = endpoint in ("values/quant", "values/cat-quant", "desc/quant", "units") - agg_type = endpoint in ("values/quant", "values/cat-quant") - desc_quant = aspect or unit or agg_type - parent_aspect = endpoint == "aspects" - parent_desc_inst = endpoint == "desc/inst" + objects = endpoint == 'objects' + desc_inst = endpoint not in ('objects', 'terms', 'units', 'aspects',) + desc_cat = endpoint in ('values/cat', 'values/cat-quant', 'desc/cat') + value_cat = endpoint in ('values/cat', 'values/cat-quant', 'terms') + aspect = endpoint in ('values/quant', 'values/cat-quant', 'desc/quant', 'aspects') + unit = endpoint in ('values/quant', 'values/cat-quant', 'desc/quant', 'units') + agg_type = endpoint in ('values/quant', 'values/cat-quant') + desc_quant = (aspect or unit or agg_type) + parent_aspect = endpoint == 'aspects' + parent_desc_inst = endpoint == 'desc/inst' class kw: # keywords prov = gkw('prov') @@ -233,8 +245,7 @@ class kw: # keywords adc.addr_type as prov_value_addr_type, adc.addr_field as prov_value_addr_field, adc.value_type as prov_value_type -""" + ( - """, +""" + (""", NULL::address_type as prov_unit_addr_type, NULL as prov_unit_addr_field, NULL::field_value_type as prov_unit_type, @@ -242,10 +253,7 @@ class kw: # keywords NULL::address_type as prov_aspect_addr_type, NULL as prov_aspect_addr_field, NULL::field_value_type as prov_aspect_type -""" - if sn.unit or endpoint == "values/inst" - else "" - ) +""" if sn.unit or endpoint == 'values/inst' else '') s_prov_q = """ adq.addr_type as prov_value_addr_type, @@ -279,26 +287,18 @@ class kw: # keywords LEFT OUTER JOIN addresses AS ada ON ada.id = odq.addr_aspect """ - maybe_distinct = ( - "DISTINCT " - if ( - endpoint.startswith("desc/") - or endpoint in ("terms", "units", "aspects") - or (sn.objects or kw.prov) - and not kw.source_only - ) - else "" - ) + maybe_distinct = 'DISTINCT ' if ( + endpoint.startswith('desc/') or + endpoint in ('terms', 'units', 'aspects') or + (sn.objects or kw.prov) and not kw.source_only) else '' ep_select_cat, ep_select_quant = ep_select if isinstance(ep_select, tuple) else (ep_select, ep_select) - select_cat = f"SELECT {maybe_distinct}{ep_select_cat}" + ( - (s_prov_objs + s_prov_i + ((",\n" + s_prov_c) if endpoint != "values/inst" else "")) if kw.prov else "" - ) - select_quant = f"SELECT {maybe_distinct}{ep_select_quant}" + ( - (s_prov_objs + s_prov_i + ((",\n" + s_prov_q) if endpoint != "values/inst" else "")) if kw.prov else "" - ) + select_cat = f'SELECT {maybe_distinct}{ep_select_cat}' + ( + (s_prov_objs + s_prov_i + ((',\n' + s_prov_c) if endpoint != 'values/inst' else '')) if kw.prov else '') + select_quant = f'SELECT {maybe_distinct}{ep_select_quant}' + ( + (s_prov_objs + s_prov_i + ((',\n' + s_prov_q) if endpoint != 'values/inst' else '')) if kw.prov else '') _where_cat, _where_quant, params = get_where(kwargs) - where_cat = f"WHERE {_where_cat}" if _where_cat else "" - where_quant = f"WHERE {_where_quant}" if _where_quant else "" + where_cat = f'WHERE {_where_cat}' if _where_cat else '' + where_quant = f'WHERE {_where_quant}' if _where_quant else '' q_inst_parent = '\n'.join(( 'JOIN values_inst AS icin', @@ -371,66 +371,55 @@ class kw: # keywords (q_prov_i + q_prov_q) if kw.prov else '', )) - sw_cat = f"{select_cat}\n{q_cat}\n{where_cat}" # XXX yes this can be malformed in some cases - sw_quant = f"{select_quant}\n{q_quant}\n{where_quant}" # XXX yes this can be malformed in some cases - if endpoint in ("values/cat", "terms", "desc/cat"): + sw_cat = f'{select_cat}\n{q_cat}\n{where_cat}' # XXX yes this can be malformed in some cases + sw_quant = f'{select_quant}\n{q_quant}\n{where_quant}' # XXX yes this can be malformed in some cases + if endpoint in ('values/cat', 'terms', 'desc/cat'): query = sw_cat - elif endpoint in ( - "values/quant", - "units", - "aspects", - "desc/quant", - ): # FIXME TODO make it possible to cross query terms, units, aspects + elif endpoint in ('values/quant', 'units', 'aspects', 'desc/quant'): # FIXME TODO make it possible to cross query terms, units, aspects query = sw_quant else: - operator = "UNION" if "union-cat-quant" in kwargs and kwargs["union-cat-quant"] else "INTERSECT" - query = f"{sw_cat}\n{operator}\n{sw_quant}" + operator = 'UNION' if 'union-cat-quant' in kwargs and kwargs['union-cat-quant'] else 'INTERSECT' + query = f'{sw_cat}\n{operator}\n{sw_quant}' - log.log(9, "\n" + query) + log.log(9, '\n' + query) return query, params def to_json(record_type, res, prov=False): rows = list(res) if rows: - if record_type == "object": - result = [ - ( - {k: v for k, v in r._asdict().items() if k != "id"} - # do not leak internal ids because the might change and are not meaningful - if r.id_type == "quantdb" - else {k: v for k, v in r._asdict().items() if k != "updated_transitive"} - ) - for r in rows - ] - elif record_type is None and "type" in rows[0]._fields: - rem_cat = "value", "agg_type" - + if record_type == 'object': + result = [{k: v for k, v in r._asdict().items() if k != 'id'} + # do not leak internal ids because the might change and are not meaningful + if r.id_type == 'quantdb' else + {k: v for k, v in r._asdict().items() if k != 'updated_transitive'} + for r in rows] + elif record_type is None and 'type' in rows[0]._fields: + rem_cat = 'value', 'agg_type' def type_fields_cat(k): - if k == "pred_or_asp": - return "desc_cat" - elif k == "vo_or_unit": - return "value_open" + if k == 'pred_or_asp': + return 'desc_cat' + elif k == 'vo_or_unit': + return 'value_open' else: return k - rem_quant = "domain", "range", "value_controlled" - + rem_quant = 'domain', 'range', 'value_controlled' def type_fields_quant(k): - if k == "pred_or_asp": - return "aspect" - elif k == "vo_or_unit": - return "unit" + if k == 'pred_or_asp': + return 'aspect' + elif k == 'vo_or_unit': + return 'unit' else: return k def prow(r): - if r.type == "value-cat": + if r.type == 'value-cat': rem, type_fields = rem_cat, type_fields_cat - elif r.type == "value-quant": + elif r.type == 'value-quant': rem, type_fields = rem_quant, type_fields_quant else: - raise NotImplementedError(f"wat {r.type}") + raise NotImplementedError(f'wat {r.type}') return {type_fields(k): v for k, v in r._asdict().items() if k not in rem} @@ -440,40 +429,35 @@ def prow(r): for r in result: if record_type is not None: - r["type"] = record_type + r['type'] = record_type - for cull_none in ("subclassof",): + for cull_none in ('subclassof',): if cull_none in r and r[cull_none] is None: r.pop(cull_none) if prov: - def pop_prefix(d, prefix): - usc = prefix.count("_") - return { - k.split("_", 1 + usc)[-1]: v - for k in list(d) - if k.startswith(prefix + "_") and (v := d.pop(k)) is not None - } + usc = prefix.count('_') + return {k.split('_', 1 + usc)[-1]:v for k in list(d) if k.startswith(prefix + '_') and (v := d.pop(k)) is not None} for r in result: - provs = pop_prefix(r, "prov") - if "source_id_type" in provs and provs["source_id_type"] == "quantdb": - provs.pop("source_id", None) # don't leak internal ids + provs = pop_prefix(r, 'prov') + if 'source_id_type' in provs and provs['source_id_type'] == 'quantdb': + provs.pop('source_id', None) # don't leak internal ids else: - provs.pop("source_updated_transitive", None) # always None in this case + provs.pop('source_updated_transitive', None) # always None in this case - for prefix in ("desc_inst", "inst", "value", "value", "source"): + for prefix in ('desc_inst', 'inst', 'value', 'value', 'source'): d = pop_prefix(provs, prefix) if d: - d["type"] = "address" if prefix != "source" else "object" + d['type'] = 'address' if prefix != 'source' else 'object' provs[prefix] = d - provs["type"] = "prov" - r["prov"] = provs + provs['type'] = 'prov' + r['prov'] = provs out = result - # breakpoint() + #breakpoint() else: out = [] @@ -488,20 +472,22 @@ def wrap_out(endpoint, kwargs, out): parameters = {k: v for k, v in kwargs.items() if v} n_records = len(out) blob = { - "type": "quantdb-query-result", - "endpoint": endpoint, - "parameters": parameters, - "records": n_records, - "result": out, + 'type': 'quantdb-query-result', + 'endpoint': endpoint, + 'parameters': parameters, + 'records': n_records, + 'result': out, } return blob args_default = { - "object": [], - "updated-transitive": None, # TODO needed to query for some internal + 'object': [], + 'updated-transitive': None, # TODO needed to query for some internal + ## inst - "desc-inst": [], # aka class + 'desc-inst': [], # aka class + # value-inst 'dataset': None, 'inst': [], @@ -511,30 +497,36 @@ def wrap_out(endpoint, kwargs, out): 'include-equivalent': False, ## cat - "desc-cat": [], # aka predicate - "value-cat": [], - "value-cat-open": [], + 'desc-cat': [], # aka predicate + + 'value-cat': [], + 'value-cat-open': [], + ## quant # desc-quant - "unit": [], - "aspect": [], - "agg-type": None, + 'unit': [], + 'aspect': [], + 'agg-type': None, # TODO shape - "value-quant": None, - "value-quant-margin": None, - "value-quant-min": None, - "value-quant-max": None, - "limit": 100, + + 'value-quant': None, + 'value-quant-margin': None, + 'value-quant-min': None, + 'value-quant-max': None, + + 'limit': 100, #'operator': 'INTERSECT', # XXX ... - "union-cat-quant": False, # by default we intersect but sometimes you want the union instead e.g. if object is passed - "source-only": False, - "include-unused": False, - "prov": False, + 'union-cat-quant': False, # by default we intersect but sometimes you want the union instead e.g. if object is passed + 'source-only': False, + 'include-unused': False, + 'prov': False, + #'cat-value': [], #'class': [], #'predicate': None, #'object': None, #'filter': [], + #'quant-value': None, #'quant-margin': None, #'quant-min': None, @@ -546,33 +538,33 @@ def getArgs(request, endpoint, dev=False): default = copy.deepcopy(args_default) if dev: - default["return-query"] = False + default['return-query'] = False # modify defaults by endpoint - if endpoint != "objects": - default.pop("source-only") + if endpoint != 'objects': + default.pop('source-only') - if not (endpoint.startswith("desc/") or endpoint in ("terms", "units", "aspects")): - default.pop("include-unused") + if not (endpoint.startswith('desc/') or endpoint in ('terms', 'units', 'aspects')): + default.pop('include-unused') else: # prevent filtering on the thing we are trying to query - if endpoint == "terms": - default.pop("value-cat") - elif endpoint == "units": - default.pop("unit") - elif endpoint == "aspects": - default.pop("aspect") - elif endpoint == "desc/inst": - default.pop("desc-inst") - elif endpoint == "desc/cat": - default.pop("desc-cat") - - if not endpoint.startswith("values/"): - default.pop("prov") - elif endpoint == "values/cat": - [default.pop(k) for k in list(default) if k.startswith("value-quant") or k in ("unit", "aspect", "agg-type")] - elif endpoint == "values/quant": - [default.pop(k) for k in list(default) if k in ("desc-cat", "value-cat", "value-cat-open")] + if endpoint == 'terms': + default.pop('value-cat') + elif endpoint == 'units': + default.pop('unit') + elif endpoint == 'aspects': + default.pop('aspect') + elif endpoint == 'desc/inst': + default.pop('desc-inst') + elif endpoint == 'desc/cat': + default.pop('desc-cat') + + if not endpoint.startswith('values/'): + default.pop('prov') + elif endpoint == 'values/cat': + [default.pop(k) for k in list(default) if k.startswith('value-quant') or k in ('unit', 'aspect', 'agg-type')] + elif endpoint == 'values/quant': + [default.pop(k) for k in list(default) if k in ('desc-cat', 'value-cat', 'value-cat-open')] if (endpoint == 'values/inst') or (endpoint == 'objects'): # prevent getting no results if only cat or quant @@ -584,8 +576,8 @@ def getArgs(request, endpoint, dev=False): extras = set(request.args) - set(default) if extras: # FIXME raise this as a 401, TODO need error types for this - nl = "\n" - raise exc.UnknownArg(f"unknown args: {nl.join(extras)}") + nl = '\n' + raise exc.UnknownArg(f'unknown args: {nl.join(extras)}') def convert(k, d): if k in request.args: @@ -620,14 +612,14 @@ def convert(k, d): else: return d - if k in ("include-equivalent", "union-cat-quant", "include-unused"): - if v.lower() == "true": + if k in ('include-equivalent', 'union-cat-quant', 'include-unused'): + if v.lower() == 'true': return True - elif v.lower() == "false": + elif v.lower() == 'false': return False else: raise TypeError(f'Expected a bool, got "{v}" instead.') - elif k.startswith("value-quant") or k in ("limit",): + elif k.startswith('value-quant') or k in ('limit',): try: return float(v) except ValueError as e: @@ -635,20 +627,21 @@ def convert(k, d): else: return v - out = {k: convert(k, v) for k, v in default.items()} + out = {k:convert(k, v) for k, v in default.items()} return out -def make_app(db=None, name="quantdb-api-server", dev=False): +def make_app(db=None, name='quantdb-api-server', dev=False): app = Flask(name) - kwargs = {k: auth.get(f"db-{k}") for k in ("user", "host", "port", "database")} # TODO integrate with cli options - kwargs["dbuser"] = kwargs.pop("user") - app.config["SQLALCHEMY_DATABASE_URI"] = dbUri(**kwargs) # use os.environ.update - app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False + kwargs = {k:auth.get(f'db-{k}') # TODO integrate with cli options + for k in ('user', 'host', 'port', 'database')} + kwargs['dbuser'] = kwargs.pop('user') + app.config['SQLALCHEMY_DATABASE_URI'] = dbUri(**kwargs) # use os.environ.update + app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False db.init_app(app) session = db.session - bp = "/api/1/" + bp = '/api/1/' def default_flow(endpoint, record_type, query_fun, json_fun, alt_query_fun=None): try: @@ -659,10 +652,9 @@ def default_flow(endpoint, record_type, query_fun, json_fun, alt_query_fun=None) breakpoint() raise e - def gkw(k): - return k in kwargs and kwargs[k] + def gkw(k): return k in kwargs and kwargs[k] - if gkw("include-unused"): + if gkw('include-unused'): query_fun = alt_query_fun # FIXME record_type is actually determined entirely in query_fun right now @@ -708,137 +700,137 @@ def gkw(k): raise e try: - out = json_fun(record_type, res, prov=("prov" in kwargs and kwargs["prov"])) - resp = json.dumps(wrap_out(endpoint, kwargs, out), cls=JEncode), 200, {"Content-Type": "application/json"} + out = json_fun(record_type, res, prov=('prov' in kwargs and kwargs['prov'])) + resp = json.dumps(wrap_out(endpoint, kwargs, out), cls=JEncode), 200, {'Content-Type': 'application/json'} except Exception as e: breakpoint() raise e return resp - @app.route(f"{bp}/test") - def route_test_check(): - "objects with derived values that match all criteria" - return "testing-api" - - @app.route(f"{bp}/objects") + @app.route(f'{bp}/objects') def route_1_objects(): "objects with derived values that match all criteria" - return default_flow("objects", "object", main_query, to_json) + return default_flow('objects', 'object', main_query, to_json) - @app.route(f"{bp}/desc/inst") - @app.route(f"{bp}/descriptors/inst") - @app.route(f"{bp}/classes") + @app.route(f'{bp}/desc/inst') + @app.route(f'{bp}/descriptors/inst') + @app.route(f'{bp}/classes') def route_1_desc_inst(): def query(endpoint, kwargs): - return ( - ( - "SELECT " - "id.iri, " - "id.label, " - "idpar.label as subclassof" + return ('SELECT ' + + 'id.iri, ' + 'id.label, ' + 'idpar.label as subclassof' + """ FROM descriptors_inst AS id LEFT OUTER JOIN class_parent AS clp ON clp.id = id.id LEFT OUTER JOIN descriptors_inst AS idpar ON idpar.id = clp.parent """), {} - return default_flow("desc/inst", "desc-inst", main_query, to_json, alt_query_fun=query) + return default_flow('desc/inst', 'desc-inst', main_query, to_json, alt_query_fun=query) - @app.route(f"{bp}/desc/cat") - @app.route(f"{bp}/descriptors/cat") - @app.route(f"{bp}/predicates") + @app.route(f'{bp}/desc/cat') + @app.route(f'{bp}/descriptors/cat') + @app.route(f'{bp}/predicates') def route_1_desc_cat(): def query(endpoint, kwargs): - return ( - "select " - "cd.label, " - "cdid.label AS domain, " - "cd.range, " - "cd.description " - "from descriptors_cat as cd " - "left outer join descriptors_inst as cdid on cdid.id = cd.domain" - ), {} - - return default_flow( - "desc/cat", "desc-cat", main_query, to_json, alt_query_fun=query - ) # TODO likely need different args e.g. to filter by desc_inst - - @app.route(f"{bp}/desc/quant") - @app.route(f"{bp}/descriptors/quant") + return ('select ' + + 'cd.label, ' + 'cdid.label AS domain, ' + 'cd.range, ' + 'cd.description ' + + 'from descriptors_cat as cd ' + 'left outer join descriptors_inst as cdid on cdid.id = cd.domain' + ), {} + + return default_flow('desc/cat', 'desc-cat', main_query, to_json, alt_query_fun=query) # TODO likely need different args e.g. to filter by desc_inst + + @app.route(f'{bp}/desc/quant') + @app.route(f'{bp}/descriptors/quant') def route_1_desc_quant(): def query(endpoint, kwargs): - return ( - "select " - "qd.label, " - "id.label AS domain, " - "qd.shape, " - "qd.aggregation_type as agg_type, " - "a.label AS aspect, " - "u.label AS unit, " - "qd.description " - "from descriptors_quant as qd " - "left outer join descriptors_inst as id on id.id = qd.domain " - "left outer join units as u on u.id = qd.unit " - "join aspects as a on a.id = qd.aspect" - ), {} - - return default_flow( - "desc/quant", "desc-quant", main_query, to_json, alt_query_fun=query - ) # TODO likely need different args e.g. to filter by desc_inst - - @app.route(f"{bp}/values/inst") - @app.route(f"{bp}/instances") + return ('select ' + + 'qd.label, ' + 'id.label AS domain, ' + 'qd.shape, ' + 'qd.aggregation_type as agg_type, ' + 'a.label AS aspect, ' + 'u.label AS unit, ' + 'qd.description ' + + 'from descriptors_quant as qd ' + 'left outer join descriptors_inst as id on id.id = qd.domain ' + 'left outer join units as u on u.id = qd.unit ' + 'join aspects as a on a.id = qd.aspect' + ), {} + + return default_flow('desc/quant', 'desc-quant', main_query, to_json, alt_query_fun=query) # TODO likely need different args e.g. to filter by desc_inst + + @app.route(f'{bp}/values/inst') + @app.route(f'{bp}/instances') def route_1_val_inst(): "instances associated with values that match all critiera" - return default_flow("values/inst", "instance", main_query, to_json) + return default_flow('values/inst', 'instance', main_query, to_json) - @app.route(f"{bp}/values") - @app.route(f"{bp}/values/cat-quant") + @app.route(f'{bp}/values') + @app.route(f'{bp}/values/cat-quant') def route_1_val_cat_quant(): - return default_flow("values/cat-quant", None, main_query, to_json) + return default_flow('values/cat-quant', None, main_query, to_json) - @app.route(f"{bp}/values/cat") + @app.route(f'{bp}/values/cat') def route_1_val_cat(): - return default_flow("values/cat", "value-cat", main_query, to_json) + return default_flow('values/cat', 'value-cat', main_query, to_json) - @app.route(f"{bp}/values/quant") + @app.route(f'{bp}/values/quant') def route_1_val_quant(): - return default_flow("values/quant", "value-quant", main_query, to_json) + return default_flow('values/quant', 'value-quant', main_query, to_json) - @app.route(f"{bp}/terms") - @app.route(f"{bp}/controlled-terms") + @app.route(f'{bp}/terms') + @app.route(f'{bp}/controlled-terms') def route_1_cterms(): def query(endpoint, kwargs): - return ("select " "ct.iri, " "ct.label " "from controlled_terms as ct"), {} + return ('select ' + + 'ct.iri, ' + 'ct.label ' + + 'from controlled_terms as ct'), {} - return default_flow("terms", "term", main_query, to_json, alt_query_fun=query) + return default_flow('terms', 'term', main_query, to_json, alt_query_fun=query) - @app.route(f"{bp}/units") + @app.route(f'{bp}/units') def route_1_units(): def query(endpoint, kwargs): - return ("select " "u.iri, " "u.label " "from units as u"), {} + return ('select ' + + 'u.iri, ' + 'u.label ' + + 'from units as u'), {} - return default_flow("units", "unit", main_query, to_json, alt_query_fun=query) + return default_flow('units', 'unit', main_query, to_json, alt_query_fun=query) - @app.route(f"{bp}/aspects") + @app.route(f'{bp}/aspects') def route_1_aspects(): def query(endpoint, kwargs): - return ( - ( - "SELECT " - "a.iri, " - "a.label, " - "aspar.label AS subclassof " + return ('SELECT ' + + 'a.iri, ' + 'a.label, ' + 'aspar.label AS subclassof ' + """ FROM aspects AS a LEFT OUTER JOIN aspect_parent AS ap ON ap.id = a.id LEFT OUTER JOIN aspects AS aspar ON aspar.id = ap.parent -""" - ), - {}, - ) +"""), {} - return default_flow("aspects", "aspect", main_query, to_json, alt_query_fun=query) + return default_flow('aspects', 'aspect', main_query, to_json, alt_query_fun=query) return app diff --git a/quantdb/ingest.py b/quantdb/ingest.py index b3f514d..85c37a1 100644 --- a/quantdb/ingest.py +++ b/quantdb/ingest.py @@ -24,7 +24,7 @@ log = log.getChild('ingest') try: - if get_ipython().__class__.__name__ == "ZMQInteractiveShell": + if get_ipython().__class__.__name__ == 'ZMQInteractiveShell': import sys # FIXME hack that should be in jupyter-repl env or something sys.breakpointhook = lambda: None @@ -66,7 +66,7 @@ def __call__(self, value, type=None): return self.value_to_name[value] else: self.counter += 1 - name = "v" + str(self.counter) + name = 'v' + str(self.counter) if type is None: self.value_to_name[value] = name @@ -80,8 +80,8 @@ def __call__(self, value, type=None): def makeParamsValues(*value_sets, constants=tuple(), types=tuple(), row_types=tuple()): # TODO variable sized records and # common value names - if constants and not all(":" in c for c in constants): - raise ValueError(f"All constants must pass variables in via params {constants}") + if constants and not all(':' in c for c in constants): + raise ValueError(f'All constants must pass variables in via params {constants}') getname = getName() @@ -145,9 +145,9 @@ def makeParamsValues(*value_sets, constants=tuple(), types=tuple(), row_types=tu 'p': 2, # posterior } seg_ordering = { - "c": 0, # cervical - "t": 1, # thoracic - "a": 2, # abdominal + 'c': 0, # cervical + 't': 1, # thoracic + 'a': 2, # abdominal } @@ -155,7 +155,7 @@ def anat_index(sample): # count the number of distinct values less than a given integer # create the map - sam, sam_id, seg, seg_id = sample.split("-") + sam, sam_id, seg, seg_id = sample.split('-') # FIXME bad becase left and right are unstable and we don't care about this, we just want relative to max possible # don't do this with sort sam_ind = sam_ordering[sam_id] @@ -172,7 +172,7 @@ def anat_index(sample): suffix = int(seg_id[-1].encode().hex()) return sam_ind, 0, rest, suffix else: - msg = f"unknown seg {sample}" + msg = f'unknown seg {sample}' print(msg) # FIXME TODO logging # raise ValueError(msg) # return int(f'{sam_ind}000') @@ -392,8 +392,8 @@ def __init__(self, queries): 'json-path-with-types', '#/path-metadata/data/#int/dataset_relative_path#derive-sample-id' ) - self.addr_jpspec = q.address_from_fadd_type_fadd("json-path-with-types", "#/local/tom-made-it-up/species") - self.addr_jpsaty = q.address_from_fadd_type_fadd("json-path-with-types", "#/local/tom-made-it-up/sample_type") + self.addr_jpspec = q.address_from_fadd_type_fadd('json-path-with-types', '#/local/tom-made-it-up/species') + self.addr_jpsaty = q.address_from_fadd_type_fadd('json-path-with-types', '#/local/tom-made-it-up/sample_type') # future version when we actually have the metadata files # addr_jpmod = address_from_fadd_type_fadd('json-path-with-types', '#/curation-export/manifest/#int/modality') @@ -402,7 +402,7 @@ def __init__(self, queries): # addr_jpsuid = address_from_fadd_type_fadd('json-path-with-types', '#/curation-export/subjects/#int/id_sub') # addr_jpsaid = address_from_fadd_type_fadd('json-path-with-types', '#/curation-export/samples/#int/id_sam') - self.addr_const_null = q.address_from_fadd_type_fadd("constant", None) + self.addr_const_null = q.address_from_fadd_type_fadd('constant', None) self.qd_nvlai1 = q.desc_quant_from_label('vagus level anatomical location distance index normalized v1') self.qd_nvlain1 = q.desc_quant_from_label('vagus level anatomical location distance index normalized v1 min') @@ -440,11 +440,11 @@ def __init__(self, queries): 'fascicle-cross-section': self.id_fascicle_cross_section, } - self.ct_mod = q.cterm_from_label("microct") # lol ct ct - self.ct_hack = q.cterm_from_label("hack-associate-some-value") + self.ct_mod = q.cterm_from_label('microct') # lol ct ct + self.ct_hack = q.cterm_from_label('hack-associate-some-value') self.luct = { - "ct-hack": self.ct_hack, - "microct": self.ct_mod, + 'ct-hack': self.ct_hack, + 'microct': self.ct_mod, } @@ -459,10 +459,10 @@ def ingest(dataset_uuid, extract_fun, session, commit=False, dev=False, values_a add a kwarg to control it maybe? """ - ocdn = " ON CONFLICT DO NOTHING" if dev else "" + ocdn = ' ON CONFLICT DO NOTHING' if dev else '' if extract_fun is None and values_args is None: - raise TypeError("need one of extract_fun or values_args") + raise TypeError('need one of extract_fun or values_args') ( updated_transitive, @@ -523,10 +523,10 @@ def ingest(dataset_uuid, extract_fun, session, commit=False, dev=False, values_a ) vt, params = makeParamsValues(values_objects) - session.execute(sql_text(f"INSERT INTO objects (id, id_type, id_file) VALUES {vt}{ocdn}"), params) + session.execute(sql_text(f'INSERT INTO objects (id, id_type, id_file) VALUES {vt}{ocdn}'), params) vt, params = makeParamsValues(values_dataset_object) - session.execute(sql_text(f"INSERT INTO dataset_object (dataset, object) VALUES {vt}{ocdn}"), params) + session.execute(sql_text(f'INSERT INTO dataset_object (dataset, object) VALUES {vt}{ocdn}'), params) vt, params = makeParamsValues(values_instances) session.execute( @@ -545,7 +545,7 @@ def ingest(dataset_uuid, extract_fun, session, commit=False, dev=False, values_a values_qv = make_values_quant(this_dataset_updated_uuid, i, luinst) vt, params = makeParamsValues(values_parents) - session.execute(sql_text(f"INSERT INTO instance_parent VALUES {vt}{ocdn}"), params) + session.execute(sql_text(f'INSERT INTO instance_parent VALUES {vt}{ocdn}'), params) vt, params = makeParamsValues(void) session.execute( @@ -601,7 +601,7 @@ def extract_reva_ft(dataset_uuid, source_local=False, visualize=True): blob = json.load(f) else: - resp = requests.get(f"https://cassava.ucsd.edu/sparc/datasets/{dataset_uuid}/LATEST/path-metadata.json") + resp = requests.get(f'https://cassava.ucsd.edu/sparc/datasets/{dataset_uuid}/LATEST/path-metadata.json') try: blob = resp.json() @@ -609,8 +609,8 @@ def extract_reva_ft(dataset_uuid, source_local=False, visualize=True): breakpoint() raise e - for j in blob["data"]: - j["type"] = "pathmeta" + for j in blob['data']: + j['type'] = 'pathmeta' ir = fromJson(blob) @@ -641,7 +641,7 @@ def extract_reva_ft(dataset_uuid, source_local=False, visualize=True): ) / mdp1 # ensure there is overlap between section for purposes of testing # TODO norm_anat_index_min # TODO norm_anat_index_max - dd[e["dataset"], e["sample"]].append(e) + dd[e['dataset'], e['sample']].append(e) inst_obj_index = dict(dd) max_nai = max([e['norm_anat_index_v2'] for e in exts]) @@ -728,9 +728,9 @@ def extract_reva_ft(dataset_uuid, source_local=False, visualize=True): instances = {**subjects, **samples} values_objects = [ - (i, o["id_type"], o["id_file"] if "id_file" in o else None) + (i, o['id_type'], o['id_file'] if 'id_file' in o else None) for i, o in objects.items() - if o["id_type"] != "dataset" # already did it above + if o['id_type'] != 'dataset' # already did it above ] values_dataset_object = dataset_object @@ -1315,12 +1315,12 @@ def ingest_demo_jp2(session, source_local=True, do_insert=True, commit=False, de def ingest_reva_ft_all(session, source_local=False, do_insert=True, batch=False, commit=False, dev=False): dataset_uuids = ( - "aa43eda8-b29a-4c25-9840-ecbd57598afc", # f001 + 'aa43eda8-b29a-4c25-9840-ecbd57598afc', # f001 # the rest have uuid1 issues :/ all in the undefined folder it seems, might be able to fix with a reupload - "bc4cc558-727c-4691-ae6d-498b57a10085", # f002 # XXX has a uuid1 so breaking in prod right now have to push the new pipelines - "ec6ad74e-7b59-409b-8fc7-a304319b6faf", # f003 # also uuid1 issue - "a8b2bdc7-54df-46a3-810e-83cdf33cfc3a", # f004 - "04a5fed9-7ba6-4292-b1a6-9cab5c38895f", # f005 + 'bc4cc558-727c-4691-ae6d-498b57a10085', # f002 # XXX has a uuid1 so breaking in prod right now have to push the new pipelines + 'ec6ad74e-7b59-409b-8fc7-a304319b6faf', # f003 # also uuid1 issue + 'a8b2bdc7-54df-46a3-810e-83cdf33cfc3a', # f004 + '04a5fed9-7ba6-4292-b1a6-9cab5c38895f', # f005 ) batched = [] @@ -1378,5 +1378,5 @@ def main(source_local=False, commit=False, echo=True): engine.dispose() -if __name__ == "__main__": +if __name__ == '__main__': main() diff --git a/quantdb/mysql_app/__init__.py b/quantdb/mysql_app/__init__.py deleted file mode 100644 index e73dd2a..0000000 --- a/quantdb/mysql_app/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from __future__ import annotations - -from . import database - -__all__ = ['database'] diff --git a/quantdb/mysql_app/database.py b/quantdb/mysql_app/database.py deleted file mode 100644 index 411d706..0000000 --- a/quantdb/mysql_app/database.py +++ /dev/null @@ -1,17 +0,0 @@ -from __future__ import annotations - -from typing import Any - -from sqlalchemy import create_engine -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import sessionmaker - -from quantdb.config import Settings - -# TODO: get new setting env setup first -# SQLALCHEMY_DATABASE_URL = Settings().MYSQL_URL - -# engine = create_engine(SQLALCHEMY_DATABASE_URL) -# SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) - -# Base: Any = declarative_base() From db38a8ec08623d205fab55ed343cf8a9be5887e9 Mon Sep 17 00:00:00 2001 From: Troy Sincomb Date: Mon, 12 May 2025 14:07:56 -0700 Subject: [PATCH 14/18] init for test_db --- .gitignore | 1 + bin/prepare_test_db.sh | 30 + pyproject.toml | 3 +- quantdb/client.py | 33 + quantdb/config.py | 13 + quantdb/generic_ingest.py | 236 +++++ quantdb/models.py | 1456 ++++++++++++++++++++++++++++ quantdb/postgresql_app/__init__.py | 5 - quantdb/postgresql_app/database.py | 17 - {test => tests}/__init__.py | 0 {test => tests}/test_api.py | 0 tests/test_generic_ingest.py | 97 ++ 12 files changed, 1868 insertions(+), 23 deletions(-) create mode 100644 bin/prepare_test_db.sh create mode 100644 quantdb/client.py create mode 100644 quantdb/generic_ingest.py create mode 100644 quantdb/models.py delete mode 100644 quantdb/postgresql_app/__init__.py delete mode 100644 quantdb/postgresql_app/database.py rename {test => tests}/__init__.py (100%) rename {test => tests}/test_api.py (100%) create mode 100644 tests/test_generic_ingest.py diff --git a/.gitignore b/.gitignore index a08c0b8..5a3835f 100644 --- a/.gitignore +++ b/.gitignore @@ -174,3 +174,4 @@ cython_debug/ .pypirc README.pdf +.DS_STORE \ No newline at end of file diff --git a/bin/prepare_test_db.sh b/bin/prepare_test_db.sh new file mode 100644 index 0000000..d585e5e --- /dev/null +++ b/bin/prepare_test_db.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +# quantdb-prepare-test-db.sh +# This script prepares a test PostgreSQL database with the same schema as the main DB and populates it with the first 10 rows of each table. +# Usage: ./bin/prepare_test_db.sh [MAIN_DB] [TEST_DB] [PGUSER] [PGHOST] [PGPORT] + +set -e + +MAIN_DB=${1:-quantdb} +TEST_DB=${2:-quantdb_test} +PGUSER=${3:-quantdb-test-admin} +PGHOST=${4:-localhost} +PGPORT=${5:-5432} + +# Drop and recreate the test database +psql -U $PGUSER -h $PGHOST -p $PGPORT -c "DROP DATABASE IF EXISTS $TEST_DB;" +psql -U $PGUSER -h $PGHOST -p $PGPORT -c "CREATE DATABASE $TEST_DB;" + +# Dump schema only from main DB and restore to test DB +pg_dump -U $PGUSER -h $PGHOST -p $PGPORT -s $MAIN_DB | psql -U $PGUSER -h $PGHOST -p $PGPORT $TEST_DB + +# Get all table names from the main DB +TABLES=$(psql -U $PGUSER -h $PGHOST -p $PGPORT -d $MAIN_DB -Atc "SELECT tablename FROM pg_tables WHERE schemaname='public';") + +# For each table, copy the first 10 rows from main DB to test DB +for TABLE in $TABLES; do + COLS=$(psql -U $PGUSER -h $PGHOST -p $PGPORT -d $MAIN_DB -Atc "SELECT string_agg('"' || column_name || '"', ',') FROM information_schema.columns WHERE table_name='$TABLE' AND table_schema='public';") + psql -U $PGUSER -h $PGHOST -p $PGPORT -d $TEST_DB -c "INSERT INTO \"$TABLE\" ($COLS) SELECT $COLS FROM dblink('dbname=$MAIN_DB','SELECT $COLS FROM \"$TABLE\" LIMIT 10') AS t($COLS);" 2>/dev/null || true +done + +echo "Test database '$TEST_DB' prepared with schema and first 10 rows of each table from '$MAIN_DB'." diff --git a/pyproject.toml b/pyproject.toml index 2b360b8..d0e0f9b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,8 @@ dependencies = [ "orthauth ~= 0.0.18", "cython", "psycopg2-binary", - "pre-commit ~= 4.1.0" + "pre-commit ~= 4.1.0", + "pytest==8.3.5", ] name = "quantdb" version = "0.1.0" diff --git a/quantdb/client.py b/quantdb/client.py new file mode 100644 index 0000000..0909a5a --- /dev/null +++ b/quantdb/client.py @@ -0,0 +1,33 @@ +from sqlalchemy import create_engine +from sqlalchemy.orm import Session + +from quantdb.config import auth +from quantdb.utils import dbUri + + +def get_session(echo: bool = True, test: bool = False) -> Session: + """ + Get a SQLAlchemy session for the main or test database. + + Parameters + ---------- + echo : bool, optional + If True, SQLAlchemy will log all statements. + test : bool, optional + If True, connect to the test database (quantdb_test or as configured). + + Returns + ------- + session : sqlalchemy.orm.Session + The database session. + """ + # pull in the db connection info + dbkwargs = {k: auth.get(f'db-{k}') for k in ('user', 'host', 'port', 'database')} + dbkwargs['dbuser'] = dbkwargs.pop('user') + dbkwargs['dbuser'] = 'quantdb-test-admin' if test else dbkwargs['dbuser'] + if test: + dbkwargs['database'] = auth.get('test-db-database') or 'quantdb_test' + engine = create_engine(dbUri(**dbkwargs)) # type: ignore + engine.echo = echo + session = Session(engine) + return session diff --git a/quantdb/config.py b/quantdb/config.py index a50ec33..e42b009 100644 --- a/quantdb/config.py +++ b/quantdb/config.py @@ -1,3 +1,16 @@ +from dataclasses import dataclass + import orthauth as oa +from quantdb.utils import dbUri + auth = oa.configure_here('auth-config.py', __name__) + + +@dataclass +class Settings: + """Settings for the app.""" + + db_params = {k: auth.get(f'db-{k}') for k in ('user', 'host', 'port', 'database')} + db_params['dbuser'] = db_params.pop('user') + SQLALCHEMY_DATABASE_URI = dbUri(**db_params) diff --git a/quantdb/generic_ingest.py b/quantdb/generic_ingest.py new file mode 100644 index 0000000..b28d5d3 --- /dev/null +++ b/quantdb/generic_ingest.py @@ -0,0 +1,236 @@ +import ast +import uuid + +import pandas as pd +from sqlalchemy import UniqueConstraint, inspect +from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm import Session +from sqlalchemy.sql.elements import ClauseElement + +from quantdb.client import get_session +from quantdb.models import Objects + +# df = pd.read_csv("data/CadaverVNMorphology_OutputMetrics.csv", index_col=0) +# df = df.T.reset_index(drop=True) +# df["id_sub"] = "sub-" + df.sub_sam +# df.index = df.index + 1 +# df.head() + + +def object_as_dict(obj): + """ + Convert SQLAlchemy ORM object to a dictionary of attributes. + + Parameters + ---------- + obj : SQLAlchemy ORM object + The object to convert to a dictionary. + + Returns + ------- + dict + Dictionary containing non-null column attributes of the object. + """ + return {c.key: getattr(obj, c.key) for c in inspect(obj).mapper.column_attrs if getattr(obj, c.key) is not None} + + +def get_or_create(session, obj, back_populate=None): + """ + Retrieve an existing instance of the object from the database, or create it if it does not exist. + + Parameters + ---------- + session : sqlalchemy.orm.Session + The database session to use for querying and committing. + obj : SQLAlchemy model instance + The object to retrieve or create. + back_populate : dict, optional + Dictionary mapping attribute names to values to back-populate after creation. + + Returns + ------- + instance : SQLAlchemy model instance + The retrieved or newly created instance. + + Notes + ----- + The function will roll back the transaction and attempt to retrieve the object + if an exception occurs during creation. + """ + model = obj.__class__ + data = object_as_dict(obj) + instance = session.query(model).filter_by(**data).one_or_none() + if instance: + return instance + else: + params = {k: v for k, v in data.items() if not isinstance(v, ClauseElement)} + instance = model(**params) + try: + session.add(instance) + session.commit() + if back_populate: + for attr, value in back_populate.items(): + setattr(instance, attr, value) + session.commit() + except Exception: + session.rollback() + instance = session.query(model).filter_by(**data).one() + return instance + else: + return instance + + +def get_constraint_columns(model): + """ + Returns the columns of the unique constraints for a SQLAlchemy model. + + Parameters + ---------- + model : SQLAlchemy model class + The SQLAlchemy model class to inspect for constraints. + + Returns + ------- + list of list of str + A list of lists, where each inner list contains the column names + that form a unique constraint. + """ + constraints = [ + constraint for constraint in model.__mapper__.tables[0].constraints if isinstance(constraint, UniqueConstraint) + ] + + return [[c.name for c in list(constraint.columns)] for constraint in constraints] + + +def query_by_constraints(db, obj): + """ + Query for an existing object in the database using its unique constraints. + + Parameters + ---------- + db : sqlalchemy.orm.Session + The database session to use for querying. + obj : SQLAlchemy model instance + The object to find in the database. + + Returns + ------- + SQLAlchemy model instance or None + The existing instance if found, None otherwise. + """ + filter_criteria = object_as_dict(obj) + + existing_obj = None + for cols in get_constraint_columns(obj.__class__): + if set(cols) != (set(cols) & set(filter_criteria.keys())): + continue + + constraint_filter = {key: value for key, value in filter_criteria.items() if key in cols} + + existing_obj = db.query(type(obj)).filter_by(**constraint_filter).first() + if existing_obj: + return existing_obj + + +def back_populate_tables(db: Session, obj) -> object: + """ + Recursively back-populates objects in the database, + retrieving existing objects instead of failing on duplicates. + + Parameters + ---------- + db : sqlalchemy.orm.Session + The database session. + obj : SQLAlchemy model instance + The ORM model object. + + Returns + ------- + object + The ORM model object, with its related objects back-populated. + + Notes + ----- + This function: + - Iterates through all relationships of the object + - Handles both initial population and recursive calls + - Performs a complete rollback if any commit fails + - Filters by all primary key columns of the object + - Handles cases where the child object is not a valid ORM instance + """ + mapper = obj.__mapper__ # Get the mapper for the object + # print(obj) + try: + # Iterate through all relationships of the object + for relationship_prop in mapper.relationships: + if relationship_prop.direction.name == 'MANYTOONE' or relationship_prop.direction.name == 'MANYTOMANY': + parent = getattr(obj, relationship_prop.key) + if parent: + filter_criteria = object_as_dict(parent) + existing_obj = db.query(type(parent)).filter_by(**filter_criteria).first() + if existing_obj: + for key, value in object_as_dict(existing_obj).items(): + if hasattr(parent, key): + setattr(parent, key, value) + # print("populated", object_as_dict(parent)) + setattr(obj, relationship_prop.key, parent) + # db.add(parent) + # db.commit() + back_populate_tables(db, parent) + + # print("object", type(obj)) + filter_criteria = object_as_dict(obj) + print(type(obj), filter_criteria) + existing_obj = db.query(type(obj)).filter_by(**filter_criteria).first() + print('filter criteria', filter_criteria) + print('found:', existing_obj) + + if not existing_obj: + existing_obj = query_by_constraints(db, obj) + + # print("filter using", filter_criteria) + # existing_obj = db.query(type(obj)).filter_by(**filter_criteria).first() + # print("is existing??", existing_obj) + + if not existing_obj: + db.add(obj) + # db.add(obj) + # db.flush() + db.commit() + # db.refresh(obj) + print('added new') + else: + db.merge(obj) + db.commit() + for key, value in object_as_dict(existing_obj).items(): + if hasattr(obj, key): + setattr(obj, key, value) + print('updated existing') + # print("after update", object_as_dict(existing_obj)) + + except IntegrityError: + db.rollback() # Rollback the transaction in case of an error + # print(f"Error during commit: {e}") + raise # Re-raise the exception to indicate failure + + return obj # Return the object with its related objects back-populated + + +if __name__ == '__main__': + + session = get_session(echo=False) + + object_dataset = Objects( + id='55c5b69c-a5b8-4881-a105-e4048af26fa5', + id_type='dataset', + id_file=None, + id_internal=None, + ) + object_package = Objects( + id='20720c2e-83fb-4454-bef1-1ce6a97fa748', + id_type='package', + id_file=1094489, + id_internal=None, + objects_=object_dataset, + ) + back_populate_tables(session, object_package) diff --git a/quantdb/models.py b/quantdb/models.py new file mode 100644 index 0000000..5fbc6ee --- /dev/null +++ b/quantdb/models.py @@ -0,0 +1,1456 @@ +import uuid +from typing import List, Optional + +from sqlalchemy import ( + CheckConstraint, + Column, + DateTime, + Enum, + ForeignKey, + ForeignKeyConstraint, + Identity, + Index, + Integer, + Numeric, + PrimaryKeyConstraint, + String, + Table, + Text, + UniqueConstraint, + Uuid, + event, + text, +) +from sqlalchemy.dialects.postgresql import JSONB +from sqlalchemy.orm import ( + Mapped, + declarative_base, + foreign, + mapped_column, + relationship, + validates, +) +from sqlalchemy.orm.base import Mapped + +Base = declarative_base() +metadata = Base.metadata + + +class Addresses(Base): + """Addresses of data + + Parameters + ---------- + id : int + Unique identifier for the address. + addr_type : str, optional + Type of address. + value_type : str, optional + Type of value. + addr_field : str, optional + Address field. + curator_note : str, optional + Curator note. + + Example + ------- + #/path-metadata/data/#int/dataset_relative_path + """ + + __tablename__ = 'addresses' + __table_args__ = ( + PrimaryKeyConstraint('id', name='addresses_pkey'), + UniqueConstraint( + 'addr_type', + 'addr_field', + 'value_type', + name='addresses_addr_type_addr_field_value_type_key', + ), + ) + + id = mapped_column( + Integer, + Identity( + start=1, + increment=1, + minvalue=1, + maxvalue=2147483647, + cycle=False, + cache=1, + ), + ) + addr_type = mapped_column( + Enum( + 'constant', + 'tabular-header', + 'tabular-alt-header', + 'workbook-sheet-tabular-header', + 'workbook-sheet-tabular-alt-header', + 'json-path-with-types', + 'file-system-extracted', + 'arbitrary-function', + name='address_type', + ) + ) + value_type = mapped_column( + Enum('single', 'multi', name='field_value_type'), + server_default=text("'single'::field_value_type"), + ) + addr_field = mapped_column(Text) + curator_note = mapped_column(Text) + + obj_desc_inst: Mapped[List['ObjDescInst']] = relationship( + 'ObjDescInst', + uselist=True, + foreign_keys='[ObjDescInst.addr_desc_inst]', + back_populates='addresses', + viewonly=True, + cascade='all, delete-orphan', + ) + obj_desc_inst_: Mapped[List['ObjDescInst']] = relationship( + 'ObjDescInst', + uselist=True, + foreign_keys='[ObjDescInst.addr_field]', + back_populates='addresses_field', + viewonly=True, + cascade='all, delete-orphan', + ) + obj_desc_cat: Mapped[List['ObjDescCat']] = relationship( + 'ObjDescCat', + uselist=True, + foreign_keys='[ObjDescCat.addr_desc_inst]', + back_populates='addresses', + viewonly=True, + cascade='all, delete-orphan', + ) + obj_desc_cat_: Mapped[List['ObjDescCat']] = relationship( + 'ObjDescCat', + uselist=True, + foreign_keys='[ObjDescCat.addr_field]', + back_populates='addresses_', + viewonly=True, + cascade='all, delete-orphan', + ) + obj_desc_quant: Mapped[List['ObjDescQuant']] = relationship( + 'ObjDescQuant', + uselist=True, + foreign_keys='[ObjDescQuant.addr_aspect]', + back_populates='addresses_aspect', + viewonly=True, + cascade='all, delete-orphan', + ) + obj_desc_quant_: Mapped[List['ObjDescQuant']] = relationship( + 'ObjDescQuant', + uselist=True, + foreign_keys='[ObjDescQuant.addr_desc_inst]', + back_populates='addresses', + viewonly=True, + cascade='all, delete-orphan', + ) + obj_desc_quant1: Mapped[List['ObjDescQuant']] = relationship( + 'ObjDescQuant', + uselist=True, + foreign_keys='[ObjDescQuant.addr_field]', + back_populates='addresses_field', + viewonly=True, + cascade='all, delete-orphan', + ) + obj_desc_quant2: Mapped[List['ObjDescQuant']] = relationship( + 'ObjDescQuant', + uselist=True, + foreign_keys='[ObjDescQuant.addr_unit]', + back_populates='addresses_unit', + viewonly=True, + cascade='all, delete-orphan', + ) + + +class Aspects(Base): + """Aspects of data + + Parameters + ---------- + id : int + Unique identifier for the aspect. + label : str + Label of the aspect. + iri : str + IRI of the aspect. + description : str, optional + Description of the aspect. + + Example + ------- + label: "distance" + iri: "http://uri.interlex.org/tgbugs/uris/readable/aspect/distance" + """ + + __tablename__ = 'aspects' + __table_args__ = ( + PrimaryKeyConstraint('id', name='aspects_pkey'), + UniqueConstraint('iri', name='aspects_iri_key'), + UniqueConstraint('label', name='aspects_label_key'), + ) + + id = mapped_column( + Integer, + Identity( + start=1, + increment=1, + minvalue=1, + maxvalue=2147483647, + cycle=False, + cache=1, + ), + ) + label = mapped_column(Text, nullable=False) + iri = mapped_column(Text, nullable=False) + description = mapped_column(Text) + + # aspects: Mapped["Aspects"] = relationship( + # "Aspects", + # secondary="aspect_parent", + # primaryjoin=lambda: Aspects.id == t_aspect_parent.c.id, + # secondaryjoin=lambda: Aspects.id == t_aspect_parent.c.parent, + # back_populates="aspects_", + # ) + # aspects_: Mapped["Aspects"] = relationship( + # "Aspects", + # secondary="aspect_parent", + # primaryjoin=lambda: Aspects.id == t_aspect_parent.c.parent, + # secondaryjoin=lambda: Aspects.id == t_aspect_parent.c.id, + # back_populates="aspects", + # ) + descriptors_quant: Mapped[List['DescriptorsQuant']] = relationship( + 'DescriptorsQuant', + uselist=True, + back_populates='aspects', + viewonly=True, + cascade='all, delete-orphan', + sync_backref=False, + ) + + +class ControlledTerms(Base): + """Controlled terms + + Parameters + ---------- + id : int + Unique identifier for the controlled term. + label : str + Label of the controlled term. + iri : str + IRI of the controlled term. + """ + + __tablename__ = 'controlled_terms' + __table_args__ = ( + PrimaryKeyConstraint('id', name='controlled_terms_pkey'), + UniqueConstraint('iri', name='controlled_terms_iri_key'), + UniqueConstraint('label', name='controlled_terms_label_key'), + ) + + id = mapped_column( + Integer, + Identity( + start=1, + increment=1, + minvalue=1, + maxvalue=2147483647, + cycle=False, + cache=1, + ), + ) + label = mapped_column(Text, nullable=False) + iri = mapped_column(Text, nullable=False) + + values_cat: Mapped[List['ValuesCat']] = relationship('ValuesCat', uselist=True, back_populates='controlled_terms') + + +class DescriptorsInst(Base): + """Descriptors Instance + + Parameters + ---------- + id : int + Unique identifier for the descriptor. + label : str + Label of the descriptor. + iri : str + IRI of the descriptor. + description : str, optional + Description of the descriptor. + + """ + + __tablename__ = 'descriptors_inst' + __table_args__ = ( + PrimaryKeyConstraint('id', name='descriptors_inst_pkey'), + UniqueConstraint('iri', name='descriptors_inst_iri_key'), + UniqueConstraint('label', name='descriptors_inst_label_key'), + ) + + id = mapped_column( + Integer, + Identity( + start=1, + increment=1, + minvalue=1, + maxvalue=2147483647, + cycle=False, + cache=1, + ), + ) + label = mapped_column(Text, nullable=False) + iri = mapped_column(Text, nullable=False) + description = mapped_column(Text) + + # descriptors_inst: Mapped["DescriptorsInst"] = relationship( + # "DescriptorsInst", + # secondary="class_parent", + # primaryjoin=lambda: DescriptorsInst.id == t_class_parent.c.id, + # secondaryjoin=lambda: DescriptorsInst.id == t_class_parent.c.parent, + # back_populates="descriptors_inst_", + # ) + # descriptors_inst_: Mapped["DescriptorsInst"] = relationship( + # "DescriptorsInst", + # secondary="class_parent", + # primaryjoin=lambda: DescriptorsInst.id == t_class_parent.c.parent, + # secondaryjoin=lambda: DescriptorsInst.id == t_class_parent.c.id, + # back_populates="descriptors_inst", + # ) + descriptors_cat: Mapped[List['DescriptorsCat']] = relationship( + 'DescriptorsCat', uselist=True, back_populates='descriptors_inst' + ) + descriptors_quant: Mapped[List['DescriptorsQuant']] = relationship( + 'DescriptorsQuant', + uselist=True, + back_populates='descriptors_inst', + viewonly=True, + cascade='all, delete-orphan', + ) + obj_desc_inst: Mapped[List['ObjDescInst']] = relationship( + 'ObjDescInst', + uselist=True, + back_populates='descriptors_inst', + viewonly=True, + cascade='all, delete-orphan', + ) + values_inst: Mapped[List['ValuesInst']] = relationship( + 'ValuesInst', + uselist=True, + back_populates='descriptors_inst', + viewonly=True, + cascade='all, delete-orphan', + ) + values_cat: Mapped[List['ValuesCat']] = relationship( + 'ValuesCat', + uselist=True, + back_populates='descriptors_inst', + viewonly=True, + cascade='all, delete-orphan', + ) + values_quant: Mapped[List['ValuesQuant']] = relationship( + 'ValuesQuant', + uselist=True, + back_populates='descriptors_inst', + viewonly=True, + cascade='all, delete-orphan', + ) + + +class Objects(Base): + """ + Represents an object in the database with various relationships and constraints. + + Attributes + ---------- + id : Uuid + Unique identifier for the object. + id_type : Enum + Type of the remote ID, can be one of 'organization', 'dataset', 'collection', 'package', or 'quantdb'. + id_file : Integer + Identifier for the file associated with the object. + id_internal : Uuid + Internal identifier for the object. + + Relationships + ------------- + objects : Mapped["Objects"] + Relationship to other objects through the 'dataset_object' table. + objects_ : Mapped["Objects"] + Reverse relationship to other objects through the 'dataset_object' table. + obj_desc_inst : Mapped[List["ObjDescInst"]] + Relationship to object description instances. + values_inst : Mapped[List["ValuesInst"]] + Relationship to value instances. + obj_desc_cat : Mapped[List["ObjDescCat"]] + Relationship to object description categories. + obj_desc_quant : Mapped[List["ObjDescQuant"]] + Relationship to object description quantities. + values_cat : Mapped[List["ValuesCat"]] + Relationship to value categories. + values_quant : Mapped[List["ValuesQuant"]] + Relationship to value quantities. + + Constraints + ----------- + __table_args__ : tuple + Contains various constraints and indexes for the table: + - CheckConstraint to ensure 'id_file' is not NULL if 'id_type' is 'package'. + - CheckConstraint to ensure 'id_internal' is not NULL and 'id' equals 'id_internal' if 'id_type' is 'quantdb'. + - PrimaryKeyConstraint on 'id'. + - Index on 'id_internal'. + """ + + __tablename__ = 'objects' + __table_args__ = ( + # CheckConstraint( + # "id_type <> 'package'::remote_id_type OR id_file IS NOT NULL", + # name="constraint_objects_remote_id_type_id_package", + # ), + # CheckConstraint( + # "id_type <> 'quantdb'::remote_id_type OR id_internal IS NOT NULL AND id = id_internal", + # name="constraint_objects_remote_id_type_id_internal", + # ), + # ForeignKeyConstraint( + # ["id_internal"], + # ["objects_internal.id"], + # name="objects_id_internal_fkey", + # ), + PrimaryKeyConstraint('id', name='objects_pkey'), + Index('idx_objects_id_internal', 'id_internal'), + ) + + id = mapped_column(Uuid) + id_type = mapped_column( + Enum( + 'organization', + 'dataset', + 'collection', + 'package', + 'quantdb', + name='remote_id_type', + ), + nullable=False, + ) + id_file = mapped_column(Integer, nullable=True) + id_internal = mapped_column(Uuid, nullable=True) + + # objects_internal: Mapped[Optional["ObjectsInternal"]] = relationship( + # "ObjectsInternal", foreign_keys=[id_internal], back_populates="objects" + # ) + objects: Mapped['Objects'] = relationship( + 'Objects', + secondary='dataset_object', + primaryjoin=lambda: Objects.id == t_dataset_object.c.dataset, + secondaryjoin=lambda: Objects.id == t_dataset_object.c.object, + # back_populates="objects_", + # cascade="all, delete-orphan", + ) + objects_: Mapped['Objects'] = relationship( + 'Objects', + secondary='dataset_object', + primaryjoin=lambda: Objects.id == t_dataset_object.c.object, + secondaryjoin=lambda: Objects.id == t_dataset_object.c.dataset, + # back_populates="objects", + # cascade="all, delete-orphan", + ) + # objects_internal_: Mapped[List["ObjectsInternal"]] = relationship( + # "ObjectsInternal", + # uselist=True, + # foreign_keys="[ObjectsInternal.dataset]", + # back_populates="objects_", + # ) + obj_desc_inst: Mapped[List['ObjDescInst']] = relationship( + 'ObjDescInst', + uselist=True, + back_populates='objects', + viewonly=True, + cascade='all, delete-orphan', + ) + obj_desc_cat: Mapped[List['ObjDescCat']] = relationship( + 'ObjDescCat', + uselist=True, + back_populates='objects', + viewonly=True, + cascade='all, delete-orphan', + ) + obj_desc_quant: Mapped[List['ObjDescQuant']] = relationship( + 'ObjDescQuant', + uselist=True, + back_populates='objects', + viewonly=True, + cascade='all, delete-orphan', + ) + values_inst: Mapped[List['ValuesInst']] = relationship( + 'ValuesInst', + uselist=True, + back_populates='objects', + viewonly=True, + cascade='all, delete-orphan', + ) + values_cat: Mapped[List['ValuesCat']] = relationship( + 'ValuesCat', + uselist=True, + back_populates='objects', + viewonly=True, + cascade='all, delete-orphan', + ) + values_quant: Mapped[List['ValuesQuant']] = relationship( + 'ValuesQuant', + uselist=True, + back_populates='objects', + viewonly=True, + cascade='all, delete-orphan', + ) + + def __repr__(self): + return f'' + + @staticmethod + def preprocess_id(id_value: str) -> str: + """ + Preprocess the id to remove 'N:dataset:' if it exists. + + Parameters + ---------- + id_value : str + The original id value. + + Returns + ------- + str + The preprocessed id value. + """ + if isinstance(id_value, str): + if id_value.startswith('N:dataset:'): + return id_value.replace('N:dataset:', '') + # if isinstance(id_value, str): + # id_value = uuid.UUID(id_value) + return id_value + + @validates('id') + def validate_id(self, key: str, id_value: str) -> str: + """ + Validate and preprocess the id before storing it. + + Parameters + ---------- + key : str + The key being validated. + id_value : str + The original id value. + + Returns + ------- + str + The preprocessed id value. + """ + return self.preprocess_id(id_value) + + +# class ObjectsInternal(Base): +# __tablename__ = "objects_internal" +# __table_args__ = ( +# CheckConstraint( +# "type <> 'path-metadata'::oi_type OR updated_transitive IS NOT NULL AND dataset IS NOT NULL", +# name="constraint_objects_internal_type_updated_transitive", +# ), +# ForeignKeyConstraint( +# ["dataset"], ["objects.id"], name="constraint_oi_dataset_fk" +# ), +# PrimaryKeyConstraint("id", name="objects_internal_pkey"), +# UniqueConstraint( +# "dataset", +# "updated_transitive", +# name="objects_internal_dataset_updated_transitive_key", +# ), +# ) + +# id = mapped_column(Uuid, server_default=text("gen_random_uuid()")) +# type = mapped_column( +# Enum("path-metadata", "other", name="oi_type"), +# server_default=text("'other'::oi_type"), +# ) +# dataset = mapped_column(Uuid) +# updated_transitive = mapped_column(DateTime) +# label = mapped_column(Text) +# curator_note = mapped_column(Text) + +# BUG: cycles! +# objects: Mapped[List["Objects"]] = relationship( +# "Objects", +# uselist=True, +# foreign_keys="[Objects.id_internal]", +# back_populates="objects_internal", +# ) +# objects_: Mapped[Optional["Objects"]] = relationship( +# "Objects", foreign_keys=[dataset], back_populates="objects_internal_" +# ) + + +class Units(Base): + __tablename__ = 'units' + __table_args__ = ( + PrimaryKeyConstraint('id', name='units_pkey'), + UniqueConstraint('iri', name='units_iri_key'), + UniqueConstraint('label', name='units_label_key'), + ) + + id = mapped_column( + Integer, + Identity( + start=1, + increment=1, + minvalue=1, + maxvalue=2147483647, + cycle=False, + cache=1, + ), + primary_key=True, + ) + label = mapped_column(Text, nullable=False) + iri = mapped_column(Text, nullable=False) + + descriptors_quant: Mapped[List['DescriptorsQuant']] = relationship( + 'DescriptorsQuant', uselist=True, back_populates='units' + ) + + +t_aspect_parent = Table( + 'aspect_parent', + metadata, + Column('id', Integer, nullable=False), + Column('parent', Integer, nullable=False), + ForeignKeyConstraint(['id'], ['aspects.id'], name='aspect_parent_id_fkey'), + ForeignKeyConstraint(['parent'], ['aspects.id'], name='aspect_parent_parent_fkey'), + PrimaryKeyConstraint('id', 'parent', name='aspect_parent_pkey'), +) + + +t_class_parent = Table( + 'class_parent', + metadata, + Column('id', Integer, nullable=False), + Column('parent', Integer, nullable=False), + ForeignKeyConstraint(['id'], ['descriptors_inst.id'], name='class_parent_id_fkey'), + ForeignKeyConstraint(['parent'], ['descriptors_inst.id'], name='class_parent_parent_fkey'), + PrimaryKeyConstraint('id', 'parent', name='class_parent_pkey'), +) + + +t_dataset_object = Table( + 'dataset_object', + metadata, + Column('dataset', Uuid, nullable=False), + Column('object', Uuid, nullable=False), + ForeignKeyConstraint(['dataset'], ['objects.id'], name='dataset_object_dataset_fkey'), + ForeignKeyConstraint(['object'], ['objects.id'], name='dataset_object_object_fkey'), + PrimaryKeyConstraint('dataset', 'object', name='dataset_object_pkey'), +) + + +class DescriptorsCat(Base): + __tablename__ = 'descriptors_cat' + __table_args__ = ( + ForeignKeyConstraint( + ['domain'], + ['descriptors_inst.id'], + name='descriptors_cat_domain_fkey', + ), + PrimaryKeyConstraint('id', name='descriptors_cat_pkey'), + UniqueConstraint( + 'domain', + 'range', + 'label', + name='descriptors_cat_domain_range_label_key', + ), + ) + + id = mapped_column( + Integer, + Identity( + start=1, + increment=1, + minvalue=1, + maxvalue=2147483647, + cycle=False, + cache=1, + ), + ) + domain = mapped_column(Integer) + range = mapped_column(Enum('open', 'controlled', name='cat_range_type')) + label = mapped_column(Text) + description = mapped_column(Text) + curator_note = mapped_column(Text) + + descriptors_inst: Mapped[Optional['DescriptorsInst']] = relationship( + 'DescriptorsInst', back_populates='descriptors_cat' + ) + obj_desc_cat: Mapped[List['ObjDescCat']] = relationship( + 'ObjDescCat', uselist=True, back_populates='descriptors_cat' + ) + values_cat: Mapped[List['ValuesCat']] = relationship('ValuesCat', uselist=True, back_populates='descriptors_cat') + + +class DescriptorsQuant(Base): + """ + DescriptorsQuant model representing quantitative descriptors. + Attributes + ---------- + id : int + Primary key of the descriptor. + shape : str + Shape of the descriptor, default is 'scalar'. + label : str + Label of the descriptor. + aggregation_type : str + Type of aggregation, default is 'instance'. + unit : int + Foreign key referencing the unit. + aspect : int + Foreign key referencing the aspect. + domain : int + Foreign key referencing the domain. + description : str + Description of the descriptor. + curator_note : str + Notes from the curator. + Relationships + ------------- + aspects : Optional[Aspects] + Relationship to the Aspects model. + descriptors_inst : Optional[DescriptorsInst] + Relationship to the DescriptorsInst model. + units : Optional[Units] + Relationship to the Units model. + obj_desc_quant : List[ObjDescQuant] + Relationship to the ObjDescQuant model. + values_quant : List[ValuesQuant] + Relationship to the ValuesQuant model. + """ + + __tablename__ = 'descriptors_quant' + __table_args__ = ( + ForeignKeyConstraint(['aspect'], ['aspects.id'], name='descriptors_quant_aspect_fkey'), + ForeignKeyConstraint( + ['domain'], + ['descriptors_inst.id'], + name='descriptors_quant_domain_fkey', + ), + ForeignKeyConstraint(['unit'], ['units.id'], name='descriptors_quant_unit_fkey'), + PrimaryKeyConstraint('id', name='descriptors_quant_pkey'), + UniqueConstraint('label', name='descriptors_quant_label_key'), + UniqueConstraint( + 'unit', + 'aspect', + 'domain', + 'shape', + 'aggregation_type', + name='descriptors_quant_unit_aspect_domain_shape_aggregation_type_key', + ), + ) + + id = mapped_column( + Integer, + Identity( + start=1, + increment=1, + minvalue=1, + maxvalue=2147483647, + cycle=False, + cache=1, + ), + ) + shape = mapped_column( + Enum('scalar', name='quant_shape'), + nullable=False, + server_default=text("'scalar'::quant_shape"), + ) + label = mapped_column(Text, nullable=False) + aggregation_type = mapped_column( + Enum( + 'instance', + 'function', + 'summary', + 'mean', + 'media', + 'mode', + 'sum', + 'min', + 'max', + name='quant_agg_type', + ), + nullable=False, + server_default=text("'instance'::quant_agg_type"), + ) + unit = mapped_column(Integer) + aspect = mapped_column(Integer, ForeignKey('aspects.id')) + domain = mapped_column(Integer) + description = mapped_column(Text) + curator_note = mapped_column(Text) + + aspects: Mapped[Optional['Aspects']] = relationship( + 'Aspects', + back_populates='descriptors_quant', + # viewonly=True, + # cascade="all, delete-orphan", + ) + descriptors_inst: Mapped[Optional['DescriptorsInst']] = relationship( + 'DescriptorsInst', + back_populates='descriptors_quant', + # viewonly=True, + # cascade="all, delete-orphan", + ) + units: Mapped[Optional['Units']] = relationship('Units', back_populates='descriptors_quant') + obj_desc_quant: Mapped[List['ObjDescQuant']] = relationship( + 'ObjDescQuant', + uselist=True, + back_populates='descriptors_quant', + # viewonly=True, + # cascade="all, delete-orphan", + ) + values_quant: Mapped[List['ValuesQuant']] = relationship( + 'ValuesQuant', uselist=True, back_populates='descriptors_quant' + ) + + +class ObjDescInst(Base): + __tablename__ = 'obj_desc_inst' + __table_args__ = ( + ForeignKeyConstraint( + ['addr_desc_inst'], + ['addresses.id'], + name='obj_desc_inst_addr_desc_inst_fkey', + ), + ForeignKeyConstraint( + ['addr_field'], + ['addresses.id'], + name='obj_desc_inst_addr_field_fkey', + ), + ForeignKeyConstraint( + ['desc_inst'], + ['descriptors_inst.id'], + name='obj_desc_inst_desc_inst_fkey', + ), + ForeignKeyConstraint(['object'], ['objects.id'], name='obj_desc_inst_object_fkey'), + PrimaryKeyConstraint('object', 'desc_inst', name='obj_desc_inst_pkey'), + ) + + object = mapped_column(Uuid, nullable=False) + desc_inst = mapped_column(Integer, nullable=False) + addr_field = mapped_column(Integer, nullable=False) + addr_desc_inst = mapped_column(Integer) + expect = mapped_column(Integer) + + addresses: Mapped[Optional['Addresses']] = relationship( + 'Addresses', + foreign_keys=[addr_desc_inst], + back_populates='obj_desc_inst', + viewonly=True, + cascade='all, delete-orphan', + ) + addresses_field: Mapped['Addresses'] = relationship( + 'Addresses', + foreign_keys=[addr_field], + back_populates='obj_desc_inst_', + viewonly=True, + cascade='all, delete-orphan', + ) + descriptors_inst: Mapped['DescriptorsInst'] = relationship( + 'DescriptorsInst', + back_populates='obj_desc_inst', + viewonly=True, + cascade='all, delete-orphan', + ) + objects: Mapped['Objects'] = relationship( + 'Objects', + back_populates='obj_desc_inst', + viewonly=True, + cascade='all, delete-orphan', + ) + values_cat: Mapped[List['ValuesCat']] = relationship( + 'ValuesCat', + uselist=True, + back_populates='obj_desc_inst', + viewonly=True, + cascade='all, delete-orphan', + ) + values_quant: Mapped[List['ValuesQuant']] = relationship( + 'ValuesQuant', + uselist=True, + back_populates='obj_desc_inst', + viewonly=True, + cascade='all, delete-orphan', + ) + + +class ValuesInst(Base): + """ + Represents an instance of values in the database. + + Attributes + ---------- + id : int + The primary key of the values instance. + type : str + The type of the instance, which can be 'subject', 'sample', or 'below'. + id_sub : str + The subject identifier. + desc_inst : int + The descriptor instance identifier. + dataset : UUID + The dataset identifier. + id_formal : str + The formal identifier. + local_identifier : str + The local identifier. + id_sam : str + The sample identifier. + objects : Optional[Objects] + The related objects. + descriptors_inst : Optional[DescriptorsInst] + The related descriptor instances. + values_inst : ValuesInst + The related values instances through the 'equiv_inst' table. + values_inst_ : ValuesInst + The related values instances through the 'equiv_inst' table. + values_inst1 : ValuesInst + The related values instances through the 'instance_parent' table. + values_inst2 : ValuesInst + The related values instances through the 'instance_parent' table. + values_cat : List[ValuesCat] + The related categorical values. + values_quant : List[ValuesQuant] + The related quantitative values. + + Relationships + ------------- + objects : relationship + Relationship to the Objects table. + descriptors_inst : relationship + Relationship to the DescriptorsInst table. + values_inst : relationship + Self-referential relationship through the 'equiv_inst' table (left to right). + values_inst_ : relationship + Self-referential relationship through the 'equiv_inst' table (right to left). + values_inst1 : relationship + Self-referential relationship through the 'instance_parent' table (id to parent). + values_inst2 : relationship + Self-referential relationship through the 'instance_parent' table (parent to id). + values_cat : relationship + Relationship to the ValuesCat table. + values_quant : relationship + Relationship to the ValuesQuant table. + """ + + __tablename__ = 'values_inst' + __table_args__ = ( + CheckConstraint("id_sam ~ '^sam-'::text", name='values_inst_id_sam_check'), + CheckConstraint("id_sub ~ '^sub-'::text", name='values_inst_id_sub_check'), + CheckConstraint( + "type <> 'below'::instance_type OR id_sub IS NOT NULL OR id_sam IS NOT NULL", + name='constraint_values_inst_type_below', + ), + CheckConstraint( + "type <> 'sample'::instance_type OR id_sam IS NOT NULL AND id_sam = id_formal", + name='constraint_values_inst_type_id_sam', + ), + CheckConstraint( + "type <> 'subject'::instance_type OR id_sub = id_formal AND id_sam IS NULL", + name='constraint_values_inst_type_id_sub', + ), + CheckConstraint( + "type = 'below'::instance_type AND NOT id_formal ~ '^(sub|sam)-'::text OR type = 'subject'::instance_type AND id_formal ~ '^sub-'::text OR type = 'sample'::instance_type AND id_formal ~ '^sam-'::text", + name='constraint_values_inst_type_id_formal', + ), + ForeignKeyConstraint(['dataset'], ['objects.id'], name='values_inst_dataset_fkey'), + ForeignKeyConstraint( + ['desc_inst'], + ['descriptors_inst.id'], + name='values_inst_desc_inst_fkey', + ), + PrimaryKeyConstraint('id', name='values_inst_pkey'), + UniqueConstraint('dataset', 'id_formal', name='values_inst_dataset_id_formal_key'), + Index('idx_values_inst_dataset', 'dataset'), + Index('idx_values_inst_dataset_id_formal', 'dataset', 'id_formal'), + Index('idx_values_inst_dataset_id_sam', 'dataset', 'id_sam'), + Index('idx_values_inst_dataset_id_sub', 'dataset', 'id_sub'), + Index('idx_values_inst_desc_inst', 'desc_inst'), + Index('idx_values_inst_id_formal', 'id_formal'), + Index('idx_values_inst_id_sam', 'id_sam'), + Index('idx_values_inst_id_sub', 'id_sub'), + ) + + id = mapped_column( + Integer, + Identity( + start=1, + increment=1, + minvalue=1, + maxvalue=2147483647, + cycle=False, + cache=1, + ), + ) + type = mapped_column(Enum('subject', 'sample', 'below', name='instance_type'), nullable=False) + id_sub = mapped_column(Text, nullable=False) + desc_inst = mapped_column(Integer) + dataset = mapped_column(Uuid, ForeignKey('objects.id')) + id_formal = mapped_column(Text) + local_identifier = mapped_column(Text) + id_sam = mapped_column(Text) + + objects: Mapped[Optional['Objects']] = relationship( + 'Objects', + back_populates='values_inst', + viewonly=True, + cascade='all, delete-orphan', + ) + descriptors_inst: Mapped[Optional['DescriptorsInst']] = relationship( + 'DescriptorsInst', + back_populates='values_inst', + viewonly=True, + # cascade="all, delete-orphan", + # single_parent=True, + ) + # values_inst: Mapped["ValuesInst"] = relationship( + # "ValuesInst", + # secondary="equiv_inst", + # primaryjoin=lambda: ValuesInst.id == t_equiv_inst.c.left_thing, + # secondaryjoin=lambda: ValuesInst.id == t_equiv_inst.c.right_thing, + # back_populates="values_inst_", + # ) + # values_inst_: Mapped["ValuesInst"] = relationship( + # "ValuesInst", + # secondary="equiv_inst", + # primaryjoin=lambda: ValuesInst.id == t_equiv_inst.c.right_thing, + # secondaryjoin=lambda: ValuesInst.id == t_equiv_inst.c.left_thing, + # back_populates="values_inst", + # ) + # values_inst1: Mapped["ValuesInst"] = relationship( + # "ValuesInst", + # secondary="instance_parent", + # primaryjoin=lambda: ValuesInst.id == t_instance_parent.c.id, + # secondaryjoin=lambda: ValuesInst.id == t_instance_parent.c.parent, + # back_populates="values_inst2", + # ) + # values_inst2: Mapped["ValuesInst"] = relationship( + # "ValuesInst", + # secondary="instance_parent", + # primaryjoin=lambda: ValuesInst.id == t_instance_parent.c.parent, + # secondaryjoin=lambda: ValuesInst.id == t_instance_parent.c.id, + # back_populates="values_inst1", + # ) + values_cat: Mapped[List['ValuesCat']] = relationship('ValuesCat', uselist=True, back_populates='values_inst') + values_quant: Mapped[List['ValuesQuant']] = relationship( + 'ValuesQuant', + uselist=True, + back_populates='values_inst', + viewonly=True, + cascade='all, delete-orphan', + ) + + +t_equiv_inst = Table( + 'equiv_inst', + metadata, + Column('left_thing', Integer, nullable=False), + Column('right_thing', Integer, nullable=False), + CheckConstraint('left_thing <> right_thing', name='sse_no_self'), + ForeignKeyConstraint(['left_thing'], ['values_inst.id'], name='equiv_inst_left_thing_fkey'), + ForeignKeyConstraint(['right_thing'], ['values_inst.id'], name='equiv_inst_right_thing_fkey'), + PrimaryKeyConstraint('left_thing', 'right_thing', name='equiv_inst_pkey'), +) + + +t_instance_parent = Table( + 'instance_parent', + metadata, + Column('id', Integer, nullable=False), + Column('parent', Integer, nullable=False), + ForeignKeyConstraint(['id'], ['values_inst.id'], name='instance_parent_id_fkey'), + ForeignKeyConstraint(['parent'], ['values_inst.id'], name='instance_parent_parent_fkey'), + PrimaryKeyConstraint('id', 'parent', name='instance_parent_pkey'), +) + + +class ObjDescCat(Base): + __tablename__ = 'obj_desc_cat' + __table_args__ = ( + ForeignKeyConstraint( + ['addr_desc_inst'], + ['addresses.id'], + name='obj_desc_cat_addr_desc_inst_fkey', + ), + ForeignKeyConstraint( + ['addr_field'], + ['addresses.id'], + name='obj_desc_cat_addr_field_fkey', + ), + ForeignKeyConstraint( + ['desc_cat'], + ['descriptors_cat.id'], + name='obj_desc_cat_desc_cat_fkey', + ), + ForeignKeyConstraint(['object'], ['objects.id'], name='obj_desc_cat_object_fkey'), + PrimaryKeyConstraint('object', 'desc_cat', name='obj_desc_cat_pkey'), + ) + + object = mapped_column(Uuid, nullable=False) + desc_cat = mapped_column(Integer, nullable=False) + addr_field = mapped_column(Integer, nullable=False) + addr_desc_inst = mapped_column(Integer) + expect = mapped_column(Integer) + + addresses: Mapped[Optional['Addresses']] = relationship( + 'Addresses', + foreign_keys=[addr_desc_inst], + back_populates='obj_desc_cat', + ) + addresses_: Mapped['Addresses'] = relationship( + 'Addresses', foreign_keys=[addr_field], back_populates='obj_desc_cat_' + ) + descriptors_cat: Mapped['DescriptorsCat'] = relationship('DescriptorsCat', back_populates='obj_desc_cat') + objects: Mapped['Objects'] = relationship('Objects', back_populates='obj_desc_cat') + values_cat: Mapped[List['ValuesCat']] = relationship('ValuesCat', uselist=True, back_populates='obj_desc_cat') + + +class ObjDescQuant(Base): + __tablename__ = 'obj_desc_quant' + __table_args__ = ( + ForeignKeyConstraint( + ['addr_aspect'], + ['addresses.id'], + name='obj_desc_quant_addr_aspect_fkey', + ), + ForeignKeyConstraint( + ['addr_desc_inst'], + ['addresses.id'], + name='obj_desc_quant_addr_desc_inst_fkey', + ), + ForeignKeyConstraint( + ['addr_field'], + ['addresses.id'], + name='obj_desc_quant_addr_field_fkey', + ), + ForeignKeyConstraint( + ['addr_unit'], + ['addresses.id'], + name='obj_desc_quant_addr_unit_fkey', + ), + ForeignKeyConstraint( + ['desc_quant'], + ['descriptors_quant.id'], + name='obj_desc_quant_desc_quant_fkey', + ), + ForeignKeyConstraint(['object'], ['objects.id'], name='obj_desc_quant_object_fkey'), + PrimaryKeyConstraint('object', 'desc_quant', name='obj_desc_quant_pkey'), + ) + + object = mapped_column(Uuid, nullable=False) + desc_quant = mapped_column(Integer, nullable=False) + addr_field = mapped_column(Integer, nullable=False) + addr_unit = mapped_column(Integer) + addr_aspect = mapped_column(Integer) + addr_desc_inst = mapped_column(Integer) + expect = mapped_column(Integer) + + addresses: Mapped[Optional['Addresses']] = relationship( + 'Addresses', + foreign_keys=[addr_desc_inst], + back_populates='obj_desc_quant_', + viewonly=True, + cascade='all, delete-orphan', + ) + addresses_aspect: Mapped[Optional['Addresses']] = relationship( + 'Addresses', + foreign_keys=[addr_aspect], + back_populates='obj_desc_quant', + viewonly=True, + cascade='all, delete-orphan', + ) + addresses_field: Mapped['Addresses'] = relationship( + 'Addresses', + foreign_keys=[addr_field], + back_populates='obj_desc_quant1', + viewonly=True, + cascade='all, delete-orphan', + ) + addresses_unit: Mapped[Optional['Addresses']] = relationship( + 'Addresses', + foreign_keys=[addr_unit], + back_populates='obj_desc_quant2', + viewonly=True, + cascade='all, delete-orphan', + ) + descriptors_quant: Mapped['DescriptorsQuant'] = relationship( + 'DescriptorsQuant', + back_populates='obj_desc_quant', + viewonly=True, + cascade='all, delete-orphan', + ) + objects: Mapped['Objects'] = relationship( + 'Objects', + back_populates='obj_desc_quant', + viewonly=True, + cascade='all, delete-orphan', + ) + values_quant: Mapped[List['ValuesQuant']] = relationship( + 'ValuesQuant', + uselist=True, + back_populates='obj_desc_quant', + viewonly=True, + cascade='all, delete-orphan', + ) + + +class ValuesCat(Base): + __tablename__ = 'values_cat' + __table_args__ = ( + CheckConstraint( + 'value_open IS NOT NULL OR value_controlled IS NOT NULL', + name='constraint_values_cat_some_value', + ), + ForeignKeyConstraint( + ['desc_cat'], + ['descriptors_cat.id'], + name='values_cat_desc_cat_fkey', + ), + ForeignKeyConstraint( + ['desc_inst'], + ['descriptors_inst.id'], + name='values_cat_desc_inst_fkey', + ), + ForeignKeyConstraint(['instance'], ['values_inst.id'], name='values_cat_instance_fkey'), + ForeignKeyConstraint( + ['object', 'desc_cat'], + ['obj_desc_cat.object', 'obj_desc_cat.desc_cat'], + name='values_cat_object_desc_cat_fkey', + ), + ForeignKeyConstraint( + ['object', 'desc_inst'], + ['obj_desc_inst.object', 'obj_desc_inst.desc_inst'], + name='values_cat_object_desc_inst_fkey', + ), + ForeignKeyConstraint(['object'], ['objects.id'], name='values_cat_object_fkey'), + ForeignKeyConstraint( + ['value_controlled'], + ['controlled_terms.id'], + name='values_cat_value_controlled_fkey', + ), + PrimaryKeyConstraint('id', name='values_cat_pkey'), + Index('idx_values_cat_desc_cat', 'desc_cat'), + Index('idx_values_cat_desc_inst', 'desc_inst'), + Index('idx_values_cat_instance', 'instance'), + Index('idx_values_cat_object', 'object'), + Index('idx_values_cat_value_controlled', 'value_controlled'), + ) + + id = mapped_column( + Integer, + Identity( + start=1, + increment=1, + minvalue=1, + maxvalue=2147483647, + cycle=False, + cache=1, + ), + ) + object = mapped_column(Uuid, nullable=False) + desc_inst = mapped_column(Integer, nullable=False) + desc_cat = mapped_column(Integer, nullable=False) + value_open = mapped_column(Text) + value_controlled = mapped_column(Integer) + instance = mapped_column(Integer) + + descriptors_cat: Mapped['DescriptorsCat'] = relationship('DescriptorsCat', back_populates='values_cat') + descriptors_inst: Mapped['DescriptorsInst'] = relationship('DescriptorsInst', back_populates='values_cat') + values_inst: Mapped[Optional['ValuesInst']] = relationship('ValuesInst', back_populates='values_cat') + obj_desc_cat: Mapped['ObjDescCat'] = relationship('ObjDescCat', back_populates='values_cat') + obj_desc_inst: Mapped['ObjDescInst'] = relationship('ObjDescInst', back_populates='values_cat') + objects: Mapped['Objects'] = relationship('Objects', back_populates='values_cat') + controlled_terms: Mapped[Optional['ControlledTerms']] = relationship('ControlledTerms', back_populates='values_cat') + + +class ValuesQuant(Base): + __tablename__ = 'values_quant' + __table_args__ = ( + ForeignKeyConstraint( + ['desc_inst'], + ['descriptors_inst.id'], + name='values_quant_desc_inst_fkey', + ), + ForeignKeyConstraint( + ['desc_quant'], + ['descriptors_quant.id'], + name='values_quant_desc_quant_fkey', + ), + ForeignKeyConstraint(['instance'], ['values_inst.id'], name='values_quant_instance_fkey'), + ForeignKeyConstraint( + ['object', 'desc_inst'], + ['obj_desc_inst.object', 'obj_desc_inst.desc_inst'], + name='values_quant_object_desc_inst_fkey', + ), + ForeignKeyConstraint( + ['object', 'desc_quant'], + ['obj_desc_quant.object', 'obj_desc_quant.desc_quant'], + name='values_quant_object_desc_quant_fkey', + ), + ForeignKeyConstraint(['object'], ['objects.id'], name='values_quant_object_fkey'), + PrimaryKeyConstraint('id', name='values_quant_pkey'), + Index('idx_values_quant_desc_inst', 'desc_inst'), + Index('idx_values_quant_desc_quant', 'desc_quant'), + Index('idx_values_quant_instance', 'instance'), + Index('idx_values_quant_object', 'object'), + ) + + id = mapped_column( + Integer, + Identity( + start=1, + increment=1, + minvalue=1, + maxvalue=2147483647, + cycle=False, + cache=1, + ), + ) + value = mapped_column(Numeric, nullable=False) + object = mapped_column( + Uuid, ForeignKey('objects.id'), nullable=False + ) # TODO: object is not obj id, but File Obj ID that | should be same + desc_inst = mapped_column(Integer, nullable=False) + desc_quant = mapped_column(Integer, nullable=False) + value_blob = mapped_column(JSONB, nullable=False) + instance = mapped_column(Integer) + orig_value = mapped_column(String) + orig_units = mapped_column(String) + + descriptors_inst: Mapped['DescriptorsInst'] = relationship( + 'DescriptorsInst', + back_populates='values_quant', + viewonly=True, + cascade='all, delete-orphan', + ) + descriptors_quant: Mapped['DescriptorsQuant'] = relationship( + 'DescriptorsQuant', + back_populates='values_quant', + viewonly=True, + cascade='all, delete-orphan', + ) + values_inst: Mapped[Optional['ValuesInst']] = relationship( + 'ValuesInst', + back_populates='values_quant', + viewonly=True, + cascade='all, delete-orphan', + ) + obj_desc_inst: Mapped['ObjDescInst'] = relationship( + 'ObjDescInst', + back_populates='values_quant', + viewonly=True, + cascade='all, delete-orphan', + # sync_backref=False, + ) + obj_desc_quant: Mapped['ObjDescQuant'] = relationship( + 'ObjDescQuant', + back_populates='values_quant', + viewonly=True, + cascade='all, delete-orphan', + # sync_backref=False, + ) + objects: Mapped['Objects'] = relationship( + 'Objects', + back_populates='values_quant', + viewonly=True, + cascade='all, delete-orphan', + ) + + @staticmethod + def update_desc_inst(target: 'ValuesQuant', value, oldvalue, initiator) -> None: + """ + Automatically update desc_inst when descriptors_inst parent is updated. + """ + if target.descriptors_inst: + target.desc_inst = target.descriptors_inst.id + + @staticmethod + def register_listeners() -> None: + """ + Register event listeners for the ValuesQuant model. + """ + event.listen(ValuesQuant.descriptors_inst, 'set', ValuesQuant.update_desc_inst) + + +# @event.listens_for(ValuesQuant.descriptors_inst, "set") +# def set_desc_inst_from_descriptor(target, value, oldvalue, initiator): +# print(target, value, oldvalue, initiator) +# if value is not None: +# target.desc_inst = value.id + + +def update_all_children(target, value, oldvalue, initiator): + # Loop through all relationships of the target (parent) object + for relationship_prop in target.__mapper__.relationships: + related_objects = getattr(target, relationship_prop.key) + + # If there are related objects, update their attributes + if related_objects is not None: + # Handle single and multiple relationships + related_objects = related_objects if isinstance(related_objects, list) else [related_objects] + for related_obj in related_objects: + # Update attributes on related objects as needed + # For demonstration, we'll update an attribute based on parent_value + if hasattr(related_obj, 'child_value'): + related_obj.child_value = f'Updated from parent_value: {value}' + if hasattr(related_obj, 'other_child_value'): + related_obj.other_child_value = f'Updated from parent_value: {value}' + + +# # Attach the listener to all attributes on the Parent model +# for column in ValuesQuant.__table__.columns: +# event.listen(getattr(ValuesQuant, column.name), "set", update_all_children) + + +# @event.listens_for(ValuesQuant.descriptors_inst, "set") +# def update_desc_inst(target, value, oldvalue, initiator): +# if value is not None: +# target.desc_inst = value.id + + +# Helper function to find the foreign key field associated with a relationship +def get_foreign_key_field(model, relationship_key): + """Get the local foreign key field name for a given relationship key in the model.""" + for constraint in model.__table__.constraints: + if isinstance(constraint, ForeignKeyConstraint): + for element in constraint.elements: + # Match the local column to the relationship key + if element.column.table.name == relationship_key: + return element.parent.name + return None + + +# General listener function for updating foreign keys dynamically +def update_foreign_key(target, value, oldvalue, initiator): + if value is not None: + # Use the initiator key to find the appropriate foreign key field + relationship_key = initiator.key # Use initiator.key to get the relationship name + foreign_key_field = get_foreign_key_field(target.__class__, relationship_key) + + # Set the foreign key field if it exists on the target + if foreign_key_field and hasattr(target, foreign_key_field): + setattr(target, foreign_key_field, value.id) + + +# Function to attach the listener to all relationships in a given model +def add_dynamic_listeners(base): + for cls in base.__subclasses__(): + for relationship_prop in cls.__mapper__.relationships: + event.listen(getattr(cls, relationship_prop.key), 'set', update_foreign_key) + + +# Attach listeners to all models in the Base +add_dynamic_listeners(Base) diff --git a/quantdb/postgresql_app/__init__.py b/quantdb/postgresql_app/__init__.py deleted file mode 100644 index e73dd2a..0000000 --- a/quantdb/postgresql_app/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from __future__ import annotations - -from . import database - -__all__ = ['database'] diff --git a/quantdb/postgresql_app/database.py b/quantdb/postgresql_app/database.py deleted file mode 100644 index 411d706..0000000 --- a/quantdb/postgresql_app/database.py +++ /dev/null @@ -1,17 +0,0 @@ -from __future__ import annotations - -from typing import Any - -from sqlalchemy import create_engine -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import sessionmaker - -from quantdb.config import Settings - -# TODO: get new setting env setup first -# SQLALCHEMY_DATABASE_URL = Settings().MYSQL_URL - -# engine = create_engine(SQLALCHEMY_DATABASE_URL) -# SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) - -# Base: Any = declarative_base() diff --git a/test/__init__.py b/tests/__init__.py similarity index 100% rename from test/__init__.py rename to tests/__init__.py diff --git a/test/test_api.py b/tests/test_api.py similarity index 100% rename from test/test_api.py rename to tests/test_api.py diff --git a/tests/test_generic_ingest.py b/tests/test_generic_ingest.py new file mode 100644 index 0000000..b36e2b4 --- /dev/null +++ b/tests/test_generic_ingest.py @@ -0,0 +1,97 @@ +import pytest + +from quantdb.client import get_session +from quantdb.generic_ingest import ( + back_populate_tables, + get_constraint_columns, + get_or_create, + object_as_dict, + query_by_constraints, +) +from quantdb.models import Objects + + +def setup_test_session(): + """ + Returns a SQLAlchemy session connected to the test PostgreSQL database. + The test DB is prepared by bin/prepare_test_db.sh and populated with the first 10 rows of each table. + """ + return get_session(echo=False, test=True) + + +def test_object_as_dict(): + obj = Objects(id='00000000-0000-0000-0000-000000000001', id_type='dataset', id_file=None, id_internal=None) + d = object_as_dict(obj) + assert d['id'] == '00000000-0000-0000-0000-000000000001' + assert d['id_type'] == 'dataset' + + +def test_get_or_create_creates_and_gets(): + session = setup_test_session() + obj = Objects(id='00000000-0000-0000-0000-000000000002', id_type='dataset', id_file=None, id_internal=None) + instance = get_or_create(session, obj) + assert getattr(instance, 'id', None) == '00000000-0000-0000-0000-000000000002' + # Should return the same instance if called again + instance2 = get_or_create(session, obj) + assert getattr(instance, 'id', None) == getattr(instance2, 'id', None) + + +def test_get_constraint_columns(): + cols = get_constraint_columns(Objects) + assert any('id' in col for col in cols) + + +def test_query_by_constraints(): + session = setup_test_session() + obj = Objects(id='00000000-0000-0000-0000-000000000003', id_type='dataset', id_file=None, id_internal=None) + session.add(obj) + session.commit() + # Should find the object by unique constraint + found = query_by_constraints( + session, Objects(id='00000000-0000-0000-0000-000000000003', id_type='dataset', id_file=None, id_internal=None) + ) + assert found is not None + assert getattr(found, 'id', None) == '00000000-0000-0000-0000-000000000003' + assert getattr(found, 'id_type', None) == 'dataset' + + +def test_back_populate_tables_adds_and_merges(): + session = setup_test_session() + obj = Objects(id='00000000-0000-0000-0000-000000000004', id_type='dataset', id_file=None, id_internal=None) + # Should add new + out = back_populate_tables(session, obj) + assert getattr(out, 'id', None) == '00000000-0000-0000-0000-000000000004' + # Should merge existing + obj2 = Objects(id='00000000-0000-0000-0000-000000000004', id_type='dataset', id_file=None, id_internal=None) + out2 = back_populate_tables(session, obj2) + assert getattr(out2, 'id', None) == getattr(out, 'id', None) + + +def test_get_or_create_back_populate(): + session = setup_test_session() + obj = Objects(id='00000000-0000-0000-0000-000000000005', id_type='dataset', id_file=None, id_internal=None) + instance = get_or_create(session, obj, back_populate={'id_file': 12345}) + assert getattr(instance, 'id_file', None) == 12345 + + +def test_print_first_row_of_each_entity(): + """ + Prints the first row of each mapped entity in the test database. + """ + import inspect as pyinspect + + session = setup_test_session() + from quantdb import models + + printed = False + for name, cls in vars(models).items(): + if pyinspect.isclass(cls) and hasattr(cls, '__table__') and hasattr(cls, '__mapper__'): + try: + row = session.query(cls).first() + if row: + print(f'First row for {name}: {row}') + printed = True + except Exception as e: + print(f'Could not query {name}: {e}') + if not printed: + print('No rows found in any entity.') From 0e126e58bfa1d212084d80fd26c19bbf0b1d8c58 Mon Sep 17 00:00:00 2001 From: Troy Sincomb Date: Mon, 12 May 2025 15:46:29 -0700 Subject: [PATCH 15/18] attempt at clone top 10 rows, failing --- README.org | 12 ++++++++++++ bin/prepare_test_db.sh | 41 +++++++++++++++++++++++++++++++++-------- pyproject.toml | 2 +- quantdb/client.py | 8 ++++++++ 4 files changed, 54 insertions(+), 9 deletions(-) diff --git a/README.org b/README.org index f4c2572..a156e4b 100644 --- a/README.org +++ b/README.org @@ -3,3 +3,15 @@ A database schema for storing arbitrary quantitative measurements. See the [[file:./docs/api.org][REST API documentation]] for details on the external query interface. + +* Installation +add the passwords to ~/.pgpass as +``` +host:port:*:quantdb-admin:password +host:port:*:quantdb-user:password +host:port:*:quantdb-test-admin:password +host:port:*:quantdb-test-user:password +``` + +* Notes +macOS will need the config.yaml in `Library/Application Support/quantdb/config.yaml` \ No newline at end of file diff --git a/bin/prepare_test_db.sh b/bin/prepare_test_db.sh index d585e5e..b526bbe 100644 --- a/bin/prepare_test_db.sh +++ b/bin/prepare_test_db.sh @@ -4,6 +4,12 @@ # Usage: ./bin/prepare_test_db.sh [MAIN_DB] [TEST_DB] [PGUSER] [PGHOST] [PGPORT] set -e +eval "$(conda shell.bash hook)" +conda activate quantdb + +# If you want to use the remote MAIN_DB connection info from client.py, extract it using Python +REMOTE_DB_URI=$(python -c "from quantdb.client import get_session; s = get_session(echo=False); print(str(s.get_bind().url))") +echo "Established Connection" MAIN_DB=${1:-quantdb} TEST_DB=${2:-quantdb_test} @@ -11,20 +17,39 @@ PGUSER=${3:-quantdb-test-admin} PGHOST=${4:-localhost} PGPORT=${5:-5432} +# Parse the remote DB URI for pg_dump +REMOTE_PGUSER=$(echo "$REMOTE_DB_URI" | sed -E 's|.*://([^:]+):.*|\1|') +REMOTE_PGPASSWORD=$(python -c "from quantdb.config import auth; print(auth.get('db-password') or '')") +REMOTE_PGHOST=$(echo "$REMOTE_DB_URI" | sed -E 's|.*@([^:/]+).*|\1|') +REMOTE_PGPORT=$(echo "$REMOTE_DB_URI" | sed -E 's|.*:([0-9]+)/.*|\1|') +REMOTE_DBNAME=$(echo "$REMOTE_DB_URI" | sed -E 's|.*/([^?]+).*|\1|') + +# Export password for pg_dump if available +if [ -n "$REMOTE_PGPASSWORD" ]; then + export PGPASSWORD="$REMOTE_PGPASSWORD" +fi + +# Ensure the PGUSER has a database (needed for psql connection) +psql -U $PGUSER -h $PGHOST -p $PGPORT -d postgres -tc "SELECT 1 FROM pg_database WHERE datname = '$PGUSER'" | grep -q 1 || createdb -U $PGUSER -h $PGHOST -p $PGPORT $PGUSER + # Drop and recreate the test database psql -U $PGUSER -h $PGHOST -p $PGPORT -c "DROP DATABASE IF EXISTS $TEST_DB;" psql -U $PGUSER -h $PGHOST -p $PGPORT -c "CREATE DATABASE $TEST_DB;" -# Dump schema only from main DB and restore to test DB -pg_dump -U $PGUSER -h $PGHOST -p $PGPORT -s $MAIN_DB | psql -U $PGUSER -h $PGHOST -p $PGPORT $TEST_DB +# Dump schema only from remote MAIN_DB and restore to local test DB +pg_dump -U $REMOTE_PGUSER -h $REMOTE_PGHOST -p $REMOTE_PGPORT -s $REMOTE_DBNAME | psql -U $PGUSER -h $PGHOST -p $PGPORT $TEST_DB + +# Get all table names from the remote MAIN_DB +TABLES=$(psql -U $REMOTE_PGUSER -h $REMOTE_PGHOST -p $REMOTE_PGPORT -d $REMOTE_DBNAME -Atc "SELECT tablename FROM pg_tables WHERE schemaname='public';") -# Get all table names from the main DB -TABLES=$(psql -U $PGUSER -h $PGHOST -p $PGPORT -d $MAIN_DB -Atc "SELECT tablename FROM pg_tables WHERE schemaname='public';") +echo "Tables in remote DB: $TABLES" -# For each table, copy the first 10 rows from main DB to test DB +# For each table, copy the first 10 rows from remote MAIN_DB to local test DB for TABLE in $TABLES; do - COLS=$(psql -U $PGUSER -h $PGHOST -p $PGPORT -d $MAIN_DB -Atc "SELECT string_agg('"' || column_name || '"', ',') FROM information_schema.columns WHERE table_name='$TABLE' AND table_schema='public';") - psql -U $PGUSER -h $PGHOST -p $PGPORT -d $TEST_DB -c "INSERT INTO \"$TABLE\" ($COLS) SELECT $COLS FROM dblink('dbname=$MAIN_DB','SELECT $COLS FROM \"$TABLE\" LIMIT 10') AS t($COLS);" 2>/dev/null || true + COLS=$(psql -U $REMOTE_PGUSER -h $REMOTE_PGHOST -p $REMOTE_PGPORT -d $REMOTE_DBNAME -Atc "SELECT string_agg('"' || column_name || '"', ',') FROM information_schema.columns WHERE table_name='$TABLE' AND table_schema='public';") + psql -U $PGUSER -h $PGHOST -p $PGPORT -d $TEST_DB -c "INSERT INTO \"$TABLE\" ($COLS) SELECT $COLS FROM dblink('host=$REMOTE_PGHOST user=$REMOTE_PGUSER dbname=$REMOTE_DBNAME port=$REMOTE_PGPORT','SELECT $COLS FROM \"$TABLE\" LIMIT 10') AS t($COLS);" 2>/dev/null || true done -echo "Test database '$TEST_DB' prepared with schema and first 10 rows of each table from '$MAIN_DB'." +unset PGPASSWORD + +echo "Test database '$TEST_DB' prepared with schema and first 10 rows of each table from remote '$REMOTE_DBNAME'." diff --git a/pyproject.toml b/pyproject.toml index d0e0f9b..d661228 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ version = "0.1.0" description = "Quantitative DB" dynamic = ["readme"] packages = [{ include = "quantdb" }] -requires-python = ">=3.10" +requires-python = ">=3.10, <=3.13" [tool.setuptools.dynamic] readme = {file = ["README.org"], content-type = "text/plain"} diff --git a/quantdb/client.py b/quantdb/client.py index 0909a5a..8cff20f 100644 --- a/quantdb/client.py +++ b/quantdb/client.py @@ -1,5 +1,6 @@ from sqlalchemy import create_engine from sqlalchemy.orm import Session +from sqlalchemy import text from quantdb.config import auth from quantdb.utils import dbUri @@ -27,7 +28,14 @@ def get_session(echo: bool = True, test: bool = False) -> Session: dbkwargs['dbuser'] = 'quantdb-test-admin' if test else dbkwargs['dbuser'] if test: dbkwargs['database'] = auth.get('test-db-database') or 'quantdb_test' + print(dbkwargs) engine = create_engine(dbUri(**dbkwargs)) # type: ignore engine.echo = echo + print(engine, dbkwargs) session = Session(engine) return session + + +if __name__ == '__main__': + session = get_session(echo=False) + print(session.execute(text("SELECT * FROM information_schema.tables")).fetchall()) \ No newline at end of file From f2444fb3d3edff8aebe58467a41ae8a537d12745 Mon Sep 17 00:00:00 2001 From: Troy Sincomb Date: Wed, 2 Jul 2025 15:17:41 -0700 Subject: [PATCH 16/18] test postgres beta pass --- quantdb/client.py | 20 ++++-- quantdb/utils.py | 30 ++++---- sql/database.sql | 6 +- sql/tables.sql | 10 +-- sql/test_database.sql | 6 +- test/README.md | 85 +++++++++++++++++++++++ test/__init__.py | 0 test/conftest.py | 135 ++++++++++++++++++++++++++++++++++++ test/pytest.ini | 13 ++++ test/test_api.py | 134 +++++++++++++++++++++++++++++++++++ test/test_database_setup.py | 99 ++++++++++++++++++++++++++ test/test_generic_ingest.py | 97 ++++++++++++++++++++++++++ 12 files changed, 607 insertions(+), 28 deletions(-) create mode 100644 test/README.md create mode 100644 test/__init__.py create mode 100644 test/conftest.py create mode 100644 test/pytest.ini create mode 100644 test/test_api.py create mode 100644 test/test_database_setup.py create mode 100644 test/test_generic_ingest.py diff --git a/quantdb/client.py b/quantdb/client.py index 8cff20f..2bab5ce 100644 --- a/quantdb/client.py +++ b/quantdb/client.py @@ -23,11 +23,19 @@ def get_session(echo: bool = True, test: bool = False) -> Session: The database session. """ # pull in the db connection info - dbkwargs = {k: auth.get(f'db-{k}') for k in ('user', 'host', 'port', 'database')} - dbkwargs['dbuser'] = dbkwargs.pop('user') - dbkwargs['dbuser'] = 'quantdb-test-admin' if test else dbkwargs['dbuser'] if test: - dbkwargs['database'] = auth.get('test-db-database') or 'quantdb_test' + # For testing, ALWAYS use local postgres instance regardless of main db config + dbkwargs = { + "dbuser": "quantdb-test-admin", + "host": "localhost", # FORCE localhost for all testing + "port": 5432, + "database": auth.get("test-db-database") or "quantdb_test", + "password": "tom-is-cool", # Use hardcoded password for test database + } + else: + # For non-test, use regular database configuration + dbkwargs = {k: auth.get(f"db-{k}") for k in ("user", "host", "port", "database")} + dbkwargs["dbuser"] = dbkwargs.pop("user") print(dbkwargs) engine = create_engine(dbUri(**dbkwargs)) # type: ignore engine.echo = echo @@ -36,6 +44,6 @@ def get_session(echo: bool = True, test: bool = False) -> Session: return session -if __name__ == '__main__': +if __name__ == "__main__": session = get_session(echo=False) - print(session.execute(text("SELECT * FROM information_schema.tables")).fetchall()) \ No newline at end of file + print(session.execute(text("SELECT * FROM information_schema.tables")).fetchall()) diff --git a/quantdb/utils.py b/quantdb/utils.py index 35069ec..e91aae9 100644 --- a/quantdb/utils.py +++ b/quantdb/utils.py @@ -12,39 +12,43 @@ def makeSimpleLogger(name, level=logging.INFO): logger.setLevel(level) ch = logging.StreamHandler() # FileHander goes to disk - fmt = '[%(asctime)s] - %(levelname)8s - ' '%(name)14s - ' '%(filename)16s:%(lineno)-4d - ' '%(message)s' + fmt = "[%(asctime)s] - %(levelname)8s - " "%(name)14s - " "%(filename)16s:%(lineno)-4d - " "%(message)s" formatter = logging.Formatter(fmt) ch.setFormatter(formatter) logger.addHandler(ch) return logger -log = makeSimpleLogger('quantdb') -logd = log.getChild('data') +log = makeSimpleLogger("quantdb") +logd = log.getChild("data") # from pyontutils.utils_fast import setPS1 def setPS1(script__file__): try: - text = 'Running ' + os.path.basename(script__file__) - os.sys.stdout.write('\x1b]2;{}\x07\n'.format(text)) + text = "Running " + os.path.basename(script__file__) + sys.stdout.write("\x1b]2;{}\x07\n".format(text)) except AttributeError as e: log.exception(e) -def dbUri(dbuser, host, port, database): - if hasattr(sys, 'pypy_version_info'): - dialect = 'psycopg2cffi' +def dbUri(dbuser, host, port, database, password=None): + if hasattr(sys, "pypy_version_info"): + dialect = "psycopg2cffi" else: - dialect = 'psycopg2' - return f'postgresql+{dialect}://{dbuser}@{host}:{port}/{database}' + dialect = "psycopg2" + + if password: + return f"postgresql+{dialect}://{dbuser}:{password}@{host}:{port}/{database}" + else: + return f"postgresql+{dialect}://{dbuser}@{host}:{port}/{database}" # from pyontutils.utils_fast import isoformat -def isoformat(datetime_instance, timespec='auto'): +def isoformat(datetime_instance, timespec="auto"): kwargs = {} if isinstance(datetime_instance, datetime): # don't pass timespec if type is not date not datetime - kwargs['timespec'] = timespec + kwargs["timespec"] = timespec - return datetime_instance.isoformat(**kwargs).replace('.', ',').replace('+00:00', 'Z') + return datetime_instance.isoformat(**kwargs).replace(".", ",").replace("+00:00", "Z") diff --git a/sql/database.sql b/sql/database.sql index 7f1634a..4d7608a 100644 --- a/sql/database.sql +++ b/sql/database.sql @@ -10,8 +10,10 @@ CREATE DATABASE :database -- quantdb WITH OWNER = 'quantdb-admin' ENCODING = 'UTF8' --TABLESPACE = pg_default -- leave this out for now since rds doesn't really support it - LC_COLLATE = 'en_US.UTF-8' -- this was a gentoo locale issue check ${LANG} - LC_CTYPE = 'en_US.UTF-8' + -- Use template0 to avoid collation conflicts, or inherit from template1 + TEMPLATE = template0 + LC_COLLATE = 'C' -- Use C collation for maximum compatibility + LC_CTYPE = 'C' CONNECTION LIMIT = -1; REVOKE "quantdb-admin" FROM CURRENT_USER; diff --git a/sql/tables.sql b/sql/tables.sql index b532801..58e4e1d 100644 --- a/sql/tables.sql +++ b/sql/tables.sql @@ -956,7 +956,7 @@ BEGIN RETURN QUERY WITH RECURSIVE tree(child, parents) AS ( SELECT parent, ARRAY[]::integer[] FROM -- start from nodes with no parents -(SELECT ap0.parent FROM aspect_parent AS ap0 EXCEPT SELECT ap1.id FROM aspect_parent AS ap1) +(SELECT ap0.parent FROM aspect_parent AS ap0 EXCEPT SELECT ap1.id FROM aspect_parent AS ap1) AS root_nodes UNION ALL SELECT ap.id, ap.parent || t.parents FROM aspect_parent AS ap @@ -1075,7 +1075,7 @@ BEGIN RETURN QUERY WITH RECURSIVE tree(child, parents) AS ( SELECT parent, ARRAY[]::integer[] FROM -- start from nodes with no parents -(SELECT cp0.parent FROM class_parent AS cp0 EXCEPT SELECT cp1.id FROM class_parent AS cp1) +(SELECT cp0.parent FROM class_parent AS cp0 EXCEPT SELECT cp1.id FROM class_parent AS cp1) AS root_nodes UNION ALL SELECT cp.id, cp.parent || t.parents FROM class_parent AS cp @@ -1170,7 +1170,7 @@ BEGIN RETURN QUERY WITH RECURSIVE tree(child, parents) AS ( SELECT parent, ARRAY[]::integer[] FROM -- start from nodes with no parents -(SELECT ip0.parent FROM instance_parent AS ip0 EXCEPT SELECT ip1.id FROM instance_parent AS ip1) +(SELECT ip0.parent FROM instance_parent AS ip0 EXCEPT SELECT ip1.id FROM instance_parent AS ip1) AS root_nodes UNION ALL SELECT ip.id, ip.parent || t.parents FROM instance_parent AS ip @@ -1205,8 +1205,8 @@ WHERE im.id = NEW.id) SELECT -- FIXME surely there is a more efficient way to do this (((SELECT count(*) FROM samples) > 0 OR (SELECT count(*) from subjects) > 0) AND (SELECT count(*) FROM parents) = 0) AS one, ( (SELECT count(*) FROM samples) = 0 AND (SELECT count(*) from subjects) = 0 AND (SELECT count(*) FROM parents) > 0) AS two, -( (SELECT count(*) FROM (SELECT * FROM parents INTERSECT SELECT * FROM subjects)) = (SELECT count(*) FROM subjects) -AND (SELECT count(*) FROM (SELECT * FROM parents INTERSECT SELECT * FROM samples)) = (SELECT count(*) FROM samples)) AS three +( (SELECT count(*) FROM (SELECT * FROM parents INTERSECT SELECT * FROM subjects) AS parent_subject_intersect) = (SELECT count(*) FROM subjects) +AND (SELECT count(*) FROM (SELECT * FROM parents INTERSECT SELECT * FROM samples) AS parent_sample_intersect) = (SELECT count(*) FROM samples)) AS three /* -- we cannot check against parents in this last case because it can contain intermediates therefore we can only check parents contain all referenced subjects and samples, going the other way around we know we will be missing any intervening samples, but those will have been checked when they were inserted AND (SELECT count(*) FROM (SELECT * FROM parents INTERSECT SELECT * FROM (SELECT * FROM subjects UNION SELECT * FROM samples))) = (SELECT count(*) FROM parents)) diff --git a/sql/test_database.sql b/sql/test_database.sql index 1bb2fc3..dbb1cae 100644 --- a/sql/test_database.sql +++ b/sql/test_database.sql @@ -14,8 +14,10 @@ CREATE DATABASE :test_database -- quantdb WITH OWNER = 'quantdb-test-admin' ENCODING = 'UTF8' --TABLESPACE = pg_default -- leave this out for now since rds doesn't really support it - LC_COLLATE = 'en_US.UTF-8' -- this was a gentoo locale issue check ${LANG} - LC_CTYPE = 'en_US.UTF-8' + -- Use template0 to avoid collation conflicts, or inherit from template1 + TEMPLATE = template0 + LC_COLLATE = 'C' -- Use C collation for maximum compatibility + LC_CTYPE = 'C' CONNECTION LIMIT = -1; REVOKE "quantdb-test-admin" FROM CURRENT_USER; diff --git a/test/README.md b/test/README.md new file mode 100644 index 0000000..65d1815 --- /dev/null +++ b/test/README.md @@ -0,0 +1,85 @@ +# Test Database Setup + +This directory contains pytest setup for creating and verifying a local test PostgreSQL database using the `dbsetup` script. + +## Purpose + +This test setup is focused **solely** on: +1. Creating a test PostgreSQL database using the `bin/dbsetup` script +2. Verifying that the expected tables were created +3. Ensuring basic database connectivity works + +**No other functionality is tested** - this is just for basic database setup verification. + +## Prerequisites + +1. **PostgreSQL server** must be running locally on port 5432 (or configured port) +2. **postgres superuser** must be available for database creation +3. **Python dependencies** must be installed (see `pyproject.toml`) + +## Configuration + +The test database configuration is **hardcoded** to always use a local PostgreSQL instance: +- Database: `quantdb_test` (can be overridden by `QUANTDB_TEST_DB_DATABASE`) +- Host: `localhost` (**ALWAYS localhost for testing - ignores all other config**) +- Port: `5432` (**ALWAYS 5432 for testing**) +- User: `quantdb-test-admin` (**ALWAYS this user for testing**) +- Password: `tom-is-cool` (**hardcoded for test database**) + +**Important**: Testing will NEVER use remote databases (like AWS RDS). All test database connections are forced to use localhost regardless of environment variables or configuration files. + +**Note**: Tables are created in the `quantdb` schema, not the `public` schema. The database users are configured with `search_path = quantdb, public` which makes `quantdb` the default schema for table creation. + +## Running Tests + +### Run all database setup tests: +```bash +pytest test/test_database_setup.py -v +``` + +### Run with more detailed output: +```bash +pytest test/test_database_setup.py -v -s +``` + +### Run only the table creation test: +```bash +pytest test/test_database_setup.py::TestDatabaseSetup::test_tables_created -v +``` + +## What the Tests Do + +1. **`test_database_connection`**: Verifies basic database connectivity +2. **`test_tables_created`**: Checks that core tables (objects, units, aspects, etc.) exist in the `quantdb` schema +3. **`test_database_schema_info`**: Queries schema information from both `quantdb` and `public` schemas +4. **`test_database_functions_created`**: Verifies custom functions were created in the `quantdb` schema +5. **`test_database_enums_created`**: Checks that custom enum types exist + +## Test Flow + +1. The `setup_test_database` fixture runs the `bin/dbsetup` script +2. The `test_session` fixture creates a database connection +3. The `verify_database_tables` fixture queries and lists all tables +4. Individual tests verify different aspects of the database setup + +## Troubleshooting + +- **Connection errors**: Check that PostgreSQL is running and accessible +- **Permission errors**: Ensure the postgres user has proper permissions +- **Script errors**: Check that `bin/dbsetup` is executable and all SQL files exist +- **Timeout errors**: Database creation is taking longer than 2 minutes + +## Expected Tables + +The tests verify these core tables exist: +- `objects` - Main object table +- `units` - Unit definitions +- `aspects` - Aspect definitions +- `descriptors_inst` - Instance descriptors +- `descriptors_cat` - Categorical descriptors +- `descriptors_quant` - Quantitative descriptors +- `values_inst` - Instance values +- `values_cat` - Categorical values +- `values_quant` - Quantitative values +- `addresses` - Address mappings +- `controlled_terms` - Controlled vocabulary \ No newline at end of file diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/conftest.py b/test/conftest.py new file mode 100644 index 0000000..4f7324b --- /dev/null +++ b/test/conftest.py @@ -0,0 +1,135 @@ +import subprocess +import pytest +import os +import sys +from pathlib import Path +from sqlalchemy import create_engine, text +from sqlalchemy.exc import OperationalError + +from quantdb.client import get_session +from quantdb.config import auth + + +@pytest.fixture(scope="session") +def project_root(): + """Get the project root directory.""" + return Path(__file__).parent.parent + + +@pytest.fixture(scope="session") +def test_database_config(): + """Get test database configuration.""" + config = { + "user": "quantdb-test-admin", + "host": "localhost", # ALWAYS use localhost for testing, never remote + "port": 5432, + "database": auth.get("test-db-database") or "quantdb_test", + } + return config + + +@pytest.fixture(scope="session") +def setup_test_database(project_root, test_database_config): + """ + Set up the test database using the dbsetup script. + This fixture runs once per test session. + """ + print(f"\n=== Setting up test database ===") + print(f"Database: {test_database_config['database']}") + print(f"Host: {test_database_config['host']}") + print(f"Port: {test_database_config['port']}") + print(f"User: {test_database_config['user']}") + + # Path to dbsetup script + dbsetup_script = project_root / "bin" / "dbsetup" + + if not dbsetup_script.exists(): + pytest.fail(f"dbsetup script not found at: {dbsetup_script}") + + # Make sure the script is executable + os.chmod(dbsetup_script, 0o755) + + try: + # Set up environment with hardcoded test password + test_env = os.environ.copy() + test_env["PGPASSWORD"] = "tom-is-cool" # Hardcoded test password + + # Run dbsetup with test database parameters + cmd = [str(dbsetup_script), str(test_database_config["port"]), test_database_config["database"], test_database_config["host"]] + + print(f"Running command: {' '.join(cmd)}") + print(f"Using hardcoded test password: tom-is-cool") + + # Run the dbsetup script with the password environment variable + result = subprocess.run(cmd, cwd=project_root, capture_output=True, text=True, timeout=120, env=test_env) # 2 minute timeout + + if result.returncode != 0: + print(f"STDOUT: {result.stdout}") + print(f"STDERR: {result.stderr}") + pytest.fail(f"dbsetup failed with return code {result.returncode}") + + print(f"dbsetup completed successfully") + print(f"STDOUT: {result.stdout}") + + # Return the configuration for use by other fixtures + return test_database_config + + except subprocess.TimeoutExpired: + pytest.fail("dbsetup script timed out after 2 minutes") + except Exception as e: + pytest.fail(f"Failed to run dbsetup: {e}") + + +@pytest.fixture(scope="session") +def test_session(setup_test_database): + """ + Get a SQLAlchemy session connected to the test database. + Depends on setup_test_database to ensure the database is created first. + """ + try: + session = get_session(echo=False, test=True) + yield session + session.close() + except Exception as e: + pytest.fail(f"Failed to create test database session: {e}") + + +@pytest.fixture(scope="session") +def verify_database_tables(test_session): + """ + Verify that the expected database tables were created. + Returns a list of table names found in the database. + """ + try: + # Query for all tables in the quantdb and public schemas + result = test_session.execute( + text( + """ + SELECT table_schema, table_name + FROM information_schema.tables + WHERE table_schema IN ('quantdb', 'public') + ORDER BY table_schema, table_name + """ + ) + ) + + rows = result.fetchall() + tables_by_schema = {} + all_tables = [] + + for schema, table in rows: + if schema not in tables_by_schema: + tables_by_schema[schema] = [] + tables_by_schema[schema].append(table) + all_tables.append(table) + + print(f"\n=== Found {len(all_tables)} tables in test database ===") + for schema, tables in tables_by_schema.items(): + print(f"Schema '{schema}': {len(tables)} tables") + for table in tables: + print(f" - {table}") + + return all_tables + + except Exception as e: + pytest.fail(f"Failed to query database tables: {e}") diff --git a/test/pytest.ini b/test/pytest.ini new file mode 100644 index 0000000..c578cfc --- /dev/null +++ b/test/pytest.ini @@ -0,0 +1,13 @@ +[tool:pytest] +testpaths = test +python_files = test_*.py +python_classes = Test* +python_functions = test_* +addopts = + -v + --tb=short + --strict-markers + --disable-warnings +markers = + database: marks tests as database tests (deselect with '-m "not database"') + slow: marks tests as slow (deselect with '-m "not slow"') \ No newline at end of file diff --git a/test/test_api.py b/test/test_api.py new file mode 100644 index 0000000..e189822 --- /dev/null +++ b/test/test_api.py @@ -0,0 +1,134 @@ +import json +import pprint + +from flask_sqlalchemy import SQLAlchemy + +from quantdb.api import make_app +from quantdb.utils import log + + +def test(): + db = SQLAlchemy() + app = make_app(db=db, dev=True) + client = app.test_client() + runner = app.test_cli_runner() + + dataset_uuid = 'aa43eda8-b29a-4c25-9840-ecbd57598afc' + some_object = '414886a9-9ec7-447e-b4d8-3ae42fda93b7' # XXX FAKE + actual_package_uuid = '15bcbcd5-b054-40ef-9b5c-6a260d441621' + base = 'http://localhost:8989/api/1/' + urls = ( + f'{base}values/inst', + f'{base}values/inst?dataset={dataset_uuid}', + f'{base}values/inst?dataset={dataset_uuid}&union-cat-quant=true', + f'{base}values/inst?dataset={dataset_uuid}&aspect=distance&aspect=time', + f'{base}values/inst?dataset={dataset_uuid}&aspect=distance&value-quant-min=0.5', + f'{base}values/inst?dataset={dataset_uuid}&inst-parent=sub-f001', + f'{base}values/inst?dataset={dataset_uuid}&inst-parent=sam-r-seg-c1&inst-parent=sam-l-seg-c1', + f'{base}values/inst?desc-inst=nerve-volume', + f'{base}objects?dataset={dataset_uuid}', + f'{base}objects?dataset={dataset_uuid}&aspect=distance', + f'{base}objects?dataset={dataset_uuid}&aspect=distance&value-quant-min=0.5', # expect nothing + f'{base}objects?dataset={dataset_uuid}&aspect=distance&value-quant-min=0.5&union-cat-quant=true', + f'{base}objects?dataset={dataset_uuid}&subject=sub-f001', + f'{base}objects?subject=sub-f001', + f'{base}objects?subject=sub-f001&union-cat-quant=true', + f'{base}objects?subject=sub-f001&subject=sub-f002&subject=sub-f003&subject=sub-f004&subject=sub-f005', + f'{base}objects?subject=sub-f001&subject=sub-f002&subject=sub-f003&subject=sub-f004&subject=sub-f005&union-cat-quant=true', + f'{base}objects?subject=sub-f001&desc-cat=none&value-quant-min=0.5&union-cat-quant=true', + f'{base}objects?subject=sub-f001&desc-cat=none&aspect=distance&value-quant-min=0.5&union-cat-quant=true', + f'{base}objects?subject=sub-f001&aspect=distance&value-quant-min=0.5&union-cat-quant=true', + f'{base}objects?aspect=distance&value-quant-min=0.5&union-cat-quant=true', + f'{base}objects?desc-cat=none&aspect=distance&value-quant-min=0.5&union-cat-quant=true', + f'{base}objects?desc-cat=none&aspect=distance&value-quant-min=0.5', + f'{base}objects?aspect=distance&value-quant-min=0.5', + f'{base}objects?aspect=distance&value-quant-min=0.5&source-only=true', + f'{base}objects?desc-inst=nerve-volume&aspect=distance&value-quant-min=0.5&source-only=true', + # values-quant + f'{base}values/quant?dataset={dataset_uuid}&aspect=distance', + f'{base}values/quant?object={actual_package_uuid}&aspect=distance', + f'{base}values/quant?aspect=distance', + f'{base}values/quant?aspect=distance-via-reva-ft-sample-id-normalized-v1', + f'{base}values/quant?aspect=distance-via-reva-ft-sample-id-normalized-v1&agg-type=instance', + f'{base}values/quant?aspect=distance-via-reva-ft-sample-id-normalized-v1&value-quant-min=0.4&value-quant-max=0.7', + # values-cat + f'{base}values/cat?object={actual_package_uuid}', + f'{base}values/cat?object={actual_package_uuid}&union-cat-quant=true', # shouldn't need it in this case + f'{base}values/cat-quant?object={actual_package_uuid}', + f'{base}values/cat-quant?object={actual_package_uuid}&union-cat-quant=true', + # values-cat-quant + f'{base}values?dataset={dataset_uuid}&aspect=distance&value-quant-min=0.5', + f'{base}values?dataset={dataset_uuid}&aspect=distance&value-quant-min=0.5&union-cat-quant=true', + f'{base}values?object={actual_package_uuid}', + f'{base}values?object={actual_package_uuid}&union-cat-quant=true', + f'{base}values/inst?object={actual_package_uuid}', + f'{base}values/inst?object={actual_package_uuid}&union-cat-quant=true', + # prov + f'{base}values/inst?prov=true', + f'{base}values/quant?aspect=distance&prov=true', + f'{base}values/cat?object={actual_package_uuid}', + f'{base}values/cat?object={actual_package_uuid}&prov=true', # FIXME somehow this has a 3x increase in records, and non-distinct + f'{base}values/cat-quant?object={actual_package_uuid}&union-cat-quant=true', + f'{base}values/cat-quant?object={actual_package_uuid}&union-cat-quant=true&prov=true', + f'{base}values/cat-quant', + f'{base}values/cat-quant?prov=true', + f'{base}values/cat-quant?union-cat-quant=true', + f'{base}values/cat-quant?union-cat-quant=true&prov=true', + # desc + f'{base}desc/inst', + f'{base}desc/cat', + f'{base}desc/quant', + f'{base}desc/inst?include-unused=true', + f'{base}desc/cat?include-unused=true', + f'{base}desc/quant?include-unused=true', + # descriptor values + f'{base}terms', + f'{base}aspects', + f'{base}units', + f'{base}terms?include-unused=true', + f'{base}aspects?include-unused=true', + f'{base}units?include-unused=true', + # TODO maybe shapes here as well? + f'{base}terms?inst-parent=sam-r-seg-c1&inst-parent=sam-l-seg-c1', + f'{base}aspects?inst-parent=sam-r-seg-c1&inst-parent=sam-l-seg-c1', + f'{base}units?inst-parent=sam-r-seg-c1&inst-parent=sam-l-seg-c1', + ) + # log.setLevel(9) + resps = [] + for url in urls: + log.debug(url) + resp = client.get(url) + resp.ok = resp.status_code < 400 + resp.url = resp.request.url + resp.content = resp.data + resps.append(json.loads(resp.data.decode())) + + pprint.pprint(resps, width=120) + # (i := 6, resps[i], urls[i]) + # q = client.get(f'{base}values/quant?dataset={dataset_uuid}&aspect=distance&return-query=true').data.decode() + # q = client.get(f'{base}values/cat?object={actual_package_uuid}&prov=true&return-query=true').data.decode() + # print(q) + breakpoint() + + +def test_demo_load(): + db = SQLAlchemy() + app = make_app(db=db, dev=True) + client = app.test_client() + runner = app.test_cli_runner() + + dataset_uuid = '55c5b69c-a5b8-4881-a105-e4048af26fa5' + package_uuid = '20720c2e-83fb-4454-bef1-1ce6a97fa748' + base = 'http://localhost:8989/api/1/' + urls = (f'{base}values/cat-quant?desc-inst=fascicle-cross-section',) + + resps = [] + for url in urls: + log.debug(url) + resp = client.get(url) + resp.ok = resp.status_code < 400 + resp.url = resp.request.url + resp.content = resp.data + resps.append(json.loads(resp.data.decode())) + + breakpoint() diff --git a/test/test_database_setup.py b/test/test_database_setup.py new file mode 100644 index 0000000..02bb8cf --- /dev/null +++ b/test/test_database_setup.py @@ -0,0 +1,99 @@ +""" +Test database setup and table creation. + +This test module focuses solely on verifying that: +1. The test database can be created using the dbsetup script +2. The expected tables are present in the database +3. The database connection works properly + +No other functionality is tested - this is just for basic database setup verification. +""" + +import pytest +from sqlalchemy import text + + +class TestDatabaseSetup: + """Test the basic database setup and table creation.""" + + def test_database_connection(self, test_session): + """Test that we can connect to the test database.""" + # Try a simple query to verify the connection works + result = test_session.execute(text("SELECT 1 as test_value")) + row = result.fetchone() + assert row[0] == 1, "Basic database connection failed" + + def test_tables_created(self, verify_database_tables): + """Test that the expected core tables were created.""" + tables = verify_database_tables + + # Verify we have tables (the list should not be empty) + assert len(tables) > 0, "No tables found in the database" + + # Check for some core tables that should exist based on sql/tables.sql + expected_core_tables = ["objects", "units", "aspects", "descriptors_inst", "descriptors_cat", "descriptors_quant", "values_inst", "values_cat", "values_quant", "addresses", "controlled_terms"] + + missing_tables = [] + for expected_table in expected_core_tables: + if expected_table not in tables: + missing_tables.append(expected_table) + + assert len(missing_tables) == 0, f"Missing expected tables: {missing_tables}" + + print(f"✓ All {len(expected_core_tables)} core tables found in database") + + def test_database_schema_info(self, test_session): + """Test that we can query basic schema information.""" + # Check that we can query table information in both schemas + result = test_session.execute( + text( + """ + SELECT COUNT(*) as table_count + FROM information_schema.tables + WHERE table_schema IN ('quantdb', 'public') + """ + ) + ) + + table_count = result.fetchone()[0] + assert table_count > 0, "No tables found in quantdb or public schemas" + + print(f"✓ Found {table_count} tables in quantdb and public schemas") + + def test_database_functions_created(self, test_session): + """Test that database functions were created.""" + # Check for some of the custom functions from tables.sql in both schemas + result = test_session.execute( + text( + """ + SELECT COUNT(*) as function_count + FROM information_schema.routines + WHERE routine_schema IN ('quantdb', 'public') + AND routine_type = 'FUNCTION' + """ + ) + ) + + function_count = result.fetchone()[0] + # We expect several functions to be created based on tables.sql + assert function_count > 0, "No custom functions found in database" + + print(f"✓ Found {function_count} custom functions in database") + + def test_database_enums_created(self, test_session): + """Test that custom enum types were created.""" + result = test_session.execute( + text( + """ + SELECT COUNT(*) as enum_count + FROM pg_type + WHERE typtype = 'e' + """ + ) + ) + + enum_count = result.fetchone()[0] + # We expect several enum types based on tables.sql (remote_id_type, instance_type, etc.) + assert enum_count > 0, "No custom enum types found in database" + + print(f"✓ Found {enum_count} custom enum types in database") diff --git a/test/test_generic_ingest.py b/test/test_generic_ingest.py new file mode 100644 index 0000000..b36e2b4 --- /dev/null +++ b/test/test_generic_ingest.py @@ -0,0 +1,97 @@ +import pytest + +from quantdb.client import get_session +from quantdb.generic_ingest import ( + back_populate_tables, + get_constraint_columns, + get_or_create, + object_as_dict, + query_by_constraints, +) +from quantdb.models import Objects + + +def setup_test_session(): + """ + Returns a SQLAlchemy session connected to the test PostgreSQL database. + The test DB is prepared by bin/prepare_test_db.sh and populated with the first 10 rows of each table. + """ + return get_session(echo=False, test=True) + + +def test_object_as_dict(): + obj = Objects(id='00000000-0000-0000-0000-000000000001', id_type='dataset', id_file=None, id_internal=None) + d = object_as_dict(obj) + assert d['id'] == '00000000-0000-0000-0000-000000000001' + assert d['id_type'] == 'dataset' + + +def test_get_or_create_creates_and_gets(): + session = setup_test_session() + obj = Objects(id='00000000-0000-0000-0000-000000000002', id_type='dataset', id_file=None, id_internal=None) + instance = get_or_create(session, obj) + assert getattr(instance, 'id', None) == '00000000-0000-0000-0000-000000000002' + # Should return the same instance if called again + instance2 = get_or_create(session, obj) + assert getattr(instance, 'id', None) == getattr(instance2, 'id', None) + + +def test_get_constraint_columns(): + cols = get_constraint_columns(Objects) + assert any('id' in col for col in cols) + + +def test_query_by_constraints(): + session = setup_test_session() + obj = Objects(id='00000000-0000-0000-0000-000000000003', id_type='dataset', id_file=None, id_internal=None) + session.add(obj) + session.commit() + # Should find the object by unique constraint + found = query_by_constraints( + session, Objects(id='00000000-0000-0000-0000-000000000003', id_type='dataset', id_file=None, id_internal=None) + ) + assert found is not None + assert getattr(found, 'id', None) == '00000000-0000-0000-0000-000000000003' + assert getattr(found, 'id_type', None) == 'dataset' + + +def test_back_populate_tables_adds_and_merges(): + session = setup_test_session() + obj = Objects(id='00000000-0000-0000-0000-000000000004', id_type='dataset', id_file=None, id_internal=None) + # Should add new + out = back_populate_tables(session, obj) + assert getattr(out, 'id', None) == '00000000-0000-0000-0000-000000000004' + # Should merge existing + obj2 = Objects(id='00000000-0000-0000-0000-000000000004', id_type='dataset', id_file=None, id_internal=None) + out2 = back_populate_tables(session, obj2) + assert getattr(out2, 'id', None) == getattr(out, 'id', None) + + +def test_get_or_create_back_populate(): + session = setup_test_session() + obj = Objects(id='00000000-0000-0000-0000-000000000005', id_type='dataset', id_file=None, id_internal=None) + instance = get_or_create(session, obj, back_populate={'id_file': 12345}) + assert getattr(instance, 'id_file', None) == 12345 + + +def test_print_first_row_of_each_entity(): + """ + Prints the first row of each mapped entity in the test database. + """ + import inspect as pyinspect + + session = setup_test_session() + from quantdb import models + + printed = False + for name, cls in vars(models).items(): + if pyinspect.isclass(cls) and hasattr(cls, '__table__') and hasattr(cls, '__mapper__'): + try: + row = session.query(cls).first() + if row: + print(f'First row for {name}: {row}') + printed = True + except Exception as e: + print(f'Could not query {name}: {e}') + if not printed: + print('No rows found in any entity.') From afe15f453ccdb00f87bbf5f7ae5ef960150466d2 Mon Sep 17 00:00:00 2001 From: Troy Sincomb Date: Mon, 7 Jul 2025 12:55:49 -0700 Subject: [PATCH 17/18] test postgres alpha pass --- .gitignore | 2 +- README.org | 2 +- pyproject.toml | 1 + quantdb/client.py | 21 +++--- quantdb/generic_ingest.py | 55 +++++++++++--- quantdb/utils.py | 26 +++---- test/README.md | 4 +- test/conftest.py | 70 ++++++++++-------- test/pytest.ini | 4 +- test/test_database_setup.py | 42 +++++++---- test/test_generic_ingest.py | 43 ++++------- tests/__init__.py | 0 tests/test_api.py | 134 ----------------------------------- tests/test_generic_ingest.py | 97 ------------------------- 14 files changed, 158 insertions(+), 343 deletions(-) delete mode 100644 tests/__init__.py delete mode 100644 tests/test_api.py delete mode 100644 tests/test_generic_ingest.py diff --git a/.gitignore b/.gitignore index 5a3835f..c8d4804 100644 --- a/.gitignore +++ b/.gitignore @@ -174,4 +174,4 @@ cython_debug/ .pypirc README.pdf -.DS_STORE \ No newline at end of file +.DS_STORE diff --git a/README.org b/README.org index a156e4b..24c3e54 100644 --- a/README.org +++ b/README.org @@ -14,4 +14,4 @@ host:port:*:quantdb-test-user:password ``` * Notes -macOS will need the config.yaml in `Library/Application Support/quantdb/config.yaml` \ No newline at end of file +macOS will need the config.yaml in `Library/Application Support/quantdb/config.yaml` diff --git a/pyproject.toml b/pyproject.toml index d661228..0ceac3f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,7 @@ dependencies = [ "psycopg2-binary", "pre-commit ~= 4.1.0", "pytest==8.3.5", + "pandas==2.2.3", ] name = "quantdb" version = "0.1.0" diff --git a/quantdb/client.py b/quantdb/client.py index 2bab5ce..74eb0ef 100644 --- a/quantdb/client.py +++ b/quantdb/client.py @@ -1,6 +1,5 @@ -from sqlalchemy import create_engine +from sqlalchemy import create_engine, text from sqlalchemy.orm import Session -from sqlalchemy import text from quantdb.config import auth from quantdb.utils import dbUri @@ -26,16 +25,16 @@ def get_session(echo: bool = True, test: bool = False) -> Session: if test: # For testing, ALWAYS use local postgres instance regardless of main db config dbkwargs = { - "dbuser": "quantdb-test-admin", - "host": "localhost", # FORCE localhost for all testing - "port": 5432, - "database": auth.get("test-db-database") or "quantdb_test", - "password": "tom-is-cool", # Use hardcoded password for test database + 'dbuser': 'quantdb-test-admin', + 'host': 'localhost', # FORCE localhost for all testing + 'port': 5432, + 'database': auth.get('test-db-database') or 'quantdb_test', + 'password': 'tom-is-cool', # Use hardcoded password for test database } else: # For non-test, use regular database configuration - dbkwargs = {k: auth.get(f"db-{k}") for k in ("user", "host", "port", "database")} - dbkwargs["dbuser"] = dbkwargs.pop("user") + dbkwargs = {k: auth.get(f'db-{k}') for k in ('user', 'host', 'port', 'database')} + dbkwargs['dbuser'] = dbkwargs.pop('user') print(dbkwargs) engine = create_engine(dbUri(**dbkwargs)) # type: ignore engine.echo = echo @@ -44,6 +43,6 @@ def get_session(echo: bool = True, test: bool = False) -> Session: return session -if __name__ == "__main__": +if __name__ == '__main__': session = get_session(echo=False) - print(session.execute(text("SELECT * FROM information_schema.tables")).fetchall()) + print(session.execute(text('SELECT * FROM information_schema.tables')).fetchall()) diff --git a/quantdb/generic_ingest.py b/quantdb/generic_ingest.py index b28d5d3..ceba8b0 100644 --- a/quantdb/generic_ingest.py +++ b/quantdb/generic_ingest.py @@ -2,7 +2,7 @@ import uuid import pandas as pd -from sqlalchemy import UniqueConstraint, inspect +from sqlalchemy import PrimaryKeyConstraint, UniqueConstraint, inspect from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Session from sqlalchemy.sql.elements import ClauseElement @@ -30,8 +30,18 @@ def object_as_dict(obj): ------- dict Dictionary containing non-null column attributes of the object. + UUID objects are converted to strings for consistency. """ - return {c.key: getattr(obj, c.key) for c in inspect(obj).mapper.column_attrs if getattr(obj, c.key) is not None} + result = {} + for c in inspect(obj).mapper.column_attrs: + value = getattr(obj, c.key) + if value is not None: + # Convert UUID objects to strings for consistency + if hasattr(value, 'hex') and hasattr(value, 'version'): # Check if it's a UUID object + result[c.key] = str(value) + else: + result[c.key] = value + return result def get_or_create(session, obj, back_populate=None): @@ -50,7 +60,7 @@ def get_or_create(session, obj, back_populate=None): Returns ------- instance : SQLAlchemy model instance - The retrieved or newly created instance. + The retrieved or newly created instance with UUID attributes converted to strings. Notes ----- @@ -61,6 +71,8 @@ def get_or_create(session, obj, back_populate=None): data = object_as_dict(obj) instance = session.query(model).filter_by(**data).one_or_none() if instance: + # Convert UUID attributes to strings for consistency + _convert_uuids_to_strings(instance) return instance else: params = {k: v for k, v in data.items() if not isinstance(v, ClauseElement)} @@ -72,17 +84,35 @@ def get_or_create(session, obj, back_populate=None): for attr, value in back_populate.items(): setattr(instance, attr, value) session.commit() + # Convert UUID attributes to strings for consistency + _convert_uuids_to_strings(instance) except Exception: session.rollback() instance = session.query(model).filter_by(**data).one() + _convert_uuids_to_strings(instance) return instance else: return instance +def _convert_uuids_to_strings(obj): + """ + Convert UUID attributes to strings in-place for an ORM object. + + Parameters + ---------- + obj : SQLAlchemy ORM object + The object to modify. + """ + for c in inspect(obj).mapper.column_attrs: + value = getattr(obj, c.key) + if value is not None and hasattr(value, 'hex') and hasattr(value, 'version'): + setattr(obj, c.key, str(value)) + + def get_constraint_columns(model): """ - Returns the columns of the unique constraints for a SQLAlchemy model. + Returns the columns of the unique constraints and primary key constraints for a SQLAlchemy model. Parameters ---------- @@ -93,10 +123,12 @@ def get_constraint_columns(model): ------- list of list of str A list of lists, where each inner list contains the column names - that form a unique constraint. + that form a unique constraint or primary key constraint. """ constraints = [ - constraint for constraint in model.__mapper__.tables[0].constraints if isinstance(constraint, UniqueConstraint) + constraint + for constraint in model.__mapper__.tables[0].constraints + if isinstance(constraint, (UniqueConstraint, PrimaryKeyConstraint)) ] return [[c.name for c in list(constraint.columns)] for constraint in constraints] @@ -116,7 +148,7 @@ def query_by_constraints(db, obj): Returns ------- SQLAlchemy model instance or None - The existing instance if found, None otherwise. + The existing instance if found with UUID attributes converted to strings, None otherwise. """ filter_criteria = object_as_dict(obj) @@ -129,6 +161,8 @@ def query_by_constraints(db, obj): existing_obj = db.query(type(obj)).filter_by(**constraint_filter).first() if existing_obj: + # Convert UUID attributes to strings for consistency + _convert_uuids_to_strings(existing_obj) return existing_obj @@ -147,7 +181,7 @@ def back_populate_tables(db: Session, obj) -> object: Returns ------- object - The ORM model object, with its related objects back-populated. + The ORM model object, with its related objects back-populated and UUID attributes converted to strings. Notes ----- @@ -157,6 +191,7 @@ def back_populate_tables(db: Session, obj) -> object: - Performs a complete rollback if any commit fails - Filters by all primary key columns of the object - Handles cases where the child object is not a valid ORM instance + - Converts UUID attributes to strings for consistency """ mapper = obj.__mapper__ # Get the mapper for the object # print(obj) @@ -169,6 +204,7 @@ def back_populate_tables(db: Session, obj) -> object: filter_criteria = object_as_dict(parent) existing_obj = db.query(type(parent)).filter_by(**filter_criteria).first() if existing_obj: + _convert_uuids_to_strings(existing_obj) for key, value in object_as_dict(existing_obj).items(): if hasattr(parent, key): setattr(parent, key, value) @@ -200,6 +236,7 @@ def back_populate_tables(db: Session, obj) -> object: # db.refresh(obj) print('added new') else: + _convert_uuids_to_strings(existing_obj) db.merge(obj) db.commit() for key, value in object_as_dict(existing_obj).items(): @@ -213,6 +250,8 @@ def back_populate_tables(db: Session, obj) -> object: # print(f"Error during commit: {e}") raise # Re-raise the exception to indicate failure + # Convert UUID attributes to strings for consistency in the returned object + _convert_uuids_to_strings(obj) return obj # Return the object with its related objects back-populated diff --git a/quantdb/utils.py b/quantdb/utils.py index e91aae9..3097575 100644 --- a/quantdb/utils.py +++ b/quantdb/utils.py @@ -12,43 +12,43 @@ def makeSimpleLogger(name, level=logging.INFO): logger.setLevel(level) ch = logging.StreamHandler() # FileHander goes to disk - fmt = "[%(asctime)s] - %(levelname)8s - " "%(name)14s - " "%(filename)16s:%(lineno)-4d - " "%(message)s" + fmt = '[%(asctime)s] - %(levelname)8s - ' '%(name)14s - ' '%(filename)16s:%(lineno)-4d - ' '%(message)s' formatter = logging.Formatter(fmt) ch.setFormatter(formatter) logger.addHandler(ch) return logger -log = makeSimpleLogger("quantdb") -logd = log.getChild("data") +log = makeSimpleLogger('quantdb') +logd = log.getChild('data') # from pyontutils.utils_fast import setPS1 def setPS1(script__file__): try: - text = "Running " + os.path.basename(script__file__) - sys.stdout.write("\x1b]2;{}\x07\n".format(text)) + text = 'Running ' + os.path.basename(script__file__) + sys.stdout.write('\x1b]2;{}\x07\n'.format(text)) except AttributeError as e: log.exception(e) def dbUri(dbuser, host, port, database, password=None): - if hasattr(sys, "pypy_version_info"): - dialect = "psycopg2cffi" + if hasattr(sys, 'pypy_version_info'): + dialect = 'psycopg2cffi' else: - dialect = "psycopg2" + dialect = 'psycopg2' if password: - return f"postgresql+{dialect}://{dbuser}:{password}@{host}:{port}/{database}" + return f'postgresql+{dialect}://{dbuser}:{password}@{host}:{port}/{database}' else: - return f"postgresql+{dialect}://{dbuser}@{host}:{port}/{database}" + return f'postgresql+{dialect}://{dbuser}@{host}:{port}/{database}' # from pyontutils.utils_fast import isoformat -def isoformat(datetime_instance, timespec="auto"): +def isoformat(datetime_instance, timespec='auto'): kwargs = {} if isinstance(datetime_instance, datetime): # don't pass timespec if type is not date not datetime - kwargs["timespec"] = timespec + kwargs['timespec'] = timespec - return datetime_instance.isoformat(**kwargs).replace(".", ",").replace("+00:00", "Z") + return datetime_instance.isoformat(**kwargs).replace('.', ',').replace('+00:00', 'Z') diff --git a/test/README.md b/test/README.md index 65d1815..e3a6802 100644 --- a/test/README.md +++ b/test/README.md @@ -73,7 +73,7 @@ pytest test/test_database_setup.py::TestDatabaseSetup::test_tables_created -v The tests verify these core tables exist: - `objects` - Main object table -- `units` - Unit definitions +- `units` - Unit definitions - `aspects` - Aspect definitions - `descriptors_inst` - Instance descriptors - `descriptors_cat` - Categorical descriptors @@ -82,4 +82,4 @@ The tests verify these core tables exist: - `values_cat` - Categorical values - `values_quant` - Quantitative values - `addresses` - Address mappings -- `controlled_terms` - Controlled vocabulary \ No newline at end of file +- `controlled_terms` - Controlled vocabulary diff --git a/test/conftest.py b/test/conftest.py index 4f7324b..1535124 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -1,8 +1,9 @@ -import subprocess -import pytest import os +import subprocess import sys from pathlib import Path + +import pytest from sqlalchemy import create_engine, text from sqlalchemy.exc import OperationalError @@ -10,41 +11,41 @@ from quantdb.config import auth -@pytest.fixture(scope="session") +@pytest.fixture(scope='session') def project_root(): """Get the project root directory.""" return Path(__file__).parent.parent -@pytest.fixture(scope="session") +@pytest.fixture(scope='session') def test_database_config(): """Get test database configuration.""" config = { - "user": "quantdb-test-admin", - "host": "localhost", # ALWAYS use localhost for testing, never remote - "port": 5432, - "database": auth.get("test-db-database") or "quantdb_test", + 'user': 'quantdb-test-admin', + 'host': 'localhost', # ALWAYS use localhost for testing, never remote + 'port': 5432, + 'database': auth.get('test-db-database') or 'quantdb_test', } return config -@pytest.fixture(scope="session") +@pytest.fixture(scope='session') def setup_test_database(project_root, test_database_config): """ Set up the test database using the dbsetup script. This fixture runs once per test session. """ - print(f"\n=== Setting up test database ===") + print(f'\n=== Setting up test database ===') print(f"Database: {test_database_config['database']}") print(f"Host: {test_database_config['host']}") print(f"Port: {test_database_config['port']}") print(f"User: {test_database_config['user']}") # Path to dbsetup script - dbsetup_script = project_root / "bin" / "dbsetup" + dbsetup_script = project_root / 'bin' / 'dbsetup' if not dbsetup_script.exists(): - pytest.fail(f"dbsetup script not found at: {dbsetup_script}") + pytest.fail(f'dbsetup script not found at: {dbsetup_script}') # Make sure the script is executable os.chmod(dbsetup_script, 0o755) @@ -52,35 +53,42 @@ def setup_test_database(project_root, test_database_config): try: # Set up environment with hardcoded test password test_env = os.environ.copy() - test_env["PGPASSWORD"] = "tom-is-cool" # Hardcoded test password + test_env['PGPASSWORD'] = 'tom-is-cool' # Hardcoded test password # Run dbsetup with test database parameters - cmd = [str(dbsetup_script), str(test_database_config["port"]), test_database_config["database"], test_database_config["host"]] + cmd = [ + str(dbsetup_script), + str(test_database_config['port']), + test_database_config['database'], + test_database_config['host'], + ] print(f"Running command: {' '.join(cmd)}") - print(f"Using hardcoded test password: tom-is-cool") + print(f'Using hardcoded test password: tom-is-cool') # Run the dbsetup script with the password environment variable - result = subprocess.run(cmd, cwd=project_root, capture_output=True, text=True, timeout=120, env=test_env) # 2 minute timeout + result = subprocess.run( + cmd, cwd=project_root, capture_output=True, text=True, timeout=120, env=test_env + ) # 2 minute timeout if result.returncode != 0: - print(f"STDOUT: {result.stdout}") - print(f"STDERR: {result.stderr}") - pytest.fail(f"dbsetup failed with return code {result.returncode}") + print(f'STDOUT: {result.stdout}') + print(f'STDERR: {result.stderr}') + pytest.fail(f'dbsetup failed with return code {result.returncode}') - print(f"dbsetup completed successfully") - print(f"STDOUT: {result.stdout}") + print(f'dbsetup completed successfully') + print(f'STDOUT: {result.stdout}') # Return the configuration for use by other fixtures return test_database_config except subprocess.TimeoutExpired: - pytest.fail("dbsetup script timed out after 2 minutes") + pytest.fail('dbsetup script timed out after 2 minutes') except Exception as e: - pytest.fail(f"Failed to run dbsetup: {e}") + pytest.fail(f'Failed to run dbsetup: {e}') -@pytest.fixture(scope="session") +@pytest.fixture(scope='session') def test_session(setup_test_database): """ Get a SQLAlchemy session connected to the test database. @@ -91,10 +99,10 @@ def test_session(setup_test_database): yield session session.close() except Exception as e: - pytest.fail(f"Failed to create test database session: {e}") + pytest.fail(f'Failed to create test database session: {e}') -@pytest.fixture(scope="session") +@pytest.fixture(scope='session') def verify_database_tables(test_session): """ Verify that the expected database tables were created. @@ -105,8 +113,8 @@ def verify_database_tables(test_session): result = test_session.execute( text( """ - SELECT table_schema, table_name - FROM information_schema.tables + SELECT table_schema, table_name + FROM information_schema.tables WHERE table_schema IN ('quantdb', 'public') ORDER BY table_schema, table_name """ @@ -123,13 +131,13 @@ def verify_database_tables(test_session): tables_by_schema[schema].append(table) all_tables.append(table) - print(f"\n=== Found {len(all_tables)} tables in test database ===") + print(f'\n=== Found {len(all_tables)} tables in test database ===') for schema, tables in tables_by_schema.items(): print(f"Schema '{schema}': {len(tables)} tables") for table in tables: - print(f" - {table}") + print(f' - {table}') return all_tables except Exception as e: - pytest.fail(f"Failed to query database tables: {e}") + pytest.fail(f'Failed to query database tables: {e}') diff --git a/test/pytest.ini b/test/pytest.ini index c578cfc..2ee4565 100644 --- a/test/pytest.ini +++ b/test/pytest.ini @@ -3,11 +3,11 @@ testpaths = test python_files = test_*.py python_classes = Test* python_functions = test_* -addopts = +addopts = -v --tb=short --strict-markers --disable-warnings markers = database: marks tests as database tests (deselect with '-m "not database"') - slow: marks tests as slow (deselect with '-m "not slow"') \ No newline at end of file + slow: marks tests as slow (deselect with '-m "not slow"') diff --git a/test/test_database_setup.py b/test/test_database_setup.py index 02bb8cf..13a2b93 100644 --- a/test/test_database_setup.py +++ b/test/test_database_setup.py @@ -19,28 +19,40 @@ class TestDatabaseSetup: def test_database_connection(self, test_session): """Test that we can connect to the test database.""" # Try a simple query to verify the connection works - result = test_session.execute(text("SELECT 1 as test_value")) + result = test_session.execute(text('SELECT 1 as test_value')) row = result.fetchone() - assert row[0] == 1, "Basic database connection failed" + assert row[0] == 1, 'Basic database connection failed' def test_tables_created(self, verify_database_tables): """Test that the expected core tables were created.""" tables = verify_database_tables # Verify we have tables (the list should not be empty) - assert len(tables) > 0, "No tables found in the database" + assert len(tables) > 0, 'No tables found in the database' # Check for some core tables that should exist based on sql/tables.sql - expected_core_tables = ["objects", "units", "aspects", "descriptors_inst", "descriptors_cat", "descriptors_quant", "values_inst", "values_cat", "values_quant", "addresses", "controlled_terms"] + expected_core_tables = [ + 'objects', + 'units', + 'aspects', + 'descriptors_inst', + 'descriptors_cat', + 'descriptors_quant', + 'values_inst', + 'values_cat', + 'values_quant', + 'addresses', + 'controlled_terms', + ] missing_tables = [] for expected_table in expected_core_tables: if expected_table not in tables: missing_tables.append(expected_table) - assert len(missing_tables) == 0, f"Missing expected tables: {missing_tables}" + assert len(missing_tables) == 0, f'Missing expected tables: {missing_tables}' - print(f"✓ All {len(expected_core_tables)} core tables found in database") + print(f'✓ All {len(expected_core_tables)} core tables found in database') def test_database_schema_info(self, test_session): """Test that we can query basic schema information.""" @@ -49,16 +61,16 @@ def test_database_schema_info(self, test_session): text( """ SELECT COUNT(*) as table_count - FROM information_schema.tables + FROM information_schema.tables WHERE table_schema IN ('quantdb', 'public') """ ) ) table_count = result.fetchone()[0] - assert table_count > 0, "No tables found in quantdb or public schemas" + assert table_count > 0, 'No tables found in quantdb or public schemas' - print(f"✓ Found {table_count} tables in quantdb and public schemas") + print(f'✓ Found {table_count} tables in quantdb and public schemas') def test_database_functions_created(self, test_session): """Test that database functions were created.""" @@ -67,7 +79,7 @@ def test_database_functions_created(self, test_session): text( """ SELECT COUNT(*) as function_count - FROM information_schema.routines + FROM information_schema.routines WHERE routine_schema IN ('quantdb', 'public') AND routine_type = 'FUNCTION' """ @@ -76,9 +88,9 @@ def test_database_functions_created(self, test_session): function_count = result.fetchone()[0] # We expect several functions to be created based on tables.sql - assert function_count > 0, "No custom functions found in database" + assert function_count > 0, 'No custom functions found in database' - print(f"✓ Found {function_count} custom functions in database") + print(f'✓ Found {function_count} custom functions in database') def test_database_enums_created(self, test_session): """Test that custom enum types were created.""" @@ -86,7 +98,7 @@ def test_database_enums_created(self, test_session): text( """ SELECT COUNT(*) as enum_count - FROM pg_type + FROM pg_type WHERE typtype = 'e' """ ) @@ -94,6 +106,6 @@ def test_database_enums_created(self, test_session): enum_count = result.fetchone()[0] # We expect several enum types based on tables.sql (remote_id_type, instance_type, etc.) - assert enum_count > 0, "No custom enum types found in database" + assert enum_count > 0, 'No custom enum types found in database' - print(f"✓ Found {enum_count} custom enum types in database") + print(f'✓ Found {enum_count} custom enum types in database') diff --git a/test/test_generic_ingest.py b/test/test_generic_ingest.py index b36e2b4..011402f 100644 --- a/test/test_generic_ingest.py +++ b/test/test_generic_ingest.py @@ -1,6 +1,5 @@ import pytest -from quantdb.client import get_session from quantdb.generic_ingest import ( back_populate_tables, get_constraint_columns, @@ -11,14 +10,6 @@ from quantdb.models import Objects -def setup_test_session(): - """ - Returns a SQLAlchemy session connected to the test PostgreSQL database. - The test DB is prepared by bin/prepare_test_db.sh and populated with the first 10 rows of each table. - """ - return get_session(echo=False, test=True) - - def test_object_as_dict(): obj = Objects(id='00000000-0000-0000-0000-000000000001', id_type='dataset', id_file=None, id_internal=None) d = object_as_dict(obj) @@ -26,13 +17,12 @@ def test_object_as_dict(): assert d['id_type'] == 'dataset' -def test_get_or_create_creates_and_gets(): - session = setup_test_session() +def test_get_or_create_creates_and_gets(test_session): obj = Objects(id='00000000-0000-0000-0000-000000000002', id_type='dataset', id_file=None, id_internal=None) - instance = get_or_create(session, obj) + instance = get_or_create(test_session, obj) assert getattr(instance, 'id', None) == '00000000-0000-0000-0000-000000000002' # Should return the same instance if called again - instance2 = get_or_create(session, obj) + instance2 = get_or_create(test_session, obj) assert getattr(instance, 'id', None) == getattr(instance2, 'id', None) @@ -41,53 +31,50 @@ def test_get_constraint_columns(): assert any('id' in col for col in cols) -def test_query_by_constraints(): - session = setup_test_session() +def test_query_by_constraints(test_session): obj = Objects(id='00000000-0000-0000-0000-000000000003', id_type='dataset', id_file=None, id_internal=None) - session.add(obj) - session.commit() + test_session.add(obj) + test_session.commit() # Should find the object by unique constraint found = query_by_constraints( - session, Objects(id='00000000-0000-0000-0000-000000000003', id_type='dataset', id_file=None, id_internal=None) + test_session, + Objects(id='00000000-0000-0000-0000-000000000003', id_type='dataset', id_file=None, id_internal=None), ) assert found is not None assert getattr(found, 'id', None) == '00000000-0000-0000-0000-000000000003' assert getattr(found, 'id_type', None) == 'dataset' -def test_back_populate_tables_adds_and_merges(): - session = setup_test_session() +def test_back_populate_tables_adds_and_merges(test_session): obj = Objects(id='00000000-0000-0000-0000-000000000004', id_type='dataset', id_file=None, id_internal=None) # Should add new - out = back_populate_tables(session, obj) + out = back_populate_tables(test_session, obj) assert getattr(out, 'id', None) == '00000000-0000-0000-0000-000000000004' # Should merge existing obj2 = Objects(id='00000000-0000-0000-0000-000000000004', id_type='dataset', id_file=None, id_internal=None) - out2 = back_populate_tables(session, obj2) + out2 = back_populate_tables(test_session, obj2) assert getattr(out2, 'id', None) == getattr(out, 'id', None) -def test_get_or_create_back_populate(): - session = setup_test_session() +def test_get_or_create_back_populate(test_session): obj = Objects(id='00000000-0000-0000-0000-000000000005', id_type='dataset', id_file=None, id_internal=None) - instance = get_or_create(session, obj, back_populate={'id_file': 12345}) + instance = get_or_create(test_session, obj, back_populate={'id_file': 12345}) assert getattr(instance, 'id_file', None) == 12345 -def test_print_first_row_of_each_entity(): +def test_print_first_row_of_each_entity(test_session): """ Prints the first row of each mapped entity in the test database. """ import inspect as pyinspect - session = setup_test_session() from quantdb import models printed = False for name, cls in vars(models).items(): if pyinspect.isclass(cls) and hasattr(cls, '__table__') and hasattr(cls, '__mapper__'): try: - row = session.query(cls).first() + row = test_session.query(cls).first() if row: print(f'First row for {name}: {row}') printed = True diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/test_api.py b/tests/test_api.py deleted file mode 100644 index e189822..0000000 --- a/tests/test_api.py +++ /dev/null @@ -1,134 +0,0 @@ -import json -import pprint - -from flask_sqlalchemy import SQLAlchemy - -from quantdb.api import make_app -from quantdb.utils import log - - -def test(): - db = SQLAlchemy() - app = make_app(db=db, dev=True) - client = app.test_client() - runner = app.test_cli_runner() - - dataset_uuid = 'aa43eda8-b29a-4c25-9840-ecbd57598afc' - some_object = '414886a9-9ec7-447e-b4d8-3ae42fda93b7' # XXX FAKE - actual_package_uuid = '15bcbcd5-b054-40ef-9b5c-6a260d441621' - base = 'http://localhost:8989/api/1/' - urls = ( - f'{base}values/inst', - f'{base}values/inst?dataset={dataset_uuid}', - f'{base}values/inst?dataset={dataset_uuid}&union-cat-quant=true', - f'{base}values/inst?dataset={dataset_uuid}&aspect=distance&aspect=time', - f'{base}values/inst?dataset={dataset_uuid}&aspect=distance&value-quant-min=0.5', - f'{base}values/inst?dataset={dataset_uuid}&inst-parent=sub-f001', - f'{base}values/inst?dataset={dataset_uuid}&inst-parent=sam-r-seg-c1&inst-parent=sam-l-seg-c1', - f'{base}values/inst?desc-inst=nerve-volume', - f'{base}objects?dataset={dataset_uuid}', - f'{base}objects?dataset={dataset_uuid}&aspect=distance', - f'{base}objects?dataset={dataset_uuid}&aspect=distance&value-quant-min=0.5', # expect nothing - f'{base}objects?dataset={dataset_uuid}&aspect=distance&value-quant-min=0.5&union-cat-quant=true', - f'{base}objects?dataset={dataset_uuid}&subject=sub-f001', - f'{base}objects?subject=sub-f001', - f'{base}objects?subject=sub-f001&union-cat-quant=true', - f'{base}objects?subject=sub-f001&subject=sub-f002&subject=sub-f003&subject=sub-f004&subject=sub-f005', - f'{base}objects?subject=sub-f001&subject=sub-f002&subject=sub-f003&subject=sub-f004&subject=sub-f005&union-cat-quant=true', - f'{base}objects?subject=sub-f001&desc-cat=none&value-quant-min=0.5&union-cat-quant=true', - f'{base}objects?subject=sub-f001&desc-cat=none&aspect=distance&value-quant-min=0.5&union-cat-quant=true', - f'{base}objects?subject=sub-f001&aspect=distance&value-quant-min=0.5&union-cat-quant=true', - f'{base}objects?aspect=distance&value-quant-min=0.5&union-cat-quant=true', - f'{base}objects?desc-cat=none&aspect=distance&value-quant-min=0.5&union-cat-quant=true', - f'{base}objects?desc-cat=none&aspect=distance&value-quant-min=0.5', - f'{base}objects?aspect=distance&value-quant-min=0.5', - f'{base}objects?aspect=distance&value-quant-min=0.5&source-only=true', - f'{base}objects?desc-inst=nerve-volume&aspect=distance&value-quant-min=0.5&source-only=true', - # values-quant - f'{base}values/quant?dataset={dataset_uuid}&aspect=distance', - f'{base}values/quant?object={actual_package_uuid}&aspect=distance', - f'{base}values/quant?aspect=distance', - f'{base}values/quant?aspect=distance-via-reva-ft-sample-id-normalized-v1', - f'{base}values/quant?aspect=distance-via-reva-ft-sample-id-normalized-v1&agg-type=instance', - f'{base}values/quant?aspect=distance-via-reva-ft-sample-id-normalized-v1&value-quant-min=0.4&value-quant-max=0.7', - # values-cat - f'{base}values/cat?object={actual_package_uuid}', - f'{base}values/cat?object={actual_package_uuid}&union-cat-quant=true', # shouldn't need it in this case - f'{base}values/cat-quant?object={actual_package_uuid}', - f'{base}values/cat-quant?object={actual_package_uuid}&union-cat-quant=true', - # values-cat-quant - f'{base}values?dataset={dataset_uuid}&aspect=distance&value-quant-min=0.5', - f'{base}values?dataset={dataset_uuid}&aspect=distance&value-quant-min=0.5&union-cat-quant=true', - f'{base}values?object={actual_package_uuid}', - f'{base}values?object={actual_package_uuid}&union-cat-quant=true', - f'{base}values/inst?object={actual_package_uuid}', - f'{base}values/inst?object={actual_package_uuid}&union-cat-quant=true', - # prov - f'{base}values/inst?prov=true', - f'{base}values/quant?aspect=distance&prov=true', - f'{base}values/cat?object={actual_package_uuid}', - f'{base}values/cat?object={actual_package_uuid}&prov=true', # FIXME somehow this has a 3x increase in records, and non-distinct - f'{base}values/cat-quant?object={actual_package_uuid}&union-cat-quant=true', - f'{base}values/cat-quant?object={actual_package_uuid}&union-cat-quant=true&prov=true', - f'{base}values/cat-quant', - f'{base}values/cat-quant?prov=true', - f'{base}values/cat-quant?union-cat-quant=true', - f'{base}values/cat-quant?union-cat-quant=true&prov=true', - # desc - f'{base}desc/inst', - f'{base}desc/cat', - f'{base}desc/quant', - f'{base}desc/inst?include-unused=true', - f'{base}desc/cat?include-unused=true', - f'{base}desc/quant?include-unused=true', - # descriptor values - f'{base}terms', - f'{base}aspects', - f'{base}units', - f'{base}terms?include-unused=true', - f'{base}aspects?include-unused=true', - f'{base}units?include-unused=true', - # TODO maybe shapes here as well? - f'{base}terms?inst-parent=sam-r-seg-c1&inst-parent=sam-l-seg-c1', - f'{base}aspects?inst-parent=sam-r-seg-c1&inst-parent=sam-l-seg-c1', - f'{base}units?inst-parent=sam-r-seg-c1&inst-parent=sam-l-seg-c1', - ) - # log.setLevel(9) - resps = [] - for url in urls: - log.debug(url) - resp = client.get(url) - resp.ok = resp.status_code < 400 - resp.url = resp.request.url - resp.content = resp.data - resps.append(json.loads(resp.data.decode())) - - pprint.pprint(resps, width=120) - # (i := 6, resps[i], urls[i]) - # q = client.get(f'{base}values/quant?dataset={dataset_uuid}&aspect=distance&return-query=true').data.decode() - # q = client.get(f'{base}values/cat?object={actual_package_uuid}&prov=true&return-query=true').data.decode() - # print(q) - breakpoint() - - -def test_demo_load(): - db = SQLAlchemy() - app = make_app(db=db, dev=True) - client = app.test_client() - runner = app.test_cli_runner() - - dataset_uuid = '55c5b69c-a5b8-4881-a105-e4048af26fa5' - package_uuid = '20720c2e-83fb-4454-bef1-1ce6a97fa748' - base = 'http://localhost:8989/api/1/' - urls = (f'{base}values/cat-quant?desc-inst=fascicle-cross-section',) - - resps = [] - for url in urls: - log.debug(url) - resp = client.get(url) - resp.ok = resp.status_code < 400 - resp.url = resp.request.url - resp.content = resp.data - resps.append(json.loads(resp.data.decode())) - - breakpoint() diff --git a/tests/test_generic_ingest.py b/tests/test_generic_ingest.py deleted file mode 100644 index b36e2b4..0000000 --- a/tests/test_generic_ingest.py +++ /dev/null @@ -1,97 +0,0 @@ -import pytest - -from quantdb.client import get_session -from quantdb.generic_ingest import ( - back_populate_tables, - get_constraint_columns, - get_or_create, - object_as_dict, - query_by_constraints, -) -from quantdb.models import Objects - - -def setup_test_session(): - """ - Returns a SQLAlchemy session connected to the test PostgreSQL database. - The test DB is prepared by bin/prepare_test_db.sh and populated with the first 10 rows of each table. - """ - return get_session(echo=False, test=True) - - -def test_object_as_dict(): - obj = Objects(id='00000000-0000-0000-0000-000000000001', id_type='dataset', id_file=None, id_internal=None) - d = object_as_dict(obj) - assert d['id'] == '00000000-0000-0000-0000-000000000001' - assert d['id_type'] == 'dataset' - - -def test_get_or_create_creates_and_gets(): - session = setup_test_session() - obj = Objects(id='00000000-0000-0000-0000-000000000002', id_type='dataset', id_file=None, id_internal=None) - instance = get_or_create(session, obj) - assert getattr(instance, 'id', None) == '00000000-0000-0000-0000-000000000002' - # Should return the same instance if called again - instance2 = get_or_create(session, obj) - assert getattr(instance, 'id', None) == getattr(instance2, 'id', None) - - -def test_get_constraint_columns(): - cols = get_constraint_columns(Objects) - assert any('id' in col for col in cols) - - -def test_query_by_constraints(): - session = setup_test_session() - obj = Objects(id='00000000-0000-0000-0000-000000000003', id_type='dataset', id_file=None, id_internal=None) - session.add(obj) - session.commit() - # Should find the object by unique constraint - found = query_by_constraints( - session, Objects(id='00000000-0000-0000-0000-000000000003', id_type='dataset', id_file=None, id_internal=None) - ) - assert found is not None - assert getattr(found, 'id', None) == '00000000-0000-0000-0000-000000000003' - assert getattr(found, 'id_type', None) == 'dataset' - - -def test_back_populate_tables_adds_and_merges(): - session = setup_test_session() - obj = Objects(id='00000000-0000-0000-0000-000000000004', id_type='dataset', id_file=None, id_internal=None) - # Should add new - out = back_populate_tables(session, obj) - assert getattr(out, 'id', None) == '00000000-0000-0000-0000-000000000004' - # Should merge existing - obj2 = Objects(id='00000000-0000-0000-0000-000000000004', id_type='dataset', id_file=None, id_internal=None) - out2 = back_populate_tables(session, obj2) - assert getattr(out2, 'id', None) == getattr(out, 'id', None) - - -def test_get_or_create_back_populate(): - session = setup_test_session() - obj = Objects(id='00000000-0000-0000-0000-000000000005', id_type='dataset', id_file=None, id_internal=None) - instance = get_or_create(session, obj, back_populate={'id_file': 12345}) - assert getattr(instance, 'id_file', None) == 12345 - - -def test_print_first_row_of_each_entity(): - """ - Prints the first row of each mapped entity in the test database. - """ - import inspect as pyinspect - - session = setup_test_session() - from quantdb import models - - printed = False - for name, cls in vars(models).items(): - if pyinspect.isclass(cls) and hasattr(cls, '__table__') and hasattr(cls, '__mapper__'): - try: - row = session.query(cls).first() - if row: - print(f'First row for {name}: {row}') - printed = True - except Exception as e: - print(f'Could not query {name}: {e}') - if not printed: - print('No rows found in any entity.') From c8a54ce37d415638aab0799bdd04866d4b6cb2c2 Mon Sep 17 00:00:00 2001 From: tmsincomb Date: Mon, 23 Feb 2026 11:30:26 -0800 Subject: [PATCH 18/18] sync ingestion --- ingestion/f006.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 ingestion/f006.py diff --git a/ingestion/f006.py b/ingestion/f006.py new file mode 100644 index 0000000..e69de29