diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ee87b39c..06d80284 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,23 +42,6 @@ jobs: with: path: "backend/zeno_backend/" - format-api: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: psf/black@stable - with: - options: "--check --verbose" - src: "zeno_api/zeno_api/" - - lint-api: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: jpetrucciani/ruff-check@main - with: - path: "zeno_api/zeno_api/" - typecheck-python: runs-on: ubuntu-latest steps: @@ -75,7 +58,7 @@ jobs: python-version: 3.11 - name: Install Poetry - uses: snok/install-poetry@v1.3.3 + uses: snok/install-poetry@v1.3.4 with: virtualenvs-in-project: true virtualenvs-path: ~/.virtualenvs diff --git a/backend/poetry.lock b/backend/poetry.lock index 18b89080..4cfd17d0 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. [[package]] name = "aiofiles" @@ -11,6 +11,17 @@ files = [ {file = "aiofiles-23.2.1.tar.gz", hash = "sha256:84ec2218d8419404abcb9f0c02df3f34c6e0a68ed41072acfb1cef5cbc29051a"}, ] +[[package]] +name = "amplitude-analytics" +version = "1.1.3" +description = "The official Amplitude backend Python SDK for server-side instrumentation." +optional = false +python-versions = ">=3.6, <4" +files = [ + {file = "amplitude-analytics-1.1.3.tar.gz", hash = "sha256:98a1f17607795c81f3bd9fb85d419c718687c7f2307a3c50da87d4273a7c0b37"}, + {file = "amplitude_analytics-1.1.3-py3-none-any.whl", hash = "sha256:74a0d257a733c296fa7cd064a1c909286750c232850ebfd605a8049dadafc7f1"}, +] + [[package]] name = "annotated-types" version = "0.5.0" @@ -55,33 +66,33 @@ trio = ["trio (<0.22)"] [[package]] name = "black" -version = "23.7.0" +version = "23.9.1" description = "The uncompromising code formatter." optional = false python-versions = ">=3.8" files = [ - {file = "black-23.7.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:5c4bc552ab52f6c1c506ccae05681fab58c3f72d59ae6e6639e8885e94fe2587"}, - {file = "black-23.7.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:552513d5cd5694590d7ef6f46e1767a4df9af168d449ff767b13b084c020e63f"}, - {file = "black-23.7.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:86cee259349b4448adb4ef9b204bb4467aae74a386bce85d56ba4f5dc0da27be"}, - {file = "black-23.7.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:501387a9edcb75d7ae8a4412bb8749900386eaef258f1aefab18adddea1936bc"}, - {file = "black-23.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:fb074d8b213749fa1d077d630db0d5f8cc3b2ae63587ad4116e8a436e9bbe995"}, - {file = "black-23.7.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:b5b0ee6d96b345a8b420100b7d71ebfdd19fab5e8301aff48ec270042cd40ac2"}, - {file = "black-23.7.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:893695a76b140881531062d48476ebe4a48f5d1e9388177e175d76234ca247cd"}, - {file = "black-23.7.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:c333286dc3ddca6fdff74670b911cccedacb4ef0a60b34e491b8a67c833b343a"}, - {file = "black-23.7.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:831d8f54c3a8c8cf55f64d0422ee875eecac26f5f649fb6c1df65316b67c8926"}, - {file = "black-23.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:7f3bf2dec7d541b4619b8ce526bda74a6b0bffc480a163fed32eb8b3c9aed8ad"}, - {file = "black-23.7.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:f9062af71c59c004cd519e2fb8f5d25d39e46d3af011b41ab43b9c74e27e236f"}, - {file = "black-23.7.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:01ede61aac8c154b55f35301fac3e730baf0c9cf8120f65a9cd61a81cfb4a0c3"}, - {file = "black-23.7.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:327a8c2550ddc573b51e2c352adb88143464bb9d92c10416feb86b0f5aee5ff6"}, - {file = "black-23.7.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d1c6022b86f83b632d06f2b02774134def5d4d4f1dac8bef16d90cda18ba28a"}, - {file = "black-23.7.0-cp38-cp38-win_amd64.whl", hash = "sha256:27eb7a0c71604d5de083757fbdb245b1a4fae60e9596514c6ec497eb63f95320"}, - {file = "black-23.7.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:8417dbd2f57b5701492cd46edcecc4f9208dc75529bcf76c514864e48da867d9"}, - {file = "black-23.7.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:47e56d83aad53ca140da0af87678fb38e44fd6bc0af71eebab2d1f59b1acf1d3"}, - {file = "black-23.7.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:25cc308838fe71f7065df53aedd20327969d05671bac95b38fdf37ebe70ac087"}, - {file = "black-23.7.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:642496b675095d423f9b8448243336f8ec71c9d4d57ec17bf795b67f08132a91"}, - {file = "black-23.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:ad0014efc7acf0bd745792bd0d8857413652979200ab924fbf239062adc12491"}, - {file = "black-23.7.0-py3-none-any.whl", hash = "sha256:9fd59d418c60c0348505f2ddf9609c1e1de8e7493eab96198fc89d9f865e7a96"}, - {file = "black-23.7.0.tar.gz", hash = "sha256:022a582720b0d9480ed82576c920a8c1dde97cc38ff11d8d8859b3bd6ca9eedb"}, + {file = "black-23.9.1-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:d6bc09188020c9ac2555a498949401ab35bb6bf76d4e0f8ee251694664df6301"}, + {file = "black-23.9.1-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:13ef033794029b85dfea8032c9d3b92b42b526f1ff4bf13b2182ce4e917f5100"}, + {file = "black-23.9.1-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:75a2dc41b183d4872d3a500d2b9c9016e67ed95738a3624f4751a0cb4818fe71"}, + {file = "black-23.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13a2e4a93bb8ca74a749b6974925c27219bb3df4d42fc45e948a5d9feb5122b7"}, + {file = "black-23.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:adc3e4442eef57f99b5590b245a328aad19c99552e0bdc7f0b04db6656debd80"}, + {file = "black-23.9.1-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:8431445bf62d2a914b541da7ab3e2b4f3bc052d2ccbf157ebad18ea126efb91f"}, + {file = "black-23.9.1-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:8fc1ddcf83f996247505db6b715294eba56ea9372e107fd54963c7553f2b6dfe"}, + {file = "black-23.9.1-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:7d30ec46de88091e4316b17ae58bbbfc12b2de05e069030f6b747dfc649ad186"}, + {file = "black-23.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:031e8c69f3d3b09e1aa471a926a1eeb0b9071f80b17689a655f7885ac9325a6f"}, + {file = "black-23.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:538efb451cd50f43aba394e9ec7ad55a37598faae3348d723b59ea8e91616300"}, + {file = "black-23.9.1-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:638619a559280de0c2aa4d76f504891c9860bb8fa214267358f0a20f27c12948"}, + {file = "black-23.9.1-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:a732b82747235e0542c03bf352c126052c0fbc458d8a239a94701175b17d4855"}, + {file = "black-23.9.1-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:cf3a4d00e4cdb6734b64bf23cd4341421e8953615cba6b3670453737a72ec204"}, + {file = "black-23.9.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf99f3de8b3273a8317681d8194ea222f10e0133a24a7548c73ce44ea1679377"}, + {file = "black-23.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:14f04c990259576acd093871e7e9b14918eb28f1866f91968ff5524293f9c573"}, + {file = "black-23.9.1-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:c619f063c2d68f19b2d7270f4cf3192cb81c9ec5bc5ba02df91471d0b88c4c5c"}, + {file = "black-23.9.1-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:6a3b50e4b93f43b34a9d3ef00d9b6728b4a722c997c99ab09102fd5efdb88325"}, + {file = "black-23.9.1-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:c46767e8df1b7beefb0899c4a95fb43058fa8500b6db144f4ff3ca38eb2f6393"}, + {file = "black-23.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50254ebfa56aa46a9fdd5d651f9637485068a1adf42270148cd101cdf56e0ad9"}, + {file = "black-23.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:403397c033adbc45c2bd41747da1f7fc7eaa44efbee256b53842470d4ac5a70f"}, + {file = "black-23.9.1-py3-none-any.whl", hash = "sha256:6ccd59584cc834b6d127628713e4b6b968e5f79572da66284532525a042549f9"}, + {file = "black-23.9.1.tar.gz", hash = "sha256:24b6b3ff5c6d9ea08a8888f6977eae858e1f340d7260cf56d70a49823236b62d"}, ] [package.dependencies] @@ -429,16 +440,17 @@ gmpy2 = ["gmpy2"] [[package]] name = "fastapi" -version = "0.103.0" +version = "0.103.1" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" optional = false python-versions = ">=3.7" files = [ - {file = "fastapi-0.103.0-py3-none-any.whl", hash = "sha256:61ab72c6c281205dd0cbaccf503e829a37e0be108d965ac223779a8479243665"}, - {file = "fastapi-0.103.0.tar.gz", hash = "sha256:4166732f5ddf61c33e9fa4664f73780872511e0598d4d5434b1816dc1e6d9421"}, + {file = "fastapi-0.103.1-py3-none-any.whl", hash = "sha256:5e5f17e826dbd9e9b5a5145976c5cd90bcaa61f2bf9a69aca423f2bcebe44d83"}, + {file = "fastapi-0.103.1.tar.gz", hash = "sha256:345844e6a82062f06a096684196aaf96c1198b25c06b72c1311b882aa2d8a35d"}, ] [package.dependencies] +anyio = ">=3.7.1,<4.0.0" 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.27.0,<0.28.0" typing-extensions = ">=4.5.0" @@ -464,18 +476,18 @@ requests = ">=2.24.0,<3.0.0" [[package]] name = "filelock" -version = "3.12.2" +version = "3.12.3" description = "A platform independent file lock." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "filelock-3.12.2-py3-none-any.whl", hash = "sha256:cbb791cdea2a72f23da6ac5b5269ab0a0d161e9ef0100e653b69049a7706d1ec"}, - {file = "filelock-3.12.2.tar.gz", hash = "sha256:002740518d8aa59a26b0c76e10fb8c6e15eae825d34b6fdf670333fd7b938d81"}, + {file = "filelock-3.12.3-py3-none-any.whl", hash = "sha256:f067e40ccc40f2b48395a80fcbd4728262fab54e232e090a4063ab804179efeb"}, + {file = "filelock-3.12.3.tar.gz", hash = "sha256:0ecc1dd2ec4672a10c8550a8182f1bd0c0a5088470ecd5a125e45f49472fac3d"}, ] [package.extras] -docs = ["furo (>=2023.5.20)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "diff-cover (>=7.5)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)"] +docs = ["furo (>=2023.7.26)", "sphinx (>=7.1.2)", "sphinx-autodoc-typehints (>=1.24)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3)", "diff-cover (>=7.7)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)", "pytest-timeout (>=2.1)"] [[package]] name = "greenlet" @@ -489,6 +501,7 @@ files = [ {file = "greenlet-2.0.2-cp27-cp27m-win32.whl", hash = "sha256:6c3acb79b0bfd4fe733dff8bc62695283b57949ebcca05ae5c129eb606ff2d74"}, {file = "greenlet-2.0.2-cp27-cp27m-win_amd64.whl", hash = "sha256:283737e0da3f08bd637b5ad058507e578dd462db259f7f6e4c5c365ba4ee9343"}, {file = "greenlet-2.0.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:d27ec7509b9c18b6d73f2f5ede2622441de812e7b1a80bbd446cb0633bd3d5ae"}, + {file = "greenlet-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d967650d3f56af314b72df7089d96cda1083a7fc2da05b375d2bc48c82ab3f3c"}, {file = "greenlet-2.0.2-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:30bcf80dda7f15ac77ba5af2b961bdd9dbc77fd4ac6105cee85b0d0a5fcf74df"}, {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26fbfce90728d82bc9e6c38ea4d038cba20b7faf8a0ca53a9c07b67318d46088"}, {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9190f09060ea4debddd24665d6804b995a9c122ef5917ab26e1566dcc712ceeb"}, @@ -497,6 +510,7 @@ files = [ {file = "greenlet-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:76ae285c8104046b3a7f06b42f29c7b73f77683df18c49ab5af7983994c2dd91"}, {file = "greenlet-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:2d4686f195e32d36b4d7cf2d166857dbd0ee9f3d20ae349b6bf8afc8485b3645"}, {file = "greenlet-2.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c4302695ad8027363e96311df24ee28978162cdcdd2006476c43970b384a244c"}, + {file = "greenlet-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d4606a527e30548153be1a9f155f4e283d109ffba663a15856089fb55f933e47"}, {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c48f54ef8e05f04d6eff74b8233f6063cb1ed960243eacc474ee73a2ea8573ca"}, {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a1846f1b999e78e13837c93c778dcfc3365902cfb8d1bdb7dd73ead37059f0d0"}, {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a06ad5312349fec0ab944664b01d26f8d1f05009566339ac6f63f56589bc1a2"}, @@ -526,6 +540,7 @@ files = [ {file = "greenlet-2.0.2-cp37-cp37m-win32.whl", hash = "sha256:3f6ea9bd35eb450837a3d80e77b517ea5bc56b4647f5502cd28de13675ee12f7"}, {file = "greenlet-2.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:7492e2b7bd7c9b9916388d9df23fa49d9b88ac0640db0a5b4ecc2b653bf451e3"}, {file = "greenlet-2.0.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:b864ba53912b6c3ab6bcb2beb19f19edd01a6bfcbdfe1f37ddd1778abfe75a30"}, + {file = "greenlet-2.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1087300cf9700bbf455b1b97e24db18f2f77b55302a68272c56209d5587c12d1"}, {file = "greenlet-2.0.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:ba2956617f1c42598a308a84c6cf021a90ff3862eddafd20c3333d50f0edb45b"}, {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3a569657468b6f3fb60587e48356fe512c1754ca05a564f11366ac9e306526"}, {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8eab883b3b2a38cc1e050819ef06a7e6344d4a990d24d45bc6f2cf959045a45b"}, @@ -534,6 +549,7 @@ files = [ {file = "greenlet-2.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b0ef99cdbe2b682b9ccbb964743a6aca37905fda5e0452e5ee239b1654d37f2a"}, {file = "greenlet-2.0.2-cp38-cp38-win32.whl", hash = "sha256:b80f600eddddce72320dbbc8e3784d16bd3fb7b517e82476d8da921f27d4b249"}, {file = "greenlet-2.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:4d2e11331fc0c02b6e84b0d28ece3a36e0548ee1a1ce9ddde03752d9b79bba40"}, + {file = "greenlet-2.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8512a0c38cfd4e66a858ddd1b17705587900dd760c6003998e9472b77b56d417"}, {file = "greenlet-2.0.2-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:88d9ab96491d38a5ab7c56dd7a3cc37d83336ecc564e4e8816dbed12e5aaefc8"}, {file = "greenlet-2.0.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:561091a7be172ab497a3527602d467e2b3fbe75f9e783d8b8ce403fa414f71a6"}, {file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:971ce5e14dc5e73715755d0ca2975ac88cfdaefcaab078a284fea6cfabf866df"}, @@ -749,69 +765,61 @@ files = [ [[package]] name = "pandas" -version = "2.0.3" +version = "2.1.0" description = "Powerful data structures for data analysis, time series, and statistics" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "pandas-2.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e4c7c9f27a4185304c7caf96dc7d91bc60bc162221152de697c98eb0b2648dd8"}, - {file = "pandas-2.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f167beed68918d62bffb6ec64f2e1d8a7d297a038f86d4aed056b9493fca407f"}, - {file = "pandas-2.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce0c6f76a0f1ba361551f3e6dceaff06bde7514a374aa43e33b588ec10420183"}, - {file = "pandas-2.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba619e410a21d8c387a1ea6e8a0e49bb42216474436245718d7f2e88a2f8d7c0"}, - {file = "pandas-2.0.3-cp310-cp310-win32.whl", hash = "sha256:3ef285093b4fe5058eefd756100a367f27029913760773c8bf1d2d8bebe5d210"}, - {file = "pandas-2.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:9ee1a69328d5c36c98d8e74db06f4ad518a1840e8ccb94a4ba86920986bb617e"}, - {file = "pandas-2.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b084b91d8d66ab19f5bb3256cbd5ea661848338301940e17f4492b2ce0801fe8"}, - {file = "pandas-2.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:37673e3bdf1551b95bf5d4ce372b37770f9529743d2498032439371fc7b7eb26"}, - {file = "pandas-2.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9cb1e14fdb546396b7e1b923ffaeeac24e4cedd14266c3497216dd4448e4f2d"}, - {file = "pandas-2.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9cd88488cceb7635aebb84809d087468eb33551097d600c6dad13602029c2df"}, - {file = "pandas-2.0.3-cp311-cp311-win32.whl", hash = "sha256:694888a81198786f0e164ee3a581df7d505024fbb1f15202fc7db88a71d84ebd"}, - {file = "pandas-2.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:6a21ab5c89dcbd57f78d0ae16630b090eec626360085a4148693def5452d8a6b"}, - {file = "pandas-2.0.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9e4da0d45e7f34c069fe4d522359df7d23badf83abc1d1cef398895822d11061"}, - {file = "pandas-2.0.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:32fca2ee1b0d93dd71d979726b12b61faa06aeb93cf77468776287f41ff8fdc5"}, - {file = "pandas-2.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:258d3624b3ae734490e4d63c430256e716f488c4fcb7c8e9bde2d3aa46c29089"}, - {file = "pandas-2.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eae3dc34fa1aa7772dd3fc60270d13ced7346fcbcfee017d3132ec625e23bb0"}, - {file = "pandas-2.0.3-cp38-cp38-win32.whl", hash = "sha256:f3421a7afb1a43f7e38e82e844e2bca9a6d793d66c1a7f9f0ff39a795bbc5e02"}, - {file = "pandas-2.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:69d7f3884c95da3a31ef82b7618af5710dba95bb885ffab339aad925c3e8ce78"}, - {file = "pandas-2.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5247fb1ba347c1261cbbf0fcfba4a3121fbb4029d95d9ef4dc45406620b25c8b"}, - {file = "pandas-2.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:81af086f4543c9d8bb128328b5d32e9986e0c84d3ee673a2ac6fb57fd14f755e"}, - {file = "pandas-2.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1994c789bf12a7c5098277fb43836ce090f1073858c10f9220998ac74f37c69b"}, - {file = "pandas-2.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ec591c48e29226bcbb316e0c1e9423622bc7a4eaf1ef7c3c9fa1a3981f89641"}, - {file = "pandas-2.0.3-cp39-cp39-win32.whl", hash = "sha256:04dbdbaf2e4d46ca8da896e1805bc04eb85caa9a82e259e8eed00254d5e0c682"}, - {file = "pandas-2.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:1168574b036cd8b93abc746171c9b4f1b83467438a5e45909fed645cf8692dbc"}, - {file = "pandas-2.0.3.tar.gz", hash = "sha256:c02f372a88e0d17f36d3093a644c73cfc1788e876a7c4bcb4020a77512e2043c"}, + {file = "pandas-2.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:40dd20439ff94f1b2ed55b393ecee9cb6f3b08104c2c40b0cb7186a2f0046242"}, + {file = "pandas-2.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d4f38e4fedeba580285eaac7ede4f686c6701a9e618d8a857b138a126d067f2f"}, + {file = "pandas-2.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e6a0fe052cf27ceb29be9429428b4918f3740e37ff185658f40d8702f0b3e09"}, + {file = "pandas-2.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d81e1813191070440d4c7a413cb673052b3b4a984ffd86b8dd468c45742d3cc"}, + {file = "pandas-2.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:eb20252720b1cc1b7d0b2879ffc7e0542dd568f24d7c4b2347cb035206936421"}, + {file = "pandas-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:38f74ef7ebc0ffb43b3d633e23d74882bce7e27bfa09607f3c5d3e03ffd9a4a5"}, + {file = "pandas-2.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cda72cc8c4761c8f1d97b169661f23a86b16fdb240bdc341173aee17e4d6cedd"}, + {file = "pandas-2.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d97daeac0db8c993420b10da4f5f5b39b01fc9ca689a17844e07c0a35ac96b4b"}, + {file = "pandas-2.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8c58b1113892e0c8078f006a167cc210a92bdae23322bb4614f2f0b7a4b510f"}, + {file = "pandas-2.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:629124923bcf798965b054a540f9ccdfd60f71361255c81fa1ecd94a904b9dd3"}, + {file = "pandas-2.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:70cf866af3ab346a10debba8ea78077cf3a8cd14bd5e4bed3d41555a3280041c"}, + {file = "pandas-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:d53c8c1001f6a192ff1de1efe03b31a423d0eee2e9e855e69d004308e046e694"}, + {file = "pandas-2.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:86f100b3876b8c6d1a2c66207288ead435dc71041ee4aea789e55ef0e06408cb"}, + {file = "pandas-2.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28f330845ad21c11db51e02d8d69acc9035edfd1116926ff7245c7215db57957"}, + {file = "pandas-2.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9a6ccf0963db88f9b12df6720e55f337447aea217f426a22d71f4213a3099a6"}, + {file = "pandas-2.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d99e678180bc59b0c9443314297bddce4ad35727a1a2656dbe585fd78710b3b9"}, + {file = "pandas-2.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b31da36d376d50a1a492efb18097b9101bdbd8b3fbb3f49006e02d4495d4c644"}, + {file = "pandas-2.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:0164b85937707ec7f70b34a6c3a578dbf0f50787f910f21ca3b26a7fd3363437"}, + {file = "pandas-2.1.0.tar.gz", hash = "sha256:62c24c7fc59e42b775ce0679cfa7b14a5f9bfb7643cfbe708c960699e05fb918"}, ] [package.dependencies] -numpy = [ - {version = ">=1.21.0", markers = "python_version >= \"3.10\""}, - {version = ">=1.23.2", markers = "python_version >= \"3.11\""}, -] +numpy = {version = ">=1.23.2", markers = "python_version >= \"3.11\""} python-dateutil = ">=2.8.2" pytz = ">=2020.1" tzdata = ">=2022.1" [package.extras] -all = ["PyQt5 (>=5.15.1)", "SQLAlchemy (>=1.4.16)", "beautifulsoup4 (>=4.9.3)", "bottleneck (>=1.3.2)", "brotlipy (>=0.7.0)", "fastparquet (>=0.6.3)", "fsspec (>=2021.07.0)", "gcsfs (>=2021.07.0)", "html5lib (>=1.1)", "hypothesis (>=6.34.2)", "jinja2 (>=3.0.0)", "lxml (>=4.6.3)", "matplotlib (>=3.6.1)", "numba (>=0.53.1)", "numexpr (>=2.7.3)", "odfpy (>=1.4.1)", "openpyxl (>=3.0.7)", "pandas-gbq (>=0.15.0)", "psycopg2 (>=2.8.6)", "pyarrow (>=7.0.0)", "pymysql (>=1.0.2)", "pyreadstat (>=1.1.2)", "pytest (>=7.3.2)", "pytest-asyncio (>=0.17.0)", "pytest-xdist (>=2.2.0)", "python-snappy (>=0.6.0)", "pyxlsb (>=1.0.8)", "qtpy (>=2.2.0)", "s3fs (>=2021.08.0)", "scipy (>=1.7.1)", "tables (>=3.6.1)", "tabulate (>=0.8.9)", "xarray (>=0.21.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=1.4.3)", "zstandard (>=0.15.2)"] -aws = ["s3fs (>=2021.08.0)"] -clipboard = ["PyQt5 (>=5.15.1)", "qtpy (>=2.2.0)"] -compression = ["brotlipy (>=0.7.0)", "python-snappy (>=0.6.0)", "zstandard (>=0.15.2)"] -computation = ["scipy (>=1.7.1)", "xarray (>=0.21.0)"] -excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.0.7)", "pyxlsb (>=1.0.8)", "xlrd (>=2.0.1)", "xlsxwriter (>=1.4.3)"] +all = ["PyQt5 (>=5.15.6)", "SQLAlchemy (>=1.4.36)", "beautifulsoup4 (>=4.11.1)", "bottleneck (>=1.3.4)", "dataframe-api-compat (>=0.1.7)", "fastparquet (>=0.8.1)", "fsspec (>=2022.05.0)", "gcsfs (>=2022.05.0)", "html5lib (>=1.1)", "hypothesis (>=6.46.1)", "jinja2 (>=3.1.2)", "lxml (>=4.8.0)", "matplotlib (>=3.6.1)", "numba (>=0.55.2)", "numexpr (>=2.8.0)", "odfpy (>=1.4.1)", "openpyxl (>=3.0.10)", "pandas-gbq (>=0.17.5)", "psycopg2 (>=2.9.3)", "pyarrow (>=7.0.0)", "pymysql (>=1.0.2)", "pyreadstat (>=1.1.5)", "pytest (>=7.3.2)", "pytest-asyncio (>=0.17.0)", "pytest-xdist (>=2.2.0)", "pyxlsb (>=1.0.9)", "qtpy (>=2.2.0)", "s3fs (>=2022.05.0)", "scipy (>=1.8.1)", "tables (>=3.7.0)", "tabulate (>=0.8.10)", "xarray (>=2022.03.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.3)", "zstandard (>=0.17.0)"] +aws = ["s3fs (>=2022.05.0)"] +clipboard = ["PyQt5 (>=5.15.6)", "qtpy (>=2.2.0)"] +compression = ["zstandard (>=0.17.0)"] +computation = ["scipy (>=1.8.1)", "xarray (>=2022.03.0)"] +consortium-standard = ["dataframe-api-compat (>=0.1.7)"] +excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.0.10)", "pyxlsb (>=1.0.9)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.3)"] feather = ["pyarrow (>=7.0.0)"] -fss = ["fsspec (>=2021.07.0)"] -gcp = ["gcsfs (>=2021.07.0)", "pandas-gbq (>=0.15.0)"] -hdf5 = ["tables (>=3.6.1)"] -html = ["beautifulsoup4 (>=4.9.3)", "html5lib (>=1.1)", "lxml (>=4.6.3)"] -mysql = ["SQLAlchemy (>=1.4.16)", "pymysql (>=1.0.2)"] -output-formatting = ["jinja2 (>=3.0.0)", "tabulate (>=0.8.9)"] +fss = ["fsspec (>=2022.05.0)"] +gcp = ["gcsfs (>=2022.05.0)", "pandas-gbq (>=0.17.5)"] +hdf5 = ["tables (>=3.7.0)"] +html = ["beautifulsoup4 (>=4.11.1)", "html5lib (>=1.1)", "lxml (>=4.8.0)"] +mysql = ["SQLAlchemy (>=1.4.36)", "pymysql (>=1.0.2)"] +output-formatting = ["jinja2 (>=3.1.2)", "tabulate (>=0.8.10)"] parquet = ["pyarrow (>=7.0.0)"] -performance = ["bottleneck (>=1.3.2)", "numba (>=0.53.1)", "numexpr (>=2.7.1)"] +performance = ["bottleneck (>=1.3.4)", "numba (>=0.55.2)", "numexpr (>=2.8.0)"] plot = ["matplotlib (>=3.6.1)"] -postgresql = ["SQLAlchemy (>=1.4.16)", "psycopg2 (>=2.8.6)"] -spss = ["pyreadstat (>=1.1.2)"] -sql-other = ["SQLAlchemy (>=1.4.16)"] -test = ["hypothesis (>=6.34.2)", "pytest (>=7.3.2)", "pytest-asyncio (>=0.17.0)", "pytest-xdist (>=2.2.0)"] -xml = ["lxml (>=4.6.3)"] +postgresql = ["SQLAlchemy (>=1.4.36)", "psycopg2 (>=2.9.3)"] +spss = ["pyreadstat (>=1.1.5)"] +sql-other = ["SQLAlchemy (>=1.4.36)"] +test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-asyncio (>=0.17.0)", "pytest-xdist (>=2.2.0)"] +xml = ["lxml (>=4.8.0)"] [[package]] name = "pathos" @@ -898,13 +906,13 @@ dill = ["dill (>=0.3.7)"] [[package]] name = "pre-commit" -version = "3.3.3" +version = "3.4.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false python-versions = ">=3.8" files = [ - {file = "pre_commit-3.3.3-py2.py3-none-any.whl", hash = "sha256:10badb65d6a38caff29703362271d7dca483d01da88f9d7e05d0b97171c136cb"}, - {file = "pre_commit-3.3.3.tar.gz", hash = "sha256:a2256f489cd913d575c145132ae196fe335da32d91a8294b7afe6622335dd023"}, + {file = "pre_commit-3.4.0-py2.py3-none-any.whl", hash = "sha256:96d529a951f8b677f730a7212442027e8ba53f9b04d217c4c67dc56c393ad945"}, + {file = "pre_commit-3.4.0.tar.gz", hash = "sha256:6bbd5129a64cad4c0dfaeeb12cd8f7ea7e15b77028d985341478c8af3c759522"}, ] [package.dependencies] @@ -1139,13 +1147,13 @@ typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" [[package]] name = "pyright" -version = "1.1.324" +version = "1.1.326" description = "Command line wrapper for pyright" optional = false python-versions = ">=3.7" files = [ - {file = "pyright-1.1.324-py3-none-any.whl", hash = "sha256:0edb712afbbad474e347de12ca1bd9368aa85d3365a1c7b795012e48e6a65111"}, - {file = "pyright-1.1.324.tar.gz", hash = "sha256:0c48e3bca3d081bba0dddd0c1f075aaa965c59bba691f7b9bd9d73a98e44e0cf"}, + {file = "pyright-1.1.326-py3-none-any.whl", hash = "sha256:f3c5047465138558d3d106a9464cc097cf2c3611da6edcf5b535cc1fdebd45db"}, + {file = "pyright-1.1.326.tar.gz", hash = "sha256:cecbe026b14034ba0750db605718a8c2605552387c5772dfaf7f3e632cb7212a"}, ] [package.dependencies] @@ -1157,13 +1165,13 @@ dev = ["twine (>=3.4.1)"] [[package]] name = "pytest" -version = "7.4.0" +version = "7.4.2" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-7.4.0-py3-none-any.whl", hash = "sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32"}, - {file = "pytest-7.4.0.tar.gz", hash = "sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a"}, + {file = "pytest-7.4.2-py3-none-any.whl", hash = "sha256:1d881c6124e08ff0a1bb75ba3ec0bfd8b5354a01c194ddd5a0a870a48d99b002"}, + {file = "pytest-7.4.2.tar.gz", hash = "sha256:a766259cfab564a2ad52cb1aae1b881a75c3eb7e34ca3779697c23ed47c47069"}, ] [package.dependencies] @@ -1253,13 +1261,13 @@ dev = ["atomicwrites (==1.2.1)", "attrs (==19.2.0)", "coverage (==6.5.0)", "hatc [[package]] name = "pytz" -version = "2023.3" +version = "2023.3.post1" description = "World timezone definitions, modern and historical" optional = false python-versions = "*" files = [ - {file = "pytz-2023.3-py2.py3-none-any.whl", hash = "sha256:a151b3abb88eda1d4e34a9814df37de2a80e301e68ba0fd856fb9b46bfbbbffb"}, - {file = "pytz-2023.3.tar.gz", hash = "sha256:1d8ce29db189191fb55338ee6d0387d82ab59f3d00eac103412d64e0ebd0c588"}, + {file = "pytz-2023.3.post1-py2.py3-none-any.whl", hash = "sha256:ce42d816b81b68506614c11e8937d3aa9e41007ceb50bfdcb0749b921bf646c7"}, + {file = "pytz-2023.3.post1.tar.gz", hash = "sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b"}, ] [[package]] @@ -1274,6 +1282,7 @@ files = [ {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-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, {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"}, @@ -1281,8 +1290,15 @@ files = [ {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-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, {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-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, {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"}, @@ -1299,6 +1315,7 @@ files = [ {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-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, {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"}, @@ -1306,6 +1323,7 @@ files = [ {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-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, {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"}, @@ -1362,28 +1380,28 @@ pyasn1 = ">=0.1.3" [[package]] name = "ruff" -version = "0.0.286" +version = "0.0.287" description = "An extremely fast Python linter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.0.286-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:8e22cb557e7395893490e7f9cfea1073d19a5b1dd337f44fd81359b2767da4e9"}, - {file = "ruff-0.0.286-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:68ed8c99c883ae79a9133cb1a86d7130feee0397fdf5ba385abf2d53e178d3fa"}, - {file = "ruff-0.0.286-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8301f0bb4ec1a5b29cfaf15b83565136c47abefb771603241af9d6038f8981e8"}, - {file = "ruff-0.0.286-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:acc4598f810bbc465ce0ed84417ac687e392c993a84c7eaf3abf97638701c1ec"}, - {file = "ruff-0.0.286-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88c8e358b445eb66d47164fa38541cfcc267847d1e7a92dd186dddb1a0a9a17f"}, - {file = "ruff-0.0.286-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:0433683d0c5dbcf6162a4beb2356e820a593243f1fa714072fec15e2e4f4c939"}, - {file = "ruff-0.0.286-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ddb61a0c4454cbe4623f4a07fef03c5ae921fe04fede8d15c6e36703c0a73b07"}, - {file = "ruff-0.0.286-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:47549c7c0be24c8ae9f2bce6f1c49fbafea83bca80142d118306f08ec7414041"}, - {file = "ruff-0.0.286-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:559aa793149ac23dc4310f94f2c83209eedb16908a0343663be19bec42233d25"}, - {file = "ruff-0.0.286-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d73cfb1c3352e7aa0ce6fb2321f36fa1d4a2c48d2ceac694cb03611ddf0e4db6"}, - {file = "ruff-0.0.286-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:3dad93b1f973c6d1db4b6a5da8690c5625a3fa32bdf38e543a6936e634b83dc3"}, - {file = "ruff-0.0.286-py3-none-musllinux_1_2_i686.whl", hash = "sha256:26afc0851f4fc3738afcf30f5f8b8612a31ac3455cb76e611deea80f5c0bf3ce"}, - {file = "ruff-0.0.286-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9b6b116d1c4000de1b9bf027131dbc3b8a70507788f794c6b09509d28952c512"}, - {file = "ruff-0.0.286-py3-none-win32.whl", hash = "sha256:556e965ac07c1e8c1c2d759ac512e526ecff62c00fde1a046acb088d3cbc1a6c"}, - {file = "ruff-0.0.286-py3-none-win_amd64.whl", hash = "sha256:5d295c758961376c84aaa92d16e643d110be32add7465e197bfdaec5a431a107"}, - {file = "ruff-0.0.286-py3-none-win_arm64.whl", hash = "sha256:1d6142d53ab7f164204b3133d053c4958d4d11ec3a39abf23a40b13b0784e3f0"}, - {file = "ruff-0.0.286.tar.gz", hash = "sha256:f1e9d169cce81a384a26ee5bb8c919fe9ae88255f39a1a69fd1ebab233a85ed2"}, + {file = "ruff-0.0.287-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:1e0f9ee4c3191444eefeda97d7084721d9b8e29017f67997a20c153457f2eafd"}, + {file = "ruff-0.0.287-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:e9843e5704d4fb44e1a8161b0d31c1a38819723f0942639dfeb53d553be9bfb5"}, + {file = "ruff-0.0.287-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca1ed11d759a29695aed2bfc7f914b39bcadfe2ef08d98ff69c873f639ad3a8"}, + {file = "ruff-0.0.287-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1cf4d5ad3073af10f186ea22ce24bc5a8afa46151f6896f35c586e40148ba20b"}, + {file = "ruff-0.0.287-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66d9d58bcb29afd72d2afe67120afcc7d240efc69a235853813ad556443dc922"}, + {file = "ruff-0.0.287-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:06ac5df7dd3ba8bf83bba1490a72f97f1b9b21c7cbcba8406a09de1a83f36083"}, + {file = "ruff-0.0.287-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2bfb478e1146a60aa740ab9ebe448b1f9e3c0dfb54be3cc58713310eef059c30"}, + {file = "ruff-0.0.287-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:00d579a011949108c4b4fa04c4f1ee066dab536a9ba94114e8e580c96be2aeb4"}, + {file = "ruff-0.0.287-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3a810a79b8029cc92d06c36ea1f10be5298d2323d9024e1d21aedbf0a1a13e5"}, + {file = "ruff-0.0.287-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:150007028ad4976ce9a7704f635ead6d0e767f73354ce0137e3e44f3a6c0963b"}, + {file = "ruff-0.0.287-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a24a280db71b0fa2e0de0312b4aecb8e6d08081d1b0b3c641846a9af8e35b4a7"}, + {file = "ruff-0.0.287-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2918cb7885fa1611d542de1530bea3fbd63762da793751cc8c8d6e4ba234c3d8"}, + {file = "ruff-0.0.287-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:33d7b251afb60bec02a64572b0fd56594b1923ee77585bee1e7e1daf675e7ae7"}, + {file = "ruff-0.0.287-py3-none-win32.whl", hash = "sha256:022f8bed2dcb5e5429339b7c326155e968a06c42825912481e10be15dafb424b"}, + {file = "ruff-0.0.287-py3-none-win_amd64.whl", hash = "sha256:26bd0041d135a883bd6ab3e0b29c42470781fb504cf514e4c17e970e33411d90"}, + {file = "ruff-0.0.287-py3-none-win_arm64.whl", hash = "sha256:44bceb3310ac04f0e59d4851e6227f7b1404f753997c7859192e41dbee9f5c8d"}, + {file = "ruff-0.0.287.tar.gz", hash = "sha256:02dc4f5bf53ef136e459d467f3ce3e04844d509bc46c025a05b018feb37bbc39"}, ] [[package]] @@ -1468,19 +1486,19 @@ test = ["asv", "gmpy2", "mpmath", "pytest", "pytest-cov", "pytest-xdist", "sciki [[package]] name = "setuptools" -version = "68.1.2" +version = "68.2.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-68.1.2-py3-none-any.whl", hash = "sha256:3d8083eed2d13afc9426f227b24fd1659489ec107c0e86cec2ffdde5c92e790b"}, - {file = "setuptools-68.1.2.tar.gz", hash = "sha256:3d4dfa6d95f1b101d695a6160a7626e15583af71a5f52176efa5d39a054d475d"}, + {file = "setuptools-68.2.0-py3-none-any.whl", hash = "sha256:af3d5949030c3f493f550876b2fd1dd5ec66689c4ee5d5344f009746f71fd5a8"}, + {file = "setuptools-68.2.0.tar.gz", hash = "sha256:00478ca80aeebeecb2f288d3206b0de568df5cd2b8fada1209843cc9a8d88a48"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5,<=7.1.2)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "six" @@ -1555,7 +1573,7 @@ files = [ ] [package.dependencies] -greenlet = {version = "!=0.4.17", markers = "platform_machine == \"win32\" or platform_machine == \"WIN32\" or platform_machine == \"AMD64\" or platform_machine == \"amd64\" or platform_machine == \"x86_64\" or platform_machine == \"ppc64le\" or platform_machine == \"aarch64\""} +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.2.0" [package.extras] @@ -1669,13 +1687,13 @@ standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", [[package]] name = "virtualenv" -version = "20.24.3" +version = "20.24.5" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.7" files = [ - {file = "virtualenv-20.24.3-py3-none-any.whl", hash = "sha256:95a6e9398b4967fbcb5fef2acec5efaf9aa4972049d9ae41f95e0972a683fd02"}, - {file = "virtualenv-20.24.3.tar.gz", hash = "sha256:e5c3b4ce817b0b328af041506a2a299418c98747c4b1e68cb7527e74ced23efc"}, + {file = "virtualenv-20.24.5-py3-none-any.whl", hash = "sha256:b80039f280f4919c77b30f1c23294ae357c4c8701042086e3fc005963e4e537b"}, + {file = "virtualenv-20.24.5.tar.gz", hash = "sha256:e8361967f6da6fbdf1426483bfe9fca8287c242ac0bc30429905721cefbff752"}, ] [package.dependencies] @@ -1684,7 +1702,7 @@ filelock = ">=3.12.2,<4" platformdirs = ">=3.9.1,<4" [package.extras] -docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx (>=7.0.1)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +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]] @@ -1716,4 +1734,4 @@ scikit-learn = ">=1.2.2,<2.0.0" [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "c0ffd6c655304ec187c0da456dfb2e6c143d5b9a5d22dcc6fb1113c869e88886" +content-hash = "c06e454064e69a89ce2bd9bc928a54efcaa10ba11f554de7fb6eb6bf2e9340f4" diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 7c7fd4a6..ec477b65 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -10,11 +10,11 @@ homepage = "https://zenoml.com" [tool.poetry.dependencies] aiofiles = "^23.2.1" cognitojwt = "^1.4.1" -fastapi = "0.103.0" +fastapi = "^0.103.1" fastapi-cloudauth = "^0.4.3" inquirer = "^3.1.2" nest-asyncio = "^1.5.6" -pandas = "^2.0.0" +pandas = "^2.1.0" pathos = "^0.3.1" psycopg = "^3.1.10" python = "^3.11" @@ -25,13 +25,14 @@ uvicorn = "^0.23.2" zeno-sliceline = "^0.0.1" pyarrow = "^13.0.0" sqlalchemy = "^2.0.20" +amplitude-analytics = "^1.1.3" [tool.poetry.dev-dependencies] -black = "^23.3.0" -pre-commit = "^3.3.3" +black = "^23.9.1" +pre-commit = "^3.4.0" pyright = "^1.1.323" -pytest = "^7.3.2" -ruff = "^0.0.286" +pytest = "^7.4.1" +ruff = "^0.0.287" [build-system] requires = ["poetry-core"] diff --git a/backend/zeno_backend/classes/chart.py b/backend/zeno_backend/classes/chart.py index 7e1b6c09..97a909f2 100644 --- a/backend/zeno_backend/classes/chart.py +++ b/backend/zeno_backend/classes/chart.py @@ -121,3 +121,15 @@ def default( object: a dict to be encoded by a JSON encoder and saved into the database. """ return o.__dict__ + + +class ChartResponse(CamelModel): + """Chart specification and data. + + Parameters: + chart (Chart): The chart specification. + chart_data (str): The chart data in JSON string. + """ + + chart: Chart + chart_data: str diff --git a/backend/zeno_backend/classes/filter.py b/backend/zeno_backend/classes/filter.py index e1207c51..5bc6bf81 100644 --- a/backend/zeno_backend/classes/filter.py +++ b/backend/zeno_backend/classes/filter.py @@ -16,6 +16,8 @@ class Operation(str, Enum): LTE = "LTE" GTE = "GTE" LIKE = "LIKE" + ILIKE = "ILIKE" + REGEX = "REGEX" def literal(self) -> LiteralString: """Obtain a string representation to be used in a SQL filter. @@ -35,6 +37,10 @@ def literal(self) -> LiteralString: return ">=" if self == Operation.LTE: return "<=" + if self == Operation.ILIKE: + return "ILIKE" + if self == Operation.REGEX: + return "~" return "LIKE" diff --git a/backend/zeno_backend/classes/metadata.py b/backend/zeno_backend/classes/metadata.py index 13746b94..a3ced981 100644 --- a/backend/zeno_backend/classes/metadata.py +++ b/backend/zeno_backend/classes/metadata.py @@ -1,5 +1,6 @@ """Type representations for metric data.""" from zeno_backend.classes.base import CamelModel, ZenoColumn +from zeno_backend.classes.filter import Operation from zeno_backend.classes.metric import Metric from zeno_backend.classes.slice import FilterPredicateGroup @@ -9,6 +10,9 @@ class HistogramBucket(CamelModel): bucket: float | bool | int | str bucket_end: float | bool | int | str | None = None + size: int | None = None + filtered_size: int | None = None + metric: float | None = None class HistogramColumnRequest(CamelModel): @@ -33,6 +37,4 @@ class StringFilterRequest(CamelModel): column: ZenoColumn filter_string: str - is_regex: bool - case_match: bool - whole_word_match: bool + operation: Operation diff --git a/backend/zeno_backend/classes/project.py b/backend/zeno_backend/classes/project.py index b9a33ced..ea928859 100644 --- a/backend/zeno_backend/classes/project.py +++ b/backend/zeno_backend/classes/project.py @@ -1,7 +1,10 @@ """Types for Zeno projects.""" -from zeno_backend.classes.base import CamelModel +from zeno_backend.classes.base import CamelModel, ZenoColumn +from zeno_backend.classes.folder import Folder from zeno_backend.classes.metric import Metric +from zeno_backend.classes.slice import Slice +from zeno_backend.classes.tag import Tag class Project(CamelModel): @@ -45,3 +48,25 @@ class ProjectStats(CamelModel): num_instances: int num_charts: int num_models: int + + +class ProjectState(CamelModel): + """State variables for a Zeno project. + + Attributes: + project (Project): The project object with project metadata. + models (list[str]): The names of the models in the project. + metrics (list[Metric]): The metrics to calculate for the project. + columns (list[ZenoColumn]): The columns in the project. + slices (list[Slice]): The slices in the project. + tags (list[Tag]): The tags in the project. + folders (list[Folder]): The folders in the project. + """ + + project: Project + models: list[str] + metrics: list[Metric] + columns: list[ZenoColumn] + slices: list[Slice] + tags: list[Tag] + folders: list[Folder] diff --git a/backend/zeno_backend/classes/table.py b/backend/zeno_backend/classes/table.py index 83b487f2..0c99ca10 100644 --- a/backend/zeno_backend/classes/table.py +++ b/backend/zeno_backend/classes/table.py @@ -8,6 +8,7 @@ class TableRequest(CamelModel): """A request specification for table data.""" columns: list[ZenoColumn] + model: str | None = None diff_column_1: ZenoColumn | None = None diff_column_2: ZenoColumn | None = None offset: int diff --git a/backend/zeno_backend/database/insert.py b/backend/zeno_backend/database/insert.py index 240a90ef..5eadf685 100644 --- a/backend/zeno_backend/database/insert.py +++ b/backend/zeno_backend/database/insert.py @@ -279,12 +279,20 @@ def system( with Database() as db: for col in columns: + data_type = db.execute_return( + sql.SQL( + "SELECT data_type FROM information_schema.columns WHERE " + "table_schema = 'public' AND table_name = 'tmp' AND " + "column_name = {};" + ).format(sql.Literal(col.id)) + ) + db.execute( - sql.SQL("ALTER TABLE {} ADD {}{};").format( + sql.SQL("ALTER TABLE {} ADD {} ").format( sql.Identifier(project), sql.Identifier(col.id), - sql.Literal(col.data_type), ) + + sql.SQL(data_type[0][0] + ";"), ) db.execute( sql.SQL( @@ -322,31 +330,40 @@ def report(name: str, user: User): ) -def folder(project: str, name: str): +def folder(project: str, name: str) -> int | None: """Adding a folder to an existing project. Args: project (str): the project the user is currently working with. name (str): name of the folder to be added. + + + Returns: + int | None: the id of the newly created folder. """ db = Database() - db.connect_execute( - "INSERT INTO folders (name, project_uuid) VALUES (%s,%s);", + id = db.connect_execute_return( + "INSERT INTO folders (name, project_uuid) VALUES (%s,%s) RETURNING id;", [name, project], ) + if id is not None and len(id) > 0: + return id[0][0] -def slice(project: str, req: Slice): +def slice(project: str, req: Slice) -> int | None: """Add a slice to an existing project. Args: project (str): the project the user is currently working with. req (Slice): the slice to be added to the project. + + Returns: + int | None: the id of the newly created slice. """ db = Database() - db.connect_execute( + ids = db.connect_execute_return( "INSERT INTO slices (name, folder_id, filter, project_uuid) " - "VALUES (%s,%s,%s,%s);", + "VALUES (%s,%s,%s,%s) RETURNING id;", [ req.slice_name, req.folder_id, @@ -354,19 +371,27 @@ def slice(project: str, req: Slice): project, ], ) + if ids is not None: + return ids[0][0] + else: + return None -def chart(project: str, chart: Chart): +def chart(project: str, chart: Chart) -> int | None: """Add a chart to an existing project. Args: project (str): the project the user is currently working with. chart (Chart): the chart to be added to the project. + + + Returns: + int | None: the id of the newly created chart. """ db = Database() - db.connect_execute( + id = db.connect_execute_return( "INSERT INTO charts (name, type, parameters, project_uuid) " - "VALUES (%s,%s,%s,%s);", + "VALUES (%s,%s,%s,%s) RETURNING id;", [ chart.name, chart.type, @@ -374,14 +399,19 @@ def chart(project: str, chart: Chart): project, ], ) + if id is not None and len(id) > 0: + return id[0][0] -def tag(project: str, tag: Tag): +def tag(project: str, tag: Tag) -> int | None: """Add a tag to an existing project. Args: project (str): the project the user is currently working with. tag (Tag): the tag to be added to the project. + + Returns: + int | None: the id of the newly created tag. """ with Database() as db: id = db.execute_return( @@ -389,29 +419,32 @@ def tag(project: str, tag: Tag): "RETURNING id;", [tag.tag_name, tag.folder_id, project], ) - if id is None: + if id is None or len(id) == 0: return for datapoint in tag.data_ids: db.execute( sql.SQL("INSERT INTO {} (tag_id, data_id) VALUES (%s,%s);").format( sql.Identifier(f"{project}_tags_datapoints") ), - [id[0], datapoint], + [id[0][0], datapoint], ) db.commit() + return id[0][0] -def user(user: User): +def user(user: User) -> int | None: """Add a new user to the database. Args: user (User): the user to be added. """ db = Database() - db.connect_execute( - 'INSERT INTO users ("name") values(%s)', + id = db.connect_execute_return( + 'INSERT INTO users ("name") values(%s) RETURNING id;', [user.name], ) + if id is not None: + return id[0][0] def organization(user: User, organization: Organization): diff --git a/backend/zeno_backend/database/select.py b/backend/zeno_backend/database/select.py index 91ebf028..ff6c63b1 100644 --- a/backend/zeno_backend/database/select.py +++ b/backend/zeno_backend/database/select.py @@ -1,16 +1,15 @@ """Functions to select data from the database.""" import json -import re from psycopg import sql from zeno_backend.classes.base import ZenoColumn from zeno_backend.classes.chart import Chart -from zeno_backend.classes.filter import FilterPredicateGroup, Join +from zeno_backend.classes.filter import FilterPredicateGroup, Join, Operation from zeno_backend.classes.folder import Folder from zeno_backend.classes.metadata import StringFilterRequest from zeno_backend.classes.metric import Metric -from zeno_backend.classes.project import Project, ProjectStats +from zeno_backend.classes.project import Project, ProjectState, ProjectStats from zeno_backend.classes.report import Report, ReportElement from zeno_backend.classes.slice import Slice from zeno_backend.classes.slice_finder import SQLTable @@ -674,6 +673,130 @@ def report_elements(report_id: int) -> list[ReportElement] | None: ) ) +def project_state(project_uuid: str, project: Project) -> ProjectState | None: + """Get the state variables of a project. + + Args: + project_uuid (str): the uuid of the project to be fetched. + project (Project): the project object with project metadata. + + Returns: + ProjectState | None: state variables of the requested project. + """ + with Database() as db: + metric_results = db.execute_return( + "SELECT id, name, type, columns FROM metrics WHERE project_uuid = %s;", + [project_uuid], + ) + metrics = list( + map( + lambda metric: Metric( + id=metric[0], + name=metric[1], + type=metric[2], + columns=metric[3], + ), + metric_results, + ) + ) + + slice_results = db.execute_return( + "SELECT id, name, folder_id, filter FROM slices WHERE project_uuid = %s;", + [ + project_uuid, + ], + ) + slices = list( + map( + lambda slice: Slice( + id=slice[0], + slice_name=slice[1], + folder_id=slice[2], + filter_predicates=FilterPredicateGroup( + predicates=json.loads(slice[3])["predicates"], + join=Join.OMITTED, + ), + ), + slice_results, + ) + ) + + folder_results = db.execute_return( + "SELECT id, name, project_uuid FROM folders WHERE project_uuid = %s;", + [ + project_uuid, + ], + ) + folders = list( + map( + lambda folder: Folder(id=folder[0], name=folder[1]), + folder_results, + ) + ) + + tags_result = db.execute_return( + "SELECT id, name, folder_id FROM tags WHERE project_uuid = %s", + [ + project_uuid, + ], + ) + tags: list[Tag] = [] + for tag_result in tags_result: + data_results = db.execute_return( + sql.SQL("SELECT data_id FROM {} WHERE tag_id = %s").format( + sql.Identifier(f"{project_uuid}_tags_datapoints") + ), + [ + tag_result[0], + ], + ) + tags.append( + Tag( + id=tag_result[0], + tag_name=tag_result[1], + folder_id=tag_result[2], + data_ids=[] + if len(data_results) == 0 + else [d[0] for d in data_results], + ) + ) + + column_results = db.execute_return( + sql.SQL("SELECT column_id, name, type, model, data_type FROM {};").format( + sql.Identifier(f"{project_uuid}_column_map") + ), + ) + + columns = list( + map( + lambda column: ZenoColumn( + id=column[0], + name=column[1], + column_type=column[2], + model=column[3], + data_type=column[4], + ), + column_results, + ) + ) + + model_results = db.execute_return( + sql.SQL("SELECT DISTINCT model FROM {} WHERE model IS NOT NULL;").format( + sql.Identifier(f"{project_uuid}_column_map") + ), + ) + models = [m[0] for m in model_results] + + return ProjectState( + project=project, + metrics=metrics, + folders=folders, + columns=columns, + slices=slices, + tags=tags, + models=models, + ) + def project_stats(project: str) -> ProjectStats | None: """Get statistics for a specified project. @@ -747,28 +870,23 @@ def metrics_by_id(metric_ids: list[int], project_uuid: str) -> dict[int, Metric list[Metric]: list of metrics as requested by the user. """ db = Database() - # Get all metrics for the project and filter out the ones that are not in the list metric_results = db.connect_execute_return( - "SELECT id, name, type, columns FROM metrics WHERE project_uuid = %s;", - [project_uuid], + "SELECT id, name, type, columns FROM metrics" + " WHERE project_uuid = %s AND id = ANY(%s);", + [project_uuid, [metric_ids]], ) if metric_results is None: return {} - returned_ids = list(map(lambda metric: metric[0], metric_results)) - metric_objects = {} - for i, requested_metric in enumerate(metric_ids): - if requested_metric not in returned_ids: - metric_objects[requested_metric] = None - else: - metric_objects[requested_metric] = Metric( - id=requested_metric, - name=metric_results[i][1], - type=metric_results[i][2], - columns=metric_results[i][3], - ) - - return metric_objects + ret = {} + for m in metric_results: + ret[m[0]] = Metric( + id=m[0], + name=m[1], + type=m[2], + columns=m[3], + ) + return ret def folders(project: str) -> list[Folder]: @@ -867,6 +985,36 @@ def slices(project: str, ids: list[int] | None = None) -> list[Slice]: ) +def chart(project_uuid: str, chart_id: int): + """Get a project chart by its ID. + + Args: + project_uuid (str): the project the user is currently working with. + chart_id (int): the ID of the chart to be fetched. + + + Returns: + Chart | None: the requested chart. + """ + db = Database() + chart_result = db.connect_execute_return( + "SELECT id, name, type, parameters FROM " + "charts WHERE id = %s AND project_uuid = %s;", + [ + chart_id, + project_uuid, + ], + ) + if len(chart_result) == 0: + return None + return Chart( + id=chart_result[0][0], + name=chart_result[0][1], + type=chart_result[0][2], + parameters=json.loads(chart_result[0][3]), + ) + + def charts(project: str) -> list[Chart]: """Get a list of all charts created in the project. @@ -977,7 +1125,7 @@ def table_data_paginated( db.cur.execute( sql.SQL("SELECT * FROM {} WHERE ").format(sql.Identifier(project)) + filter_sql - + sql.SQL("ORDER BY {} {} LIMIT {} OFFSET {};").format( + + sql.SQL(" ORDER BY {} {} LIMIT {} OFFSET {};").format( sql.Identifier(sort_by[0].id if sort_by[0] else "data_id"), sql.SQL("DESC" if sort_by[1] else "ASC"), sql.Literal(limit), @@ -1126,7 +1274,7 @@ def tags(project: str) -> list[Tag]: folder_id=tag_result[2], data_ids=[] if len(data_results) == 0 - else list(map(lambda d: d[0], data_results[0])), + else [d[0] for d in data_results], ) ) return tags @@ -1145,7 +1293,7 @@ def user(name: str) -> User | None: user = db.connect_execute_return( "SELECT id, name FROM users WHERE name = %s", [name] ) - if len(user) is None: + if len(user) == 0: return None user = user[0] return User( @@ -1282,7 +1430,7 @@ def project_orgs(project: str) -> list[Organization]: ) -def filered_short_string_column_values( +def filtered_short_string_column_values( project: str, req: StringFilterRequest ) -> list[str]: """Select distinct string values of a column and return their short representation. @@ -1296,65 +1444,33 @@ def filered_short_string_column_values( list[str]: the filtered string column data. """ db = Database() - short_ret: list[str] = [] - if not req.is_regex: - if not req.whole_word_match: - req.filter_string = f"%{req.filter_string}%" - if not req.case_match: - req.filter_string = req.filter_string.lower() - returned_strings = db.connect_execute_return( - sql.SQL("SELECT {} from {} WHERE UPPER({}) LIKE %s;").format( - sql.Identifier(req.column.id), - sql.Identifier(project), - sql.Identifier(req.column.id), - ), - [ - req.filter_string, - ], - ) - else: - returned_strings = db.connect_execute_return( - sql.SQL("SELECT {} from {} WHERE {} LIKE %s").format( - sql.Identifier(req.column.id), - sql.Identifier(project), - sql.Identifier(req.column.id), - ), - [ - req.filter_string, - ], - ) - if len(returned_strings) == 0: - return short_ret - for result in returned_strings[0:5]: - idx = result[0].find(req.filter_string) - loc_str = result[0][0 if idx < 20 else idx - 20 : idx + 20] - if len(result[0]) > 40 + len(req.filter_string): - if idx - 20 > 0: - loc_str = "..." + loc_str - if idx + 20 < len(result[0]): - loc_str = loc_str + "..." - short_ret.append(loc_str) + if req.operation == Operation.LIKE or req.operation == Operation.ILIKE: + req.filter_string = "%" + req.filter_string + "%" - else: - returned_strings = db.connect_execute_return( - sql.SQL("SELECT {} from {} WHERE {} LIKE %s").format( - sql.Identifier(req.column.id), - sql.Identifier(project), - sql.Identifier(req.column.id), - ), - [ - req.filter_string, - ], - ) + returned_strings = db.connect_execute_return( + sql.SQL("SELECT {} from {} WHERE {} {} %s;").format( + sql.Identifier(req.column.id), + sql.Identifier(project), + sql.Identifier(req.column.id), + sql.SQL(req.operation.literal()), + ), + [ + req.filter_string, + ], + ) - if len(returned_strings) == 0: - return short_ret - for result in returned_strings: - idx = re.search(req.filter_string, result[0]) - if idx is not None: - idx = idx.start() - loc_str = result[0][0 if idx < 20 else idx - 20 : idx + 20] - short_ret.append(loc_str) + short_ret: list[str] = [] + if len(returned_strings) == 0: + return short_ret + for result in returned_strings[0:5]: + idx = result[0].find(req.filter_string) + loc_str = result[0][0 if idx < 20 else idx - 20 : idx + 20] + if len(result[0]) > 40 + len(req.filter_string): + if idx - 20 > 0: + loc_str = "..." + loc_str + if idx + 20 < len(result[0]): + loc_str = loc_str + "..." + short_ret.append(loc_str) return short_ret diff --git a/backend/zeno_backend/database/update.py b/backend/zeno_backend/database/update.py index a9ee1e6a..f10a7e77 100644 --- a/backend/zeno_backend/database/update.py +++ b/backend/zeno_backend/database/update.py @@ -96,7 +96,7 @@ def tag(tag: Tag, project: str): tag.id, ], ) - if len(data_ids_result) == 0: + if data_ids_result is None: return existing_data = set(map(lambda d: d[0], data_ids_result)) diff --git a/backend/zeno_backend/database/util.py b/backend/zeno_backend/database/util.py index b046c173..365058de 100644 --- a/backend/zeno_backend/database/util.py +++ b/backend/zeno_backend/database/util.py @@ -18,6 +18,8 @@ def resolve_metadata_type(data_frame: pd.DataFrame, column: str) -> MetadataType """ dtype = data_frame[column].dtype if pd.api.types.is_any_real_numeric_dtype(dtype): + if data_frame[column].nunique() < 20: + return MetadataType.NOMINAL return MetadataType.CONTINUOUS elif pd.api.types.is_bool_dtype(dtype): return MetadataType.BOOLEAN diff --git a/backend/zeno_backend/processing/chart.py b/backend/zeno_backend/processing/chart.py index 6e22e180..dcd8748e 100644 --- a/backend/zeno_backend/processing/chart.py +++ b/backend/zeno_backend/processing/chart.py @@ -18,7 +18,7 @@ from zeno_backend.classes.slice import Slice from zeno_backend.database.select import metrics, slices from zeno_backend.processing.filtering import table_filter -from zeno_backend.processing.metrics import metric_map +from zeno_backend.processing.metrics.map import metric_map def xyc_data(chart: Chart, project: str) -> str: @@ -34,12 +34,25 @@ def xyc_data(chart: Chart, project: str) -> str: elements: list[dict[str, Any]] = [] if not (isinstance(chart.parameters, XCParameters)): return json.dumps({"table": elements}) + all_metrics = metrics(project) selected_metric = next( (x for x in all_metrics if x.id == chart.parameters.metric), Metric(id=-1, name="count", type="count", columns=[]), ) + selected_slices = slices(project, chart.parameters.slices) + if -1 in chart.parameters.slices: + selected_slices = selected_slices + [ + Slice( + id=-1, + slice_name="All instances", + filter_predicates=FilterPredicateGroup( + predicates=[], join=Join.OMITTED + ), + ) + ] + selected_models = chart.parameters.models for current_slice in selected_slices: for model in selected_models: @@ -57,6 +70,7 @@ def xyc_data(chart: Chart, project: str) -> str: "size": metric.size, } ) + return json.dumps({"table": elements}) @@ -75,9 +89,22 @@ def table_data(chart: Chart, project: str) -> str: if not isinstance(params, TableParameters): return json.dumps({"table": elements}) selected_metrics = list( - filter(lambda metric: metric.id in params.metrics, metrics(project)) + filter( + lambda metric: metric.id in params.metrics, + metrics(project) + [Metric(id=-1, name="count", type="count", columns=[])], + ) ) selected_slices = slices(project, params.slices) + if -1 in params.slices: + selected_slices = selected_slices + [ + Slice( + id=-1, + slice_name="All instances", + filter_predicates=FilterPredicateGroup( + predicates=[], join=Join.OMITTED + ), + ) + ] selected_models = params.models for current_metric in selected_metrics: @@ -119,9 +146,22 @@ def beeswarm_data(chart: Chart, project: str) -> str: if not (isinstance(params, BeeswarmParameters)): return json.dumps({"table": elements}) selected_metrics = list( - filter(lambda metric: metric.id in params.metrics, metrics(project)) + filter( + lambda metric: metric.id in params.metrics, + metrics(project) + [Metric(id=-1, name="count", type="count", columns=[])], + ) ) selected_slices = slices(project, params.slices) + if -1 in params.slices: + selected_slices = selected_slices + [ + Slice( + id=-1, + slice_name="All instances", + filter_predicates=FilterPredicateGroup( + predicates=[], join=Join.OMITTED + ), + ) + ] selected_models = params.models for current_metric in selected_metrics: @@ -162,9 +202,22 @@ def radar_data(chart: Chart, project: str) -> str: if not (isinstance(params, RadarParameters)): return json.dumps({"table": elements}) selected_metrics = list( - filter(lambda metric: metric.id in params.metrics, metrics(project)) + filter( + lambda metric: metric.id in params.metrics, + metrics(project) + [Metric(id=-1, name="count", type="count", columns=[])], + ) ) selected_slices = slices(project, params.slices) + if -1 in params.slices: + selected_slices = selected_slices + [ + Slice( + id=-1, + slice_name="All instances", + filter_predicates=FilterPredicateGroup( + predicates=[], join=Join.OMITTED + ), + ) + ] selected_models = params.models for current_metric in selected_metrics: @@ -211,12 +264,33 @@ def heatmap_data(chart: Chart, project: str) -> str: ) x_slice = params.x_channel == SlicesOrModels.SLICES y_slice = params.y_channel == SlicesOrModels.SLICES - selected_x: list[Slice] | list[str] = ( + selected_x = ( slices(project, params.x_values) if x_slice else params.x_values # type: ignore ) + if x_slice and -1 in params.x_values: + selected_x = selected_x + [ + Slice( + id=-1, + slice_name="All instances", + filter_predicates=FilterPredicateGroup( + predicates=[], join=Join.OMITTED + ), + ) + ] + selected_y = ( slices(project, params.y_values) if y_slice else params.y_values # type: ignore ) + if y_slice and -1 in params.y_values: + selected_y = selected_y + [ + Slice( + id=-1, + slice_name="All instances", + filter_predicates=FilterPredicateGroup( + predicates=[], join=Join.OMITTED + ), + ) + ] for current_x in selected_x: for current_y in selected_y: diff --git a/backend/zeno_backend/processing/filtering.py b/backend/zeno_backend/processing/filtering.py index 105d3297..05c7d177 100644 --- a/backend/zeno_backend/processing/filtering.py +++ b/backend/zeno_backend/processing/filtering.py @@ -3,7 +3,7 @@ from psycopg import sql from zeno_backend.classes.base import MetadataType, ZenoColumn -from zeno_backend.classes.filter import FilterPredicateGroup +from zeno_backend.classes.filter import FilterPredicateGroup, Operation from zeno_backend.classes.metadata import HistogramBucket from zeno_backend.database.select import column_id_from_name_and_model @@ -34,19 +34,18 @@ def filter_to_sql( + sql.SQL(")") ) else: - try: - val = str(float(f.value)) - except ValueError: - if str(f.value).lower() in [ - "true", - "false", - ]: - val = "True" if str(f.value).lower() == "true" else "False" - else: - val = f.value + if str(f.value).lower() in [ + "true", + "false", + ]: + val = "True" if str(f.value).lower() == "true" else "False" + else: + val = f.value + if f.operation == Operation.LIKE or f.operation == Operation.ILIKE: + val = "%" + str(val) + "%" column_id = ( f.column.id - if model is None + if f.column.model is None or model is None else column_id_from_name_and_model(project, f.column.name, model) ) filt = ( diff --git a/backend/zeno_backend/processing/histogram_processing.py b/backend/zeno_backend/processing/histogram_processing.py index f6ea844f..0ee6f6ab 100644 --- a/backend/zeno_backend/processing/histogram_processing.py +++ b/backend/zeno_backend/processing/histogram_processing.py @@ -1,4 +1,6 @@ """Functions for creating the frontend metadata histograms.""" + + import numpy as np import pandas as pd from psycopg import sql @@ -6,11 +8,63 @@ from zeno_backend.classes.base import MetadataType, ZenoColumn from zeno_backend.classes.metadata import ( HistogramBucket, + HistogramColumnRequest, HistogramRequest, ) -from zeno_backend.database.select import column -from zeno_backend.processing.filtering import bucket_filter, filter_to_sql, table_filter -from zeno_backend.processing.metrics import metric_map +from zeno_backend.database.database import Database +from zeno_backend.database.select import ( + column, + column_id_from_name_and_model, + project_from_uuid, +) +from zeno_backend.processing.filtering import table_filter + + +def histogram_bucket(project_uuid: str, col: ZenoColumn, num_bins: int | str): + """Calculate the histogram buckets for a single column. + + Args: + project_uuid (str): the project the user is currently working with. + col (ZenoColumn): the column to compute buckets for. + num_bins (int): the number of bins to use for the histogram. + + + Returns: + List[HistogramBucket]: the buckets for the column histogram. + """ + col_list = column(project_uuid, col) + df_col: pd.Series = pd.DataFrame({"col": col_list})["col"] + if col.data_type == MetadataType.NOMINAL: + ret_hist: list[HistogramBucket] = [] + val_counts: pd.Series = df_col.value_counts() + if len(val_counts) > 30: + return [] + else: + for k in val_counts.keys(): # type: ignore + ret_hist.append(HistogramBucket(bucket=k)) + return ret_hist + elif col.data_type == MetadataType.CONTINUOUS: + ret_hist: list[HistogramBucket] = [] + df_col = pd.to_numeric(df_col).fillna(0) # type: ignore + bins = np.histogram_bin_edges(df_col, bins=num_bins) + for i in range(len(bins) - 1): + ret_hist.append( + HistogramBucket( + bucket=bins[i], + bucket_end=bins[i + 1], + ) + ) + return ret_hist + elif col.data_type == MetadataType.BOOLEAN: + return [ + HistogramBucket(bucket=True), + HistogramBucket(bucket=False), + ] + + elif col.data_type == MetadataType.DATETIME: + return [] + else: + return [] def histogram_buckets( @@ -30,126 +84,257 @@ def histogram_buckets( Returns: List[List[HistogramBucket]]: for each zeno column return a list of buckets """ - res: list[list[HistogramBucket]] = [] + res = [] for col in req: - col_list = column(project, col) - df_col: pd.Series = pd.DataFrame({"col": col_list})["col"] + res.append(histogram_bucket(project, col, num_bins)) + return res + + +def histogram_metric_task( + request: HistogramRequest, + col_request: HistogramColumnRequest, + project_uuid: str, + filter_sql: sql.Composed | None, +) -> list[HistogramBucket]: + """Calculate the metric and count for a column. + + Args: + request (HistogramRequest): the request object. + col_request (HistogramColumnRequest): the column request object. + bucket (HistogramBucket): the bucket to calculate the metric for. + project_uuid (str): the project the user is currently working with. + filter_sql (sql.Composed): the filter to apply to the query. + + + Returns: + HistogramBucket: the bucket with the metric added. + """ + col = col_request.column + if request.metric is None or request.model is None: + return [] + + col_id = column_id_from_name_and_model(project_uuid, col.name, request.model) + metric_col_id = column_id_from_name_and_model( + project_uuid, request.metric.columns[0], request.model + ) + + with Database() as db: if col.data_type == MetadataType.NOMINAL: - ret_hist: list[HistogramBucket] = [] - val_counts: pd.Series = df_col.value_counts() - for k in val_counts.keys(): # type: ignore - ret_hist.append(HistogramBucket(bucket=k)) - res.append(ret_hist) - elif col.data_type == MetadataType.CONTINUOUS: - ret_hist: list[HistogramBucket] = [] - df_col = pd.to_numeric(df_col).fillna(0) # type: ignore - bins = np.histogram_bin_edges(df_col, bins=num_bins) - for i in range(len(bins) - 1): - ret_hist.append( + unique = db.execute_return( + sql.SQL("SELECT COUNT(DISTINCT {}) FROM {}").format( + sql.Identifier(col_id), + sql.Identifier(project_uuid), + ) + ) + if len(unique) > 0 and unique[0][0] > 30: + return [] + else: + statement = sql.SQL("SELECT {}, AVG({}), COUNT(*) FROM {}").format( + sql.Identifier(col_id), + sql.Identifier(metric_col_id), + sql.Identifier(project_uuid), + ) + if filter_sql: + statement = sql.SQL("{} WHERE {} GROUP BY {}").format( + statement, filter_sql, sql.Identifier(col_id) + ) + else: + statement = sql.SQL("{} GROUP BY {}").format( + statement, sql.Identifier(col_id) + ) + db_res = db.execute_return(statement) + results_map = {r[0]: (r[1], r[2]) for r in db_res} + + return [ HistogramBucket( - bucket=bins[i], - bucket_end=bins[i + 1], + bucket=b.bucket, + metric=results_map[b.bucket][0] + if b.bucket in results_map + else 0, + size=results_map[b.bucket][1] if b.bucket in results_map else 0, ) + for b in col_request.buckets + ] + + elif col.data_type == MetadataType.CONTINUOUS: + case_statement = sql.SQL("CASE ") + for i, b in enumerate(col_request.buckets): + case_statement += sql.SQL("WHEN {} >= {} AND {} < {} THEN {} ").format( + sql.Identifier(col_id), + sql.Literal(b.bucket), + sql.Identifier(col_id), + sql.Literal(b.bucket_end), + sql.Literal(i), + ) + case_statement += sql.SQL("END AS bucket") + statement = sql.SQL("SELECT {}, AVG({}), COUNT(*) FROM {}").format( + case_statement, + sql.Identifier(metric_col_id), + sql.Identifier(project_uuid), + ) + + if filter_sql: + statement = sql.SQL("{} WHERE {} GROUP BY bucket").format( + statement, filter_sql ) - res.append(ret_hist) + else: + statement = sql.SQL("{} GROUP BY bucket").format(statement) + db_res = db.execute_return(statement) + results_map = {int(r[0]): (r[1], r[2]) for r in db_res if r[0] is not None} + + return [ + HistogramBucket( + bucket=b.bucket, + bucket_end=b.bucket_end, + metric=results_map[i][0] if i in results_map else 0, + size=results_map[i][1] if i in results_map else 0, + ) + for i, b in enumerate(col_request.buckets) + ] + elif col.data_type == MetadataType.BOOLEAN: - res.append( - [ - HistogramBucket(bucket=True), - HistogramBucket(bucket=False), - ] + statement = sql.SQL( + "SELECT CASE WHEN {} = TRUE THEN 0 WHEN {} = FALSE" + " THEN 1 END AS bucket, AVG({}), COUNT(*) FROM {}" + ).format( + sql.Identifier(col_id), + sql.Identifier(col_id), + sql.Identifier(metric_col_id), + sql.Identifier(project_uuid), ) - elif col.data_type == MetadataType.DATETIME: - res.append([]) + if filter_sql: + statement = sql.SQL("{} WHERE {} GROUP BY bucket").format( + statement, filter_sql + ) + else: + statement = sql.SQL("{} GROUP BY bucket").format(statement) + res = db.execute_return(statement) + + true_res = [r for r in res if r[0] == 0] + false_res = [r for r in res if r[0] == 1] + return [ + HistogramBucket( + bucket=True, + size=true_res[0][2] if len(true_res) > 0 else 0, + metric=true_res[0][1] if len(true_res) > 0 else 0, + ), + HistogramBucket( + bucket=False, + size=false_res[0][2] if len(false_res) > 0 else 0, + metric=false_res[0][1] if len(false_res) > 0 else 0, + ), + ] else: - res.append([]) - return res + return [] -def histogram_counts(project: str, req: HistogramRequest) -> list[list[int]]: - """Calculate count for each bucket in each column histogram. +def histogram_count( + request: HistogramRequest, + col_request: HistogramColumnRequest, + project_uuid: str, + filter_sql: sql.Composed | None, + calculate_histograms: bool, +) -> list[HistogramBucket]: + """Calculate the counts and metrics for a column. Args: - project (str): the project the user is currently working with. - req (HistogramRequest): specifying which histograms to calculate counts for. + request (HistogramRequest): the request object. + col_request (HistogramColumnRequest): the column request object. + project_uuid (str): the project the user is currently working with. + filter_sql (sql.Composed): the filter to apply to the query. + calculate_histograms (bool): whether to calculate the histograms or not. + Returns: - List[List[int]]: counts for the individual buckets of specified histograms. + List[HistogramBucket]: the buckets with the counts and metrics added. """ - ret: list[list[int]] = [] - for r in req.column_requests: - col = r.column - data_frame = pd.DataFrame( - { - "col": column( - project, - col, - None - if req.filter_predicates is None - else filter_to_sql(req.filter_predicates, project), - ) - } - ) + col = col_request.column + + if request.model is None or request.metric is None or not calculate_histograms: + col = col_request.column + data_frame = pd.DataFrame({"col": column(project_uuid, col, filter_sql)}) if col.data_type == MetadataType.NOMINAL: - counts: pd.Series[int] = data_frame.groupby("col").size() # type: ignore - ret.append( - [ - counts[b.bucket] if b.bucket in counts else 0 # type: ignore - for b in r.buckets + if data_frame["col"].nunique() > 30: + return [] + else: + counts: pd.Series[int] = data_frame.groupby("col").size() + return [ + HistogramBucket( + bucket=b.bucket, + size=counts[b.bucket] # type: ignore + if b.bucket in counts + else 0, + ) + for b in col_request.buckets ] - ) - elif col.data_type == MetadataType.BOOLEAN: - ret.append( - [data_frame["col"].sum(), len(data_frame) - data_frame["col"].sum()] - ) + elif col.data_type == MetadataType.CONTINUOUS: - bucs = [b.bucket for b in r.buckets] - ret.append( - data_frame.groupby([pd.cut(data_frame["col"], bucs)]) # type: ignore + bucs = [b.bucket for b in col_request.buckets] + intervals = pd.IntervalIndex.from_arrays( + [float(b.bucket) for b in col_request.buckets], + [float(b.bucket_end) for b in col_request.buckets], # type: ignore + ) + counts = ( + data_frame.groupby( + [pd.cut(data_frame["col"], intervals)], + observed=False, # type: ignore + ) .size() .astype(int) .tolist() ) + return [ + HistogramBucket( + bucket=b, + size=counts[i], + ) + for i, b in enumerate(bucs) + ] + elif col.data_type == MetadataType.BOOLEAN: + return [ + HistogramBucket( + bucket=True, + size=data_frame["col"].sum(), + ), + HistogramBucket( + bucket=False, + size=len(data_frame) - data_frame["col"].sum(), + ), + ] else: - ret.append([]) - return ret + return [] + else: + return histogram_metric_task(request, col_request, project_uuid, filter_sql) -def histogram_metrics(project: str, req: HistogramRequest) -> list[list[float | None]]: - """Calculate metric for each bucket in each column histogram. +def histogram_counts( + project_uuid: str, req: HistogramRequest +) -> list[list[HistogramBucket]]: + """Calculate count and optionally metric for each bucket in each column histogram. Args: - project (str): the project the user is currently working with. - req (HistogramRequest): the histograms for which to calculate metrics. + project_uuid (str): the project the user is currently working with. + req (HistogramRequest): specifying which histograms to calculate counts for. Returns: - List[List[Union[float, None]]]: metrics for the requested histogram buckets. + List[List[int]]: counts for the individual buckets of specified histograms. """ - if req.metric is None: + project_obj = project_from_uuid(project_uuid) + if project_obj is None: return [] - - filter_sql = table_filter(project, req.model, req.filter_predicates, req.data_ids) - ret: list[list[float | None]] = [] + filter_sql = table_filter( + project_uuid, req.model, req.filter_predicates, req.data_ids + ) + res = [] for r in req.column_requests: - loc_ret: list[float | None] = [] - index = 0 - for bucket in r.buckets: - if req.model: - filter_bucket = bucket_filter(r.column, bucket) - if index == 0: - index = 1 - final_filter = filter_sql - if filter_bucket is not None: - if final_filter is None: - final_filter = filter_bucket - else: - final_filter = final_filter + sql.SQL(" AND ") + filter_bucket - metric = metric_map(req.metric, project, req.model, final_filter) - if metric.metric is None: - loc_ret.append(None) - else: - loc_ret.append(metric.metric) - else: - loc_ret.append(None) - ret.append(loc_ret) - return ret + res.append( + histogram_count( + req, + r, + project_uuid, + filter_sql, + project_obj.calculate_histogram_metrics, + ) + ) + + return res diff --git a/backend/zeno_backend/processing/metrics/__init__.py b/backend/zeno_backend/processing/metrics/__init__.py new file mode 100644 index 00000000..1b8755ba --- /dev/null +++ b/backend/zeno_backend/processing/metrics/__init__.py @@ -0,0 +1 @@ +"""Logic for calculating specific metrics on the data.""" diff --git a/backend/zeno_backend/processing/metrics/bleu.py b/backend/zeno_backend/processing/metrics/bleu.py new file mode 100644 index 00000000..698fd1a6 --- /dev/null +++ b/backend/zeno_backend/processing/metrics/bleu.py @@ -0,0 +1,196 @@ +"""Calculate BLEU score.""" +import math +from collections import Counter + +from psycopg import sql + +from zeno_backend.classes.base import GroupMetric +from zeno_backend.classes.metric import Metric +from zeno_backend.database.database import Database + + +def bleu( + project_uuid: str, metric: Metric, model: str, filter: sql.Composed | None +) -> GroupMetric: + """Calculate BLEU score. + + Args: + project_uuid (str): project identifier + metric (Metric): metric object + model (str): model identifier + filter (sql.Composed | None): filter to apply to the data + + Returns: + GroupMetric: final metric and slice size + """ + with Database() as db: + output_col = db.execute_return( + sql.SQL( + "SELECT column_id FROM {} WHERE name = 'output' AND" + " (model IS NULL OR model = {})" + ).format( + sql.Identifier(f"{project_uuid}_column_map"), + sql.Literal(model), + ) + ) + + if len(output_col) == 0: + return GroupMetric(metric=None, size=0) + output_col = output_col[0][0] + + data = db.execute_return( + sql.SQL("SELECT {}, label FROM {}").format( + sql.Identifier(output_col), sql.Identifier(project_uuid) + ) + if filter is None + else sql.SQL("SELECT {}, label FROM {} WHERE ").format( + sql.Identifier(output_col), sql.Identifier(project_uuid) + ) + + filter + ) + + if data is None or len(data) == 0: + return GroupMetric(metric=None, size=0) + + scorer = BleuScorer() + score = scorer.score_corpus([d[0] for d in data], [d[1] for d in data]) + return GroupMetric(metric=float(score), size=len(data)) + + +class BleuScorer: + """A scorer that calculates BLEU score.""" + + def __init__(self, weights=(0.25, 0.25, 0.25, 0.25), case_insensitive=False): + """Initialize the scorer. + + Args: + weights (tuple, optional): Statistic weights. + Defaults to (0.25, 0.25, 0.25, 0.25). + case_insensitive (bool, optional): Case sensitivity. + Defaults to False. + """ + self.weights = weights + self.case_insensitive = case_insensitive + + def score_corpus(self, ref, out): + """Score a corpus using BLEU score. + + Args: + ref: A reference corpus + out: An output corpus + + Returns: + A tuple containing a single value for the BLEU score and a + string summarizing auxiliary information + """ + cached_stats = self.cache_stats(ref, out) + return self.score_cached_corpus(range(len(ref)), cached_stats) + + def _precision(self, ref, out, n): + """Caculate n-gram precision. + + Args: + ref: A reference sentence + out: An output sentence + n: The ngram length to consider + + Returns: + Numerator and denominator of the precision + """ + out_ngram = sent_ngrams_list(out, n) + ref_ngram = sent_ngrams_list(ref, n) + out_cnt = Counter(out_ngram) + ref_cnt = Counter(ref_ngram) + + num = 0 + denom = 0 + for ngram, o_cnt in out_cnt.items(): + num += min(o_cnt, ref_cnt[ngram]) + denom += o_cnt + denom = max(1, denom) + + return num, denom + + def cache_stats(self, ref, out, src=None): + """Cache sufficient statistics for caculating BLEU score. + + Args: + ref: A reference corpus + out: An output corpus + src: A source courpus. Ignored if passed + + Returns: + A list of cached statistics + """ + if self.case_insensitive: + ref = ref.lower() + out = out.lower() + + cached_stats = [] + + for r, o in zip(ref, out): + prec = [] + for n in range(1, len(self.weights) + 1): + prec.append(self._precision(r, o, n)) + cached_stats.append((len(r), len(o), prec)) + + return cached_stats + + def score_cached_corpus(self, sent_ids, cached_stats): + """Score a corpus using BLEU score with cache. + + Args: + sent_ids: The sentence ids for reference and output corpora + cached_stats: A list of cached statistics + + Returns: + A tuple containing a single value for the BLEU score and a + string summarizing auxiliary information + """ + if len(cached_stats) == 0: + return 0.0 + + cached_ref_len, cached_out_len, cached_prec = zip(*cached_stats) + + num_prec = Counter() + denom_prec = Counter() + + ref_len = 0 + out_len = 0 + for sent_id in sent_ids: + ref_len += cached_ref_len[sent_id] + out_len += cached_out_len[sent_id] + for n in range(1, len(self.weights) + 1): + num, denom = cached_prec[sent_id][n - 1] + num_prec[n] += num + denom_prec[n] += denom + + if num_prec[1] == 0: + return 0 + + prec = 0 + for i, w in enumerate(self.weights, start=1): + p = num_prec[i] / denom_prec[i] if denom_prec[i] != 0 else 0 + p = math.log(p) if p > 0 else 0 + prec += p * w + + bp = min(1, math.exp(1 - ref_len / out_len)) if out_len != 0 else 0 + + return bp * math.exp(prec) + + +def sent_ngrams_list(words, n): + """Create a list with all the n-grams in a sentence. + + Arguments: + words: A list of strings representing a sentence + n: The ngram length to consider + + Returns: + A list of n-grams in the sentence + """ + word_ngram = [] + for i in range(len(words) - n + 1): + ngram = tuple(words[i : i + n]) + word_ngram.append(ngram) + return word_ngram diff --git a/backend/zeno_backend/processing/metrics.py b/backend/zeno_backend/processing/metrics/f1.py similarity index 54% rename from backend/zeno_backend/processing/metrics.py rename to backend/zeno_backend/processing/metrics/f1.py index 2ec2133f..e24d6786 100644 --- a/backend/zeno_backend/processing/metrics.py +++ b/backend/zeno_backend/processing/metrics/f1.py @@ -1,58 +1,11 @@ -"""Methods to calculate metrics for model outputs.""" +"""Compute the F1 score.""" from psycopg import sql from zeno_backend.classes.base import GroupMetric -from zeno_backend.classes.metric import Metric from zeno_backend.database.database import Database from zeno_backend.database.select import column_id_from_name_and_model -def accuracy(project: str, model: str, filter: sql.Composed | None) -> GroupMetric: - """Accuracy of the selected model on the selected data. - - Args: - project (str): the project the user is currently working with. - model (str): the model for which to calulate the accuracy. - filter (sql.Composed | None): how to filter the data before calculation. - - Raises: - Exception: something in the database processing failed. - - Returns: - GroupMetric: accuracy metric calculated as specified. - """ - output_column_id = column_id_from_name_and_model(project, "output", model) - with Database() as db: - correct_query = sql.SQL("SELECT COUNT(*) FROM {} WHERE {} = label").format( - sql.Identifier(project), sql.Identifier(output_column_id) - ) - num_correct = db.execute_return( - correct_query - if filter is None - else correct_query + sql.SQL(" AND ") + filter, - ) - total_query = sql.SQL("SELECT COUNT(*) FROM {}").format(sql.Identifier(project)) - num_total = db.execute_return( - total_query - if filter is None - else total_query + sql.SQL(" WHERE ") + filter, - ) - return ( - ( - GroupMetric( - metric=100 * num_correct[0] / num_total[0], size=num_total[0] - ) - if num_total[0] != 0 - else GroupMetric(metric=0, size=0) - ) - if num_correct is not None - and isinstance(num_correct[0], int) - and num_total is not None - and isinstance(num_total[0], int) - else GroupMetric(metric=None, size=0) - ) - - def recall(project: str, model: str, filter: sql.Composed | None) -> GroupMetric: """Ratio of tp/(tp+fn), intuitively the ability to find all the positive samples. @@ -76,8 +29,7 @@ def recall(project: str, model: str, filter: sql.Composed | None) -> GroupMetric num_tp = db.execute_return( tp_query + group_query if filter is None - else tp_query + sql.SQL(" AND ") + filter + group_query, - return_all=True, + else tp_query + sql.SQL(" AND ") + filter + group_query ) fn_query = sql.SQL("SELECT COUNT(*), label FROM {} WHERE {} != label").format( sql.Identifier(project), sql.Identifier(output_column_id) @@ -85,8 +37,7 @@ def recall(project: str, model: str, filter: sql.Composed | None) -> GroupMetric num_fn = db.execute_return( fn_query + group_query if filter is None - else fn_query + sql.SQL(" AND ") + filter + group_query, - return_all=True, + else fn_query + sql.SQL(" AND ") + filter + group_query ) total_query = sql.SQL("SELECT COUNT(*) FROM {}").format(sql.Identifier(project)) num_total = db.execute_return( @@ -100,8 +51,7 @@ def recall(project: str, model: str, filter: sql.Composed | None) -> GroupMetric label_result = db.execute_return( labels_query if filter is None - else labels_query + sql.SQL(" WHERE ") + filter, - return_all=True, + else labels_query + sql.SQL(" WHERE ") + filter ) if ( num_tp is None @@ -161,8 +111,7 @@ def precision(project: str, model: str, filter: sql.Composed | None) -> GroupMet num_tp = db.execute_return( tp_query + group_query if filter is None - else tp_query + sql.SQL(" AND ") + filter + group_query, - return_all=True, + else tp_query + sql.SQL(" AND ") + filter + group_query ) fp_query = sql.SQL("SELECT COUNT(*), {} FROM {} WHERE {} != label").format( sql.Identifier(output_column_id), @@ -173,8 +122,7 @@ def precision(project: str, model: str, filter: sql.Composed | None) -> GroupMet num_fp = db.execute_return( fp_query + group_query if filter is None - else fp_query + sql.SQL(" AND ") + filter + group_query, - return_all=True, + else fp_query + sql.SQL(" AND ") + filter + group_query ) total_query = sql.SQL("SELECT COUNT(*) FROM {}").format(sql.Identifier(project)) num_total = db.execute_return( @@ -188,8 +136,7 @@ def precision(project: str, model: str, filter: sql.Composed | None) -> GroupMet label_result = db.execute_return( labels_query if filter is None - else labels_query + sql.SQL(" WHERE ") + filter, - return_all=True, + else labels_query + sql.SQL(" WHERE ") + filter ) if ( num_tp is None @@ -254,128 +201,3 @@ def f1(project: str, model: str, filter: sql.Composed | None) -> GroupMetric: metric=2 * (prec.metric * rec.metric) / (prec.metric + rec.metric), size=prec.size, ) - - -def count(project: str, filter: sql.Composed | None) -> GroupMetric: - """Count the number of datapoints matching a specified filter. - - Args: - project (str): the project the user is currently working with. - filter (sql.Composed | None): the filter to be applied before counting. - - Raises: - Exception: something in the database processing failed. - - Returns: - GroupMetric: count of datapoints matching the specified filter. - """ - with Database() as db: - num_total = db.execute_return( - sql.SQL("SELECT COUNT(*) FROM {}").format(sql.Identifier(project)) - if filter is None - else sql.SQL("SELECT COUNT(*) FROM {} WHERE ").format( - sql.Identifier(project) - ) - + filter, - ) - return ( - GroupMetric( - metric=None, size=num_total[0] if isinstance(num_total[0], int) else 0 - ) - if num_total is not None - else GroupMetric(metric=None, size=0) - ) - - -def mean( - project_uuid: str, metric: Metric, model: str, filter: sql.Composed | None -) -> GroupMetric: - """Calculate the mean of a metric for a given project. - - Args: - project_uuid (str): the project the user is currently working with. - metric (Metric): the metric for which to calculate the mean. - model (str): the model for which to calculate the metric. - filter (sql.Composed | None): the filter to apply to the data before metric. - - Raises: - Exception: something in the database processing failed. - - Returns: - GroupMetric: mean of the metric for the given project. - """ - with Database() as db: - # Get column name from project column map - column_id = db.execute_return( - sql.SQL( - "SELECT column_id FROM {} WHERE name = {} AND" - " (model IS NULL OR model = {})" - ).format( - sql.Identifier(f"{project_uuid}_column_map"), - sql.Literal(metric.columns[0]), - sql.Literal(model), - ) - ) - - if len(column_id) == 0: - return GroupMetric(metric=None, size=0) - column_id = column_id[0][0] - - if filter is None: - db.execute( - sql.SQL("SELECT COUNT(*) AS n, AVG({}::float) FROM {}").format( - sql.Identifier(column_id), sql.Identifier(project_uuid) - ) - ) - else: - db.execute( - sql.SQL("SELECT COUNT(*) AS n, AVG({}::float) FROM {} WHERE ").format( - sql.Identifier(column_id), sql.Identifier(project_uuid) - ) - + filter - ) - - if not db.cur or db.cur.rowcount == 0: - return GroupMetric(metric=None, size=0) - else: - res = db.cur.fetchone() - - if res: - return GroupMetric(metric=float(res[1]) if res[1] else None, size=res[0]) - else: - return GroupMetric(metric=None, size=0) - - -def metric_map( - metric: Metric | None, - project: str, - model: str | None, - sql_filter: sql.Composed | None, -) -> GroupMetric: - """Call a metric function based on the selected metric. - - Args: - metric (Metric): the metric for which to call a metric function. - project (str): the project the user is currently working with. - model (str): the model for which to calculate the metric. - sql_filter (str | None): the filter to apply to the data before metric - calculation. - - Returns: - GroupMetric: the metric result calculated on the data as specified. - """ - if metric is None or model is None: - return count(project, sql_filter) - - if metric.type == "mean": - return mean(project, metric, model, sql_filter) - if metric.type == "accuracy": - return accuracy(project, model, sql_filter) - elif metric.type == "recall": - return recall(project, model, sql_filter) - elif metric.type == "f1": - return f1(project, model, sql_filter) - elif metric.type == "precision": - return precision(project, model, sql_filter) - else: - return count(project, sql_filter) diff --git a/backend/zeno_backend/processing/metrics/map.py b/backend/zeno_backend/processing/metrics/map.py new file mode 100644 index 00000000..dae43e58 --- /dev/null +++ b/backend/zeno_backend/processing/metrics/map.py @@ -0,0 +1,88 @@ +"""Map metric names to metric functions.""" + +from psycopg import sql + +from zeno_backend.classes.base import GroupMetric +from zeno_backend.classes.metric import Metric +from zeno_backend.database.database import Database +from zeno_backend.processing.metrics.bleu import bleu +from zeno_backend.processing.metrics.f1 import f1, precision, recall +from zeno_backend.processing.metrics.mean import mean + + +def count( + project: str, filter: sql.Composed | None, size_as_metric: bool = False +) -> GroupMetric: + """Count the number of datapoints matching a specified filter. + + Args: + project (str): the project the user is currently working with. + filter (sql.Composed | None): the filter to be applied before counting. + size_as_metric (bool): whether to return the count as the metric. + + Raises: + Exception: something in the database processing failed. + + Returns: + GroupMetric: count of datapoints matching the specified filter. + """ + with Database() as db: + num_total = db.execute_return( + sql.SQL("SELECT COUNT(*) FROM {}").format(sql.Identifier(project)) + if filter is None + else sql.SQL("SELECT COUNT(*) FROM {} WHERE ").format( + sql.Identifier(project) + ) + + filter, + ) + + if size_as_metric: + met = num_total[0][0] if isinstance(num_total[0][0], int) else 0 + else: + met = None + + return ( + GroupMetric( + metric=met, + size=num_total[0][0] if isinstance(num_total[0][0], int) else 0, + ) + if num_total is not None + else GroupMetric(metric=None, size=0) + ) + + +def metric_map( + metric: Metric | None, + project: str, + model: str | None, + sql_filter: sql.Composed | None, +) -> GroupMetric: + """Call a metric function based on the selected metric. + + Args: + metric (Metric): the metric for which to call a metric function. + project (str): the project the user is currently working with. + model (str): the model for which to calculate the metric. + sql_filter (str | None): the filter to apply to the data before metric + calculation. + + Returns: + GroupMetric: the metric result calculated on the data as specified. + """ + if metric is None or model is None: + return count(project, sql_filter) + + if metric.type == "mean": + return mean(project, metric, model, sql_filter) + if metric.type == "bleu": + return bleu(project, metric, model, sql_filter) + if metric.type == "recall": + return recall(project, model, sql_filter) + if metric.type == "f1": + return f1(project, model, sql_filter) + if metric.type == "precision": + return precision(project, model, sql_filter) + if metric.type == "count": + return count(project, sql_filter, True) + + return count(project, sql_filter) diff --git a/backend/zeno_backend/processing/metrics/mean.py b/backend/zeno_backend/processing/metrics/mean.py new file mode 100644 index 00000000..4ff8c694 --- /dev/null +++ b/backend/zeno_backend/processing/metrics/mean.py @@ -0,0 +1,69 @@ +"""Mean metric calculation.""" +from psycopg import sql + +from zeno_backend.classes.base import GroupMetric, MetadataType +from zeno_backend.classes.metric import Metric +from zeno_backend.database.database import Database + + +def mean( + project_uuid: str, metric: Metric, model: str, filter: sql.Composed | None +) -> GroupMetric: + """Calculate the mean of a metric for a given project. + + Args: + project_uuid (str): the project the user is currently working with. + metric (Metric): the metric for which to calculate the mean. + model (str): the model for which to calculate the metric. + filter (sql.Composed | None): the filter to apply to the data before metric. + + Raises: + Exception: something in the database processing failed. + + Returns: + GroupMetric: mean of the metric for the given project. + """ + with Database() as db: + # Get column name from project column map + column_output = db.execute_return( + sql.SQL( + "SELECT column_id, data_type FROM {} WHERE name = {} AND" + " (model IS NULL OR model = {})" + ).format( + sql.Identifier(f"{project_uuid}_column_map"), + sql.Literal(metric.columns[0]), + sql.Literal(model), + ) + ) + + if len(column_output) == 0: + return GroupMetric(metric=None, size=0) + column_id = column_output[0][0] + if column_output[0][1] == MetadataType.BOOLEAN: + column_id = sql.Identifier(column_id) + sql.SQL("::int") + else: + column_id = sql.Identifier(column_id) + + if filter is None: + db.execute( + sql.SQL("SELECT COUNT(*) AS n, AVG({}) FROM {}").format( + column_id, sql.Identifier(project_uuid) + ) + ) + else: + db.execute( + sql.SQL("SELECT COUNT(*) AS n, AVG({}) FROM {} WHERE ").format( + column_id, sql.Identifier(project_uuid) + ) + + filter + ) + + if not db.cur or db.cur.rowcount == 0: + return GroupMetric(metric=None, size=0) + else: + res = db.cur.fetchone() + + if res: + return GroupMetric(metric=float(res[1]) if res[1] else None, size=res[0]) + else: + return GroupMetric(metric=None, size=0) diff --git a/backend/zeno_backend/routers/sdk.py b/backend/zeno_backend/routers/sdk.py index 540a0a0f..ee63ee50 100644 --- a/backend/zeno_backend/routers/sdk.py +++ b/backend/zeno_backend/routers/sdk.py @@ -1,8 +1,10 @@ """FastAPI server endpoints for the Zeno SDK.""" import io +import os import uuid import pandas as pd +from amplitude import Amplitude, BaseEvent from fastapi import ( APIRouter, Depends, @@ -18,6 +20,8 @@ from zeno_backend.classes.project import Project from zeno_backend.database import insert, select +amplitude_client = Amplitude(os.environ["AMPLITUDE_API_KEY"]) + class APIKeyBearer(HTTPBearer): """API key bearer authentication scheme.""" @@ -93,6 +97,13 @@ def create_project(project: Project, api_key=Depends(APIKeyBearer())): ) project.uuid = str(uuid.uuid4()) + amplitude_client.track( + BaseEvent( + event_type="Project Created", + user_id="00000" + str(user_id), + event_properties={"project_uuid": project.uuid}, + ) + ) insert.project(project, user_id) return project.uuid @@ -104,6 +115,7 @@ def upload_dataset( label_column: str = Form(None), data_column: str = Form(None), file: UploadFile = File(...), + api_key=Depends(APIKeyBearer()), ): """Upload a dataset to a Zeno project. @@ -115,7 +127,15 @@ def upload_dataset( data_column (str | None, optional): the name of the column containing the raw data. Only works for small text data. Defaults to None. file (UploadFile): the dataset to upload. + api_key (str, optional): API key. """ + user_id = select.user_id_by_api_key(api_key) + if user_id is None: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=("ERROR: Invalid API key."), + ) + try: bytes = file.file.read() dataset_df = pd.read_feather(io.BytesIO(bytes)) @@ -141,6 +161,7 @@ def upload_system( output_column: str = Form(...), id_column: str = Form(...), file: UploadFile = File(...), + api_key=Depends(APIKeyBearer()), ): """Upload a system to a Zeno project. @@ -150,7 +171,15 @@ def upload_system( output_column (str): the name of the column containing the system output. id_column (str): the name of the column containing the instance IDs. file (UploadFile): the dataset to upload. + api_key (str, optional): API key. """ + user_id = select.user_id_by_api_key(api_key) + if user_id is None: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=("ERROR: Invalid API key."), + ) + try: bytes = file.file.read() system_df = pd.read_feather(io.BytesIO(bytes)) @@ -167,3 +196,11 @@ def upload_system( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=("ERROR: Unable to create system table: " + str(e)), ) from e + + amplitude_client.track( + BaseEvent( + event_type="System Uploaded", + user_id="00000" + str(user_id), + event_properties={"project_uuid": project}, + ) + ) diff --git a/backend/zeno_backend/server.py b/backend/zeno_backend/server.py index 65bde486..e5858e54 100644 --- a/backend/zeno_backend/server.py +++ b/backend/zeno_backend/server.py @@ -5,6 +5,7 @@ import pandas as pd import uvicorn +from amplitude import Amplitude, BaseEvent from dotenv import load_dotenv from fastapi import Depends, FastAPI, HTTPException, Request, Response, status from fastapi.middleware.cors import CORSMiddleware @@ -19,11 +20,11 @@ GroupMetric, ZenoColumn, ) -from zeno_backend.classes.chart import Chart +from zeno_backend.classes.chart import Chart, ChartResponse from zeno_backend.classes.folder import Folder from zeno_backend.classes.metadata import HistogramBucket, StringFilterRequest from zeno_backend.classes.metric import Metric, MetricRequest -from zeno_backend.classes.project import Project, ProjectStats +from zeno_backend.classes.project import Project, ProjectState, ProjectStats from zeno_backend.classes.report import Report, ReportElement from zeno_backend.classes.slice import Slice from zeno_backend.classes.slice_finder import SliceFinderRequest, SliceFinderReturn @@ -36,14 +37,15 @@ HistogramRequest, histogram_buckets, histogram_counts, - histogram_metrics, ) -from zeno_backend.processing.metrics import metric_map +from zeno_backend.processing.metrics.map import metric_map from zeno_backend.processing.slice_finder import slice_finder from zeno_backend.processing.util import generate_diff_cols from .routers import sdk +amplitude_client = Amplitude(os.environ["AMPLITUDE_API_KEY"]) + def get_server() -> FastAPI: """Provide the FastAPI server and specifies its inputs. @@ -56,7 +58,7 @@ def get_server() -> FastAPI: ------- FastAPI: FastAPI endpoint """ - app = FastAPI(title="Frontend API") + app = FastAPI(title="Frontend API", separate_input_output_schemas=False) # load env vars for cognito if available env_path = Path("../frontend/.env") @@ -81,7 +83,9 @@ def get_server() -> FastAPI: ) api_app = FastAPI( - title="Backend API", generate_unique_id_function=lambda route: route.name + title="Backend API", + generate_unique_id_function=lambda route: route.name, + separate_input_output_schemas=False, ) api_app.include_router(sdk.router) app.mount("/api", api_app) @@ -172,14 +176,19 @@ def get_slices(project: str, request: Request): return select.slices(project) @api_app.get( - "/charts/{project}", + "/charts/{owner}/{project}", response_model=list[Chart], tags=["zeno"], ) - def get_charts(project: str, request: Request): - if not util.access_valid(project, request): + def get_charts(owner_name: str, project_name: str, request: Request): + project_uuid = select.project_uuid(owner_name, project_name) + if project_uuid is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Project not found" + ) + if not util.access_valid(project_uuid, request): return Response(status_code=401) - return select.charts(project) + return select.charts(project_uuid) @api_app.get( "/columns/{project}", @@ -239,7 +248,7 @@ def get_filtered_table(req: TableRequest, project_uuid: str, request: Request): if not util.access_valid(project_uuid, request): return Response(status_code=401) filter_sql = table_filter( - project_uuid, None, req.filter_predicates, req.data_ids + project_uuid, req.model, req.filter_predicates, req.data_ids ) sql_table = select.table_data_paginated( project_uuid, filter_sql, req.offset, req.limit, req.sort @@ -249,7 +258,7 @@ def get_filtered_table(req: TableRequest, project_uuid: str, request: Request): # Prepend the DATA_URL to the data column if it exists project = select.project_from_uuid(project_uuid) if project and project.data_url: - filt_df["data_id"] = project.data_url + filt_df["data_id"] + filt_df["data"] = project.data_url + filt_df["data"] if req.diff_column_1 and req.diff_column_2: filt_df = generate_diff_cols( @@ -257,15 +266,30 @@ def get_filtered_table(req: TableRequest, project_uuid: str, request: Request): ) return filt_df.to_json(orient="records") - @api_app.post( - "/chart-data/{project}", + @api_app.get( + "/chart/{owner}/{project}/{chart_id}", + response_model=ChartResponse, tags=["zeno"], - response_model=str, ) - def get_chart_data(chart: Chart, project: str, request: Request): - if not util.access_valid(project, request): + def get_chart(owner_name: str, project_name: str, chart_id: int, request: Request): + project_uuid = select.project_uuid(owner_name, project_name) + if project_uuid is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Project not found" + ) + project = select.project_from_uuid(project_uuid) + if project is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Project not found" + ) + if not util.access_valid(project_uuid, request): return Response(status_code=401) - return chart_data(chart, project) + chart = select.chart(project_uuid, chart_id) + if chart is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Chart not found" + ) + return ChartResponse(chart=chart, chart_data=chart_data(chart, project_uuid)) @api_app.post("/organizations", tags=["zeno"], response_model=list[Organization]) def get_organizations(current_user=Depends(auth.claim())): @@ -278,6 +302,36 @@ def get_organizations(current_user=Depends(auth.claim())): def is_project_public(project_uuid: str): return select.project_public(project_uuid) + @api_app.get( + "/project-state/{owner}/{project}", response_model=ProjectState, tags=["zeno"] + ) + def get_project_state( + owner_name: str, + project_name: str, + request: Request, + current_user=Depends(auth.claim()), + ): + project_uuid = select.project_uuid(owner_name, project_name) + if project_uuid is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Project not found" + ) + project = select.project_from_uuid(project_uuid) + if project is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Project not found" + ) + if not util.access_valid(project_uuid, request): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Project is private", + ) + user = select.user(current_user["username"]) + if user is not None: + if user.name == project.owner_name: + project.editor = True + return select.project_state(project_uuid, project) + @api_app.post("/project/{owner}/{project}", response_model=Project, tags=["zeno"]) def get_project(owner_name: str, project_name: str, request: Request): uuid = select.project_uuid(owner_name, project_name) @@ -285,6 +339,13 @@ def get_project(owner_name: str, project_name: str, request: Request): return Response(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR) if not util.access_valid(uuid, request): return Response(status_code=401) + amplitude_client.track( + BaseEvent( + event_type="Project Viewed", + user_id="ProjectViewedUser", + event_properties={"project_uuid": uuid}, + ) + ) return select.project( owner_name, project_name, util.get_user_from_token(request) ) @@ -340,6 +401,12 @@ def get_projects(current_user=Depends(auth.claim())): tags=["zeno"], ) def get_public_projects(): + amplitude_client.track( + BaseEvent( + event_type="Home Viewed", + user_id="HomeViewedUser", + ) + ) return select.public_projects() @api_app.get( @@ -400,28 +467,14 @@ def get_histogram_buckets(req: list[ZenoColumn], project: str, request: Request) @api_app.post( "/histogram-counts/{project}", - response_model=list[list[int]], + response_model=list[list[HistogramBucket]], tags=["zeno"], ) - def calculate_histogram_counts( - req: HistogramRequest, project: str, request: Request - ): + def calculate_histograms(req: HistogramRequest, project: str, request: Request): if not util.access_valid(project, request): return Response(status_code=401) return histogram_counts(project, req) - @api_app.post( - "/histogram-metrics/{project}", - response_model=list[list[float | None]], - tags=["zeno"], - ) - def calculate_histogram_metrics( - req: HistogramRequest, project: str, request: Request - ): - if not util.access_valid(project, request): - return Response(status_code=401) - return histogram_metrics(project, req) - @api_app.get( "/project-users/{project}", response_model=list[User], @@ -459,7 +512,7 @@ def filter_string_metadata( ): if not util.access_valid(project, request): return Response(status_code=401) - return select.filered_short_string_column_values(project, req) + return select.filtered_short_string_column_values(project, req) ####################################################################### Insert @api_app.post( @@ -476,7 +529,13 @@ def login(name: str): if fetched_user is None: try: user = User(id=-1, name=name, admin=None) - insert.user(user) + user_id = insert.user(user) + amplitude_client.track( + BaseEvent( + event_type="User Registered", + user_id="00000" + str(user_id) if user_id else "", + ) + ) insert.api_key(user) return select.user(name) except Exception as exc: @@ -485,19 +544,58 @@ def login(name: str): detail=str(exc), ) from exc else: + amplitude_client.track( + BaseEvent( + event_type="User Logged In", + user_id="00000" + str(fetched_user.id), + ) + ) return fetched_user - @api_app.post("/folder/{project}", tags=["zeno"], dependencies=[Depends(auth)]) + @api_app.post( + "/folder/{project}", + response_model=int, + tags=["zeno"], + dependencies=[Depends(auth)], + ) def add_folder(project: str, name: str): - insert.folder(project, name) + id = insert.folder(project, name) + if id is None: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to insert folder", + ) + return id - @api_app.post("/slice/{project}", tags=["zeno"], dependencies=[Depends(auth)]) + @api_app.post( + "/slice/{project}", + response_model=int, + tags=["zeno"], + dependencies=[Depends(auth)], + ) def add_slice(project: str, req: Slice): - insert.slice(project, req) + id = insert.slice(project, req) + if id is None: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to insert slice", + ) + return id - @api_app.post("/chart/{project}", tags=["zeno"], dependencies=[Depends(auth)]) + @api_app.post( + "/chart/{project}", + response_model=int, + tags=["zeno"], + dependencies=[Depends(auth)], + ) def add_chart(project: str, chart: Chart): - insert.chart(project, chart) + id = insert.chart(project, chart) + if id is None: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to insert chart", + ) + return id @api_app.post("/report/{name}", tags=["zeno"], dependencies=[Depends(auth)]) def add_report(name: str, current_user=Depends(auth.claim())): @@ -506,9 +604,20 @@ def add_report(name: str, current_user=Depends(auth.claim())): return Response(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR) insert.report(name, user) - @api_app.post("/tag/{project}", tags=["zeno"], dependencies=[Depends(auth)]) + @api_app.post( + "/tag/{project}", + response_model=int, + tags=["zeno"], + dependencies=[Depends(auth)], + ) def add_tag(tag: Tag, project: str): - insert.tag(project, tag) + id = insert.tag(project, tag) + if id is None: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to insert tag", + ) + return id @api_app.post("/add-organization", tags=["zeno"], dependencies=[Depends(auth)]) def add_organization(user: User, organization: Organization): diff --git a/frontend/.env.example b/frontend/.env.example index dea33dea..29c0d3e8 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -2,4 +2,5 @@ PUBLIC_BACKEND_ENDPOINT = "the url and port at which your backend is running, th ZENO_USER_POOL_AUTH_REGION = "cognito user pool auth region" ZENO_USER_POOL_CLIENT_ID = "cognito user pool client id" ZENO_USER_POOL_ID = "cognito user pool id" -ALLOW_INSECURE_HTTP = "true or false, whether to allow http or not" \ No newline at end of file +ALLOW_INSECURE_HTTP = "true or false, whether to allow http or not" +AMPLITUDE_API_KEY = "amplitude api key" \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 6ccab7d1..95c082de 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,7 +8,7 @@ "name": "hub", "version": "0.0.1", "dependencies": { - "@auth/core": "^0.12.0", + "@auth/core": "^0.13.0", "@auth/sveltekit": "^0.3.6", "@mdi/js": "^7.2.96", "amazon-cognito-identity-js": "^6.3.5", @@ -30,12 +30,12 @@ "devDependencies": { "@formkit/auto-animate": "^1.0.0-pre-alpha.3", "@playwright/test": "^1.28.1", - "@smui/button": "7.0.0-beta.14", - "@smui/card": "7.0.0-beta.14", + "@smui/button": "7.0.0-beta.15", + "@smui/card": "7.0.0-beta.15", "@smui/checkbox": "7.0.0-beta.14", "@smui/chips": "7.0.0-beta.8", "@smui/circular-progress": "7.0.0-beta.14", - "@smui/data-table": "7.0.0-beta.8", + "@smui/data-table": "7.0.0-beta.15", "@smui/dialog": "^7.0.0-beta.14", "@smui/form-field": "7.0.0-beta.14", "@smui/icon-button": "^7.0.0-beta.8", @@ -44,9 +44,9 @@ "@smui/paper": "7.0.0-beta.14", "@smui/ripple": "7.0.0-beta.14", "@smui/segmented-button": "^7.0.0-beta.8", - "@smui/select": "7.0.0-beta.14", + "@smui/select": "7.0.0-beta.15", "@smui/slider": "^7.0.0-beta.8", - "@smui/textfield": "^7.0.0-beta.8", + "@smui/textfield": "^7.0.0-beta.15", "@svelte-plugins/tooltips": "^0.1.6", "@sveltejs/adapter-node": "^1.3.1", "@sveltejs/kit": "^1.23.0", @@ -58,8 +58,8 @@ "d3-fetch": "^3.0.1", "eslint": "^8.46.0", "eslint-config-prettier": "^9.0.0", - "eslint-plugin-svelte": "^2.32.4", - "postcss": "^8.4.28", + "eslint-plugin-svelte": "^2.33.0", + "postcss": "^8.4.29", "prettier": "^2.8.0", "prettier-plugin-svelte": "^2.10.1", "simple-svelte-autocomplete": "^2.5.2", @@ -70,7 +70,7 @@ "svelte-highlight": "^7.3.0", "svelte-loading-spinners": "^0.3.4", "tailwindcss": "^3.3.3", - "tslib": "^2.4.1", + "tslib": "^2.6.2", "typescript": "^5.1.3", "uplot": "^1.6.25", "vite": "^4.4.9", @@ -99,9 +99,9 @@ } }, "node_modules/@auth/core": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.12.0.tgz", - "integrity": "sha512-XYipdAc/nKu014VOgpcPyLlj1ghWlnwyloaB1UjQd9ElZRZQ9YpSizzXGLON23t/a0FyabOBBl0/awD2tW58Rg==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.13.0.tgz", + "integrity": "sha512-StjrzUenaKfMr68kmvhiqfY0xvxRvg8wllmem9JAULpPAAAt3uwRUDXICWOP/PfWB4OZ2wQT7rgfm0n42b+Mjg==", "dependencies": { "@panva/hkdf": "^1.0.4", "cookie": "0.5.0", @@ -1414,9 +1414,9 @@ } }, "node_modules/@smui/button": { - "version": "7.0.0-beta.14", - "resolved": "https://registry.npmjs.org/@smui/button/-/button-7.0.0-beta.14.tgz", - "integrity": "sha512-xkYjj7E8kILhToDGcHoFB3Hdh7bgIjoRJP3X65wnZBJTCBcM1949e7b0lJBWvVMEMJ3F2aKFVrWpEdfllqZmkg==", + "version": "7.0.0-beta.15", + "resolved": "https://registry.npmjs.org/@smui/button/-/button-7.0.0-beta.15.tgz", + "integrity": "sha512-yfs/HkIjLDJROzUw+lvK26AV3S/XtAGkuRXEPUQuIzlfv8LAp9kIv2S3EPpxPj9xXAS/kiKu8HnFsyknl02dSw==", "dev": true, "dependencies": { "@material/button": "^14.0.0", @@ -1425,23 +1425,47 @@ "@material/ripple": "^14.0.0", "@material/shape": "^14.0.0", "@material/theme": "^14.0.0", - "@smui/common": "^7.0.0-beta.14", - "@smui/ripple": "^7.0.0-beta.14", - "svelte2tsx": "^0.6.15" + "@smui/common": "^7.0.0-beta.15", + "@smui/ripple": "^7.0.0-beta.15", + "svelte2tsx": "^0.6.21" + } + }, + "node_modules/@smui/button/node_modules/@smui/ripple": { + "version": "7.0.0-beta.15", + "resolved": "https://registry.npmjs.org/@smui/ripple/-/ripple-7.0.0-beta.15.tgz", + "integrity": "sha512-l0c94p60gxbsClH0KzA2meJ2IGHc7ZUMyWqODkLNz7ziUYxk3VZvWf/Y7Ca+64IJj0keCGPMpFauFaJO8h3Gtw==", + "dev": true, + "dependencies": { + "@material/dom": "^14.0.0", + "@material/ripple": "^14.0.0", + "@smui/common": "^7.0.0-beta.15", + "svelte2tsx": "^0.6.21" } }, "node_modules/@smui/card": { - "version": "7.0.0-beta.14", - "resolved": "https://registry.npmjs.org/@smui/card/-/card-7.0.0-beta.14.tgz", - "integrity": "sha512-xbOpHlL0rSxBscuWovWlH8wPjdrxDFy6m5MnlI+0VbszvYge0wpQPVhMtGSe196enFXU9VY7tglv4ELJVvywOA==", + "version": "7.0.0-beta.15", + "resolved": "https://registry.npmjs.org/@smui/card/-/card-7.0.0-beta.15.tgz", + "integrity": "sha512-fGBpgmO3N5kDz5oTFxqIF++ks8/h9I1ef0yD2OgyTYQ5vzYb+N/5NRmjV9g8QitPV2BRYeRLkrOYfn3nnjOCBg==", "dev": true, "dependencies": { "@material/card": "^14.0.0", - "@smui/button": "^7.0.0-beta.14", - "@smui/common": "^7.0.0-beta.14", - "@smui/icon-button": "^7.0.0-beta.14", - "@smui/ripple": "^7.0.0-beta.14", - "svelte2tsx": "^0.6.15" + "@smui/button": "7.0.0-beta.15", + "@smui/common": "^7.0.0-beta.15", + "@smui/icon-button": "^7.0.0-beta.15", + "@smui/ripple": "^7.0.0-beta.15", + "svelte2tsx": "^0.6.21" + } + }, + "node_modules/@smui/card/node_modules/@smui/ripple": { + "version": "7.0.0-beta.15", + "resolved": "https://registry.npmjs.org/@smui/ripple/-/ripple-7.0.0-beta.15.tgz", + "integrity": "sha512-l0c94p60gxbsClH0KzA2meJ2IGHc7ZUMyWqODkLNz7ziUYxk3VZvWf/Y7Ca+64IJj0keCGPMpFauFaJO8h3Gtw==", + "dev": true, + "dependencies": { + "@material/dom": "^14.0.0", + "@material/ripple": "^14.0.0", + "@smui/common": "^7.0.0-beta.15", + "svelte2tsx": "^0.6.21" } }, "node_modules/@smui/checkbox": { @@ -1482,29 +1506,53 @@ } }, "node_modules/@smui/common": { - "version": "7.0.0-beta.14", - "resolved": "https://registry.npmjs.org/@smui/common/-/common-7.0.0-beta.14.tgz", - "integrity": "sha512-Vw0fth7DiFKoVSJb5UMiXxACMyF65Wof/W23nEywx9rAh/v7kF7gi565d9BZp4sYvFj3hPy2M2Ev3p2tnxzLOw==", + "version": "7.0.0-beta.15", + "resolved": "https://registry.npmjs.org/@smui/common/-/common-7.0.0-beta.15.tgz", + "integrity": "sha512-mrK7B75L9so6EtzRolkAZkatC1sKHAP+gvIjU+JSfmSJnaRuglTS5ZTcosEmP759GA7E4scAQtOY1Qw7BgVO/g==", "dev": true, "dependencies": { "@material/dom": "^14.0.0", - "svelte2tsx": "^0.6.15" + "svelte2tsx": "^0.6.21" } }, "node_modules/@smui/data-table": { - "version": "7.0.0-beta.8", - "resolved": "https://registry.npmjs.org/@smui/data-table/-/data-table-7.0.0-beta.8.tgz", - "integrity": "sha512-sBmgQccJ4qasqbYIJD6fldu486lGPW38uLDgukjXDOuB9185f2pdgUhltCbP+sz+svHZDg/QXn0qEA4nks1Tbw==", + "version": "7.0.0-beta.15", + "resolved": "https://registry.npmjs.org/@smui/data-table/-/data-table-7.0.0-beta.15.tgz", + "integrity": "sha512-qrwzKr7wtvHjfTZ25UdoUgcembV0EY2KBVhAuzpG7Jr4vI9aUvpJQhFPkoQy4Fh+Ic0jPK49cmSRx0ZG3W7vHw==", "dev": true, "dependencies": { "@material/data-table": "^14.0.0", "@material/dom": "^14.0.0", - "@smui/checkbox": "^7.0.0-beta.8", - "@smui/common": "^7.0.0-beta.8", - "@smui/icon-button": "^7.0.0-beta.8", - "@smui/ripple": "^7.0.0-beta.8", - "@smui/select": "^7.0.0-beta.8", - "svelte2tsx": "^0.6.10" + "@smui/checkbox": "^7.0.0-beta.15", + "@smui/common": "^7.0.0-beta.15", + "@smui/icon-button": "^7.0.0-beta.15", + "@smui/ripple": "^7.0.0-beta.15", + "@smui/select": "7.0.0-beta.15", + "svelte2tsx": "^0.6.21" + } + }, + "node_modules/@smui/data-table/node_modules/@smui/checkbox": { + "version": "7.0.0-beta.15", + "resolved": "https://registry.npmjs.org/@smui/checkbox/-/checkbox-7.0.0-beta.15.tgz", + "integrity": "sha512-lDRHkec8qCNz8SAdV8G1XoFLdj340OVK0KoEszo/KTu+eIfIfkiIKrvZj5TEb8ohGV1j0QuaOivlhnP+CNDERA==", + "dev": true, + "dependencies": { + "@material/checkbox": "^14.0.0", + "@smui/common": "^7.0.0-beta.15", + "@smui/ripple": "^7.0.0-beta.15", + "svelte2tsx": "^0.6.21" + } + }, + "node_modules/@smui/data-table/node_modules/@smui/ripple": { + "version": "7.0.0-beta.15", + "resolved": "https://registry.npmjs.org/@smui/ripple/-/ripple-7.0.0-beta.15.tgz", + "integrity": "sha512-l0c94p60gxbsClH0KzA2meJ2IGHc7ZUMyWqODkLNz7ziUYxk3VZvWf/Y7Ca+64IJj0keCGPMpFauFaJO8h3Gtw==", + "dev": true, + "dependencies": { + "@material/dom": "^14.0.0", + "@material/ripple": "^14.0.0", + "@smui/common": "^7.0.0-beta.15", + "svelte2tsx": "^0.6.21" } }, "node_modules/@smui/dialog": { @@ -1521,14 +1569,14 @@ } }, "node_modules/@smui/floating-label": { - "version": "7.0.0-beta.14", - "resolved": "https://registry.npmjs.org/@smui/floating-label/-/floating-label-7.0.0-beta.14.tgz", - "integrity": "sha512-VVXWJ15paZlqrnUl2FKsF81KSDjV/uWaTTRIT1kEW/McTce4xPpxk5P/pPF665yNbbBhnSRmnjSI+B63bhJpSg==", + "version": "7.0.0-beta.15", + "resolved": "https://registry.npmjs.org/@smui/floating-label/-/floating-label-7.0.0-beta.15.tgz", + "integrity": "sha512-KRjZjO8AGE5uKHewmn05vyAxIi6XOGo8a4jn8b0+dIJcYxaPpmtacH4PeQHw4vOR88cX/0DqqxSz6+Fkrt+SIA==", "dev": true, "dependencies": { "@material/floating-label": "^14.0.0", - "@smui/common": "^7.0.0-beta.14", - "svelte2tsx": "^0.6.15" + "@smui/common": "^7.0.0-beta.15", + "svelte2tsx": "^0.6.21" } }, "node_modules/@smui/form-field": { @@ -1545,16 +1593,28 @@ } }, "node_modules/@smui/icon-button": { - "version": "7.0.0-beta.14", - "resolved": "https://registry.npmjs.org/@smui/icon-button/-/icon-button-7.0.0-beta.14.tgz", - "integrity": "sha512-bNV0lwc3UEKcRmUieno5UljYSYrR+/GDUGciKunwK83NpdRxdZPCssYuHPuUGloMhrnaoqLbBSa2qY6CM9m4DA==", + "version": "7.0.0-beta.15", + "resolved": "https://registry.npmjs.org/@smui/icon-button/-/icon-button-7.0.0-beta.15.tgz", + "integrity": "sha512-Hvvv9w6hDCDOrSgxlcsXrCLOE8VJeTJ5Tu1n+FBqvIfetaF7ATXDwXx+raEznJ4kZK6CyoQTzMIr2rz4hxGiMA==", "dev": true, "dependencies": { "@material/density": "^14.0.0", "@material/icon-button": "^14.0.0", - "@smui/common": "^7.0.0-beta.14", - "@smui/ripple": "^7.0.0-beta.14", - "svelte2tsx": "^0.6.15" + "@smui/common": "^7.0.0-beta.15", + "@smui/ripple": "^7.0.0-beta.15", + "svelte2tsx": "^0.6.21" + } + }, + "node_modules/@smui/icon-button/node_modules/@smui/ripple": { + "version": "7.0.0-beta.15", + "resolved": "https://registry.npmjs.org/@smui/ripple/-/ripple-7.0.0-beta.15.tgz", + "integrity": "sha512-l0c94p60gxbsClH0KzA2meJ2IGHc7ZUMyWqODkLNz7ziUYxk3VZvWf/Y7Ca+64IJj0keCGPMpFauFaJO8h3Gtw==", + "dev": true, + "dependencies": { + "@material/dom": "^14.0.0", + "@material/ripple": "^14.0.0", + "@smui/common": "^7.0.0-beta.15", + "svelte2tsx": "^0.6.21" } }, "node_modules/@smui/layout-grid": { @@ -1569,14 +1629,14 @@ } }, "node_modules/@smui/line-ripple": { - "version": "7.0.0-beta.14", - "resolved": "https://registry.npmjs.org/@smui/line-ripple/-/line-ripple-7.0.0-beta.14.tgz", - "integrity": "sha512-9BsByyEizUPG7b4VRX3Wahmb300wlJSxgyY1EYVIna1mnKRXwvBp2TKjAnOZ8y8tKhOlfLJXFI8uVoEGnLK8BA==", + "version": "7.0.0-beta.15", + "resolved": "https://registry.npmjs.org/@smui/line-ripple/-/line-ripple-7.0.0-beta.15.tgz", + "integrity": "sha512-tfdEybgQlLiotELlWknLzb5l6pT47uGfMGYzLs5AVf74CU/zAhp8NiicHUUmI95qY5P2z2xHYYpdLTs1wZ5c9A==", "dev": true, "dependencies": { "@material/line-ripple": "^14.0.0", - "@smui/common": "^7.0.0-beta.14", - "svelte2tsx": "^0.6.15" + "@smui/common": "^7.0.0-beta.15", + "svelte2tsx": "^0.6.21" } }, "node_modules/@smui/list": { @@ -1594,55 +1654,67 @@ } }, "node_modules/@smui/menu": { - "version": "7.0.0-beta.14", - "resolved": "https://registry.npmjs.org/@smui/menu/-/menu-7.0.0-beta.14.tgz", - "integrity": "sha512-CdHdvy9X+qYTzARD+k3op/j90fSQCKkDINog8Y7O1WNxU6sFIhVTVlQWPq6y94VoOIBENJIsukjpGUuEmjZQZw==", + "version": "7.0.0-beta.15", + "resolved": "https://registry.npmjs.org/@smui/menu/-/menu-7.0.0-beta.15.tgz", + "integrity": "sha512-hvw8iRuWvo1Lc1O6MaAtO99vIUQMlheK5vvG046WZlzwuiZMulBvtipc74nhM69BdrTTDAob69xsZXHih3LHVw==", "dev": true, "dependencies": { "@material/dom": "^14.0.0", "@material/menu": "^14.0.0", - "@smui/common": "^7.0.0-beta.14", - "@smui/list": "^7.0.0-beta.14", - "@smui/menu-surface": "^7.0.0-beta.14", - "svelte2tsx": "^0.6.15" + "@smui/common": "^7.0.0-beta.15", + "@smui/list": "^7.0.0-beta.15", + "@smui/menu-surface": "^7.0.0-beta.15", + "svelte2tsx": "^0.6.21" } }, "node_modules/@smui/menu-surface": { - "version": "7.0.0-beta.14", - "resolved": "https://registry.npmjs.org/@smui/menu-surface/-/menu-surface-7.0.0-beta.14.tgz", - "integrity": "sha512-2wo7GXNt6+JRiWjy+ocwnkLaFwRMQ0glJusZl/azPO9qCNyjMCisL2iU9wSGExCkjUnCGutCre0Zb63BwRhiQw==", + "version": "7.0.0-beta.15", + "resolved": "https://registry.npmjs.org/@smui/menu-surface/-/menu-surface-7.0.0-beta.15.tgz", + "integrity": "sha512-fpBJD1+gBUdegMM0qoZg9Fu5j/9TfAtxDBRBqAoHWKY7sW9uqbHhV1lwkVLWs7V+PKXWamTIX37//EHSIVw0TQ==", "dev": true, "dependencies": { "@material/animation": "^14.0.0", "@material/menu-surface": "^14.0.0", - "@smui/common": "^7.0.0-beta.14", - "svelte2tsx": "^0.6.15" + "@smui/common": "^7.0.0-beta.15", + "svelte2tsx": "^0.6.21" } }, "node_modules/@smui/menu/node_modules/@smui/list": { - "version": "7.0.0-beta.14", - "resolved": "https://registry.npmjs.org/@smui/list/-/list-7.0.0-beta.14.tgz", - "integrity": "sha512-HzHxe360OcnNpbdp6mmcAk98kTks/R4FjlV6mb54HRVfYjMB3ZvzUzKE/kz0BRzkRXFVXQN9llEL+eDASOTA4A==", + "version": "7.0.0-beta.15", + "resolved": "https://registry.npmjs.org/@smui/list/-/list-7.0.0-beta.15.tgz", + "integrity": "sha512-5NeAgVLjcaJo0Mr4IXB1QaAb3UGBtNWJqHNwVXfPpADIb3vKLNGpKrXv5koKqCLGEapkEjQ2PqixURaB/5O2eg==", "dev": true, "dependencies": { "@material/dom": "^14.0.0", "@material/feature-targeting": "^14.0.0", "@material/list": "^14.0.0", - "@smui/common": "^7.0.0-beta.14", - "@smui/ripple": "^7.0.0-beta.14", - "svelte2tsx": "^0.6.15" + "@smui/common": "^7.0.0-beta.15", + "@smui/ripple": "^7.0.0-beta.15", + "svelte2tsx": "^0.6.21" + } + }, + "node_modules/@smui/menu/node_modules/@smui/ripple": { + "version": "7.0.0-beta.15", + "resolved": "https://registry.npmjs.org/@smui/ripple/-/ripple-7.0.0-beta.15.tgz", + "integrity": "sha512-l0c94p60gxbsClH0KzA2meJ2IGHc7ZUMyWqODkLNz7ziUYxk3VZvWf/Y7Ca+64IJj0keCGPMpFauFaJO8h3Gtw==", + "dev": true, + "dependencies": { + "@material/dom": "^14.0.0", + "@material/ripple": "^14.0.0", + "@smui/common": "^7.0.0-beta.15", + "svelte2tsx": "^0.6.21" } }, "node_modules/@smui/notched-outline": { - "version": "7.0.0-beta.14", - "resolved": "https://registry.npmjs.org/@smui/notched-outline/-/notched-outline-7.0.0-beta.14.tgz", - "integrity": "sha512-gblu47Jrn6ifEm86FGfkwaR2uG7yyiZgvZb2oxrv/fULFHIVZ5b9hHbHuv3PlDmxT79J+Z2OIxb55dOKxGQ8Ng==", + "version": "7.0.0-beta.15", + "resolved": "https://registry.npmjs.org/@smui/notched-outline/-/notched-outline-7.0.0-beta.15.tgz", + "integrity": "sha512-9oVSIYmb1/GcCFYm1M0xVy4J5/A2Ysa4H6gm3RIL9/ke3F9EmmALmCs1lmtGu4Vwyo0ES5KroW2Tly2SzMfB2g==", "dev": true, "dependencies": { "@material/notched-outline": "^14.0.0", - "@smui/common": "^7.0.0-beta.14", - "@smui/floating-label": "^7.0.0-beta.14", - "svelte2tsx": "^0.6.15" + "@smui/common": "^7.0.0-beta.15", + "@smui/floating-label": "^7.0.0-beta.15", + "svelte2tsx": "^0.6.21" } }, "node_modules/@smui/paper": { @@ -1687,9 +1759,9 @@ } }, "node_modules/@smui/select": { - "version": "7.0.0-beta.14", - "resolved": "https://registry.npmjs.org/@smui/select/-/select-7.0.0-beta.14.tgz", - "integrity": "sha512-pv3xdnbHLYE72qFfTy3Kx/sTsezyHheEdcXNeQ6DKZ9v/2RcJzlQSA/Q/Qj559ZWH1m6XOK9kXk1HjtsaQk6bg==", + "version": "7.0.0-beta.15", + "resolved": "https://registry.npmjs.org/@smui/select/-/select-7.0.0-beta.15.tgz", + "integrity": "sha512-quYIEaFDt5aXOj3ZnV5axDDWkrkASAWcRzd/P1PQ2bCAQNaaichaUOV+2QGbYVD8G+UiRBYy8fgJMpIE9s9KUg==", "dev": true, "dependencies": { "@material/feature-targeting": "^14.0.0", @@ -1697,29 +1769,41 @@ "@material/rtl": "^14.0.0", "@material/select": "^14.0.0", "@material/theme": "^14.0.0", - "@smui/common": "^7.0.0-beta.14", - "@smui/floating-label": "^7.0.0-beta.14", - "@smui/line-ripple": "^7.0.0-beta.14", - "@smui/list": "^7.0.0-beta.14", - "@smui/menu": "^7.0.0-beta.14", - "@smui/menu-surface": "^7.0.0-beta.14", - "@smui/notched-outline": "^7.0.0-beta.14", - "@smui/ripple": "^7.0.0-beta.14", - "svelte2tsx": "^0.6.15" + "@smui/common": "^7.0.0-beta.15", + "@smui/floating-label": "^7.0.0-beta.15", + "@smui/line-ripple": "^7.0.0-beta.15", + "@smui/list": "^7.0.0-beta.15", + "@smui/menu": "^7.0.0-beta.15", + "@smui/menu-surface": "^7.0.0-beta.15", + "@smui/notched-outline": "^7.0.0-beta.15", + "@smui/ripple": "^7.0.0-beta.15", + "svelte2tsx": "^0.6.21" } }, "node_modules/@smui/select/node_modules/@smui/list": { - "version": "7.0.0-beta.14", - "resolved": "https://registry.npmjs.org/@smui/list/-/list-7.0.0-beta.14.tgz", - "integrity": "sha512-HzHxe360OcnNpbdp6mmcAk98kTks/R4FjlV6mb54HRVfYjMB3ZvzUzKE/kz0BRzkRXFVXQN9llEL+eDASOTA4A==", + "version": "7.0.0-beta.15", + "resolved": "https://registry.npmjs.org/@smui/list/-/list-7.0.0-beta.15.tgz", + "integrity": "sha512-5NeAgVLjcaJo0Mr4IXB1QaAb3UGBtNWJqHNwVXfPpADIb3vKLNGpKrXv5koKqCLGEapkEjQ2PqixURaB/5O2eg==", "dev": true, "dependencies": { "@material/dom": "^14.0.0", "@material/feature-targeting": "^14.0.0", "@material/list": "^14.0.0", - "@smui/common": "^7.0.0-beta.14", - "@smui/ripple": "^7.0.0-beta.14", - "svelte2tsx": "^0.6.15" + "@smui/common": "^7.0.0-beta.15", + "@smui/ripple": "^7.0.0-beta.15", + "svelte2tsx": "^0.6.21" + } + }, + "node_modules/@smui/select/node_modules/@smui/ripple": { + "version": "7.0.0-beta.15", + "resolved": "https://registry.npmjs.org/@smui/ripple/-/ripple-7.0.0-beta.15.tgz", + "integrity": "sha512-l0c94p60gxbsClH0KzA2meJ2IGHc7ZUMyWqODkLNz7ziUYxk3VZvWf/Y7Ca+64IJj0keCGPMpFauFaJO8h3Gtw==", + "dev": true, + "dependencies": { + "@material/dom": "^14.0.0", + "@material/ripple": "^14.0.0", + "@smui/common": "^7.0.0-beta.15", + "svelte2tsx": "^0.6.21" } }, "node_modules/@smui/slider": { @@ -1736,9 +1820,9 @@ } }, "node_modules/@smui/textfield": { - "version": "7.0.0-beta.14", - "resolved": "https://registry.npmjs.org/@smui/textfield/-/textfield-7.0.0-beta.14.tgz", - "integrity": "sha512-bFAHuECVrNxeHdLgirkhtzbytZ8WqyV4aOv4pmTm6SB/K8YgdnzKT8DLWzvx3Ma+OkV1E//u2DpvcvnpB+VN4g==", + "version": "7.0.0-beta.15", + "resolved": "https://registry.npmjs.org/@smui/textfield/-/textfield-7.0.0-beta.15.tgz", + "integrity": "sha512-98lGaVJRH3K5aXVUAa8o/8rHRQ/Vwu9q6phupSrPW4BEmEEEezb/fzKExTOgm0c241dy4nDRLkdH9IdtObTA8w==", "dev": true, "dependencies": { "@material/dom": "^14.0.0", @@ -1746,12 +1830,24 @@ "@material/ripple": "^14.0.0", "@material/rtl": "^14.0.0", "@material/textfield": "^14.0.0", - "@smui/common": "^7.0.0-beta.14", - "@smui/floating-label": "^7.0.0-beta.14", - "@smui/line-ripple": "^7.0.0-beta.14", - "@smui/notched-outline": "^7.0.0-beta.14", - "@smui/ripple": "^7.0.0-beta.14", - "svelte2tsx": "^0.6.15" + "@smui/common": "^7.0.0-beta.15", + "@smui/floating-label": "^7.0.0-beta.15", + "@smui/line-ripple": "^7.0.0-beta.15", + "@smui/notched-outline": "^7.0.0-beta.15", + "@smui/ripple": "^7.0.0-beta.15", + "svelte2tsx": "^0.6.21" + } + }, + "node_modules/@smui/textfield/node_modules/@smui/ripple": { + "version": "7.0.0-beta.15", + "resolved": "https://registry.npmjs.org/@smui/ripple/-/ripple-7.0.0-beta.15.tgz", + "integrity": "sha512-l0c94p60gxbsClH0KzA2meJ2IGHc7ZUMyWqODkLNz7ziUYxk3VZvWf/Y7Ca+64IJj0keCGPMpFauFaJO8h3Gtw==", + "dev": true, + "dependencies": { + "@material/dom": "^14.0.0", + "@material/ripple": "^14.0.0", + "@smui/common": "^7.0.0-beta.15", + "svelte2tsx": "^0.6.21" } }, "node_modules/@svelte-plugins/tooltips": { @@ -3181,9 +3277,9 @@ } }, "node_modules/eslint-plugin-svelte": { - "version": "2.32.4", - "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-2.32.4.tgz", - "integrity": "sha512-VJ12i2Iogug1jvhwxSlognnfGj76P5gks/V4pUD4SCSVQOp14u47MNP0zAG8AQR3LT0Fi1iUvIFnY4l9z5Rwbg==", + "version": "2.33.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-2.33.0.tgz", + "integrity": "sha512-kk7Z4BfxVjFYJseFcOpS8kiKNio7KnAnhFagmM89h1wNSKlM7tIn+uguNQppKM9leYW+S+Us0Rjg2Qg3zsEcvg==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", @@ -3196,7 +3292,7 @@ "postcss-safe-parser": "^6.0.0", "postcss-selector-parser": "^6.0.11", "semver": "^7.5.3", - "svelte-eslint-parser": "^0.32.2" + "svelte-eslint-parser": ">=0.33.0 <1.0.0" }, "engines": { "node": "^14.17.0 || >=16.0.0" @@ -4476,9 +4572,9 @@ } }, "node_modules/postcss": { - "version": "8.4.28", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.28.tgz", - "integrity": "sha512-Z7V5j0cq8oEKyejIKfpD8b4eBy9cwW2JWPk0+fB1HOAMsfHbnAXLLS+PfVWlzMSLQaWttKDt607I0XHmpE67Vw==", + "version": "8.4.29", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.29.tgz", + "integrity": "sha512-cbI+jaqIeu/VGqXEarWkRCCffhjgXc0qjBtXpqJhTBohMUjUQnbBr0xqX3vEKudc4iviTewcJo5ajcec5+wdJw==", "funding": [ { "type": "opencollective", @@ -4603,9 +4699,9 @@ } }, "node_modules/postcss-scss": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.6.tgz", - "integrity": "sha512-rLDPhJY4z/i4nVFZ27j9GqLxj1pwxE80eAzUNRMXtcpipFYIeowerzBgG3yJhMtObGEXidtIgbUpQ3eLDsf5OQ==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.7.tgz", + "integrity": "sha512-xPv2GseoyXPa58Nro7M73ZntttusuCmZdeOojUFR5PZDz2BR62vfYx1w9TyOnp1+nYFowgOMipsCBhxzVkAEPw==", "dev": true, "funding": [ { @@ -4615,6 +4711,10 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/postcss-scss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "engines": { @@ -5337,16 +5437,16 @@ } }, "node_modules/svelte-eslint-parser": { - "version": "0.32.2", - "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.32.2.tgz", - "integrity": "sha512-Ok9D3A4b23iLQsONrjqtXtYDu5ZZ/826Blaw2LeFZVTg1pwofKDG4mz3/GYTax8fQ0plRGHI6j+d9VQYy5Lo/A==", + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.33.0.tgz", + "integrity": "sha512-5awZ6Bs+Tb/zQwa41PSdcLynAVQTwW0HGyCBjtbAQ59taLZqDgQSMzRlDmapjZdDtzERm0oXDZNE0E+PKJ6ryg==", "dev": true, "dependencies": { "eslint-scope": "^7.0.0", "eslint-visitor-keys": "^3.0.0", "espree": "^9.0.0", - "postcss": "^8.4.25", - "postcss-scss": "^4.0.6" + "postcss": "^8.4.28", + "postcss-scss": "^4.0.7" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -5497,9 +5597,9 @@ } }, "node_modules/svelte2tsx": { - "version": "0.6.19", - "resolved": "https://registry.npmjs.org/svelte2tsx/-/svelte2tsx-0.6.19.tgz", - "integrity": "sha512-h3b5OtcO8zyVL/RiB2zsDwCopeo/UH+887uyhgb2mjnewOFwiTxu+4IGuVwrrlyuh2onM2ktfUemNrNmQwXONQ==", + "version": "0.6.21", + "resolved": "https://registry.npmjs.org/svelte2tsx/-/svelte2tsx-0.6.21.tgz", + "integrity": "sha512-v+vvbiy6WDmEQdIkJpvHYxJYG/obALfH0P6CTreYO350q/9+QmFTNCOJvx0O1o59Zpzx1Bqe+qlDxP/KtJSZEA==", "dev": true, "dependencies": { "dedent-js": "^1.0.1", @@ -5702,9 +5802,9 @@ } }, "node_modules/tslib": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.0.tgz", - "integrity": "sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/tsutils": { "version": "3.21.0", @@ -5922,11 +6022,6 @@ "vega-lite": "*" } }, - "node_modules/vega-embed/node_modules/tslib": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.1.tgz", - "integrity": "sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==" - }, "node_modules/vega-encode": { "version": "4.9.2", "resolved": "https://registry.npmjs.org/vega-encode/-/vega-encode-4.9.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 776a948b..b51f7643 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -18,12 +18,12 @@ "devDependencies": { "@formkit/auto-animate": "^1.0.0-pre-alpha.3", "@playwright/test": "^1.28.1", - "@smui/button": "7.0.0-beta.14", - "@smui/card": "7.0.0-beta.14", + "@smui/button": "7.0.0-beta.15", + "@smui/card": "7.0.0-beta.15", "@smui/checkbox": "7.0.0-beta.14", "@smui/chips": "7.0.0-beta.8", "@smui/circular-progress": "7.0.0-beta.14", - "@smui/data-table": "7.0.0-beta.8", + "@smui/data-table": "7.0.0-beta.15", "@smui/dialog": "^7.0.0-beta.14", "@smui/form-field": "7.0.0-beta.14", "@smui/icon-button": "^7.0.0-beta.8", @@ -32,9 +32,9 @@ "@smui/paper": "7.0.0-beta.14", "@smui/ripple": "7.0.0-beta.14", "@smui/segmented-button": "^7.0.0-beta.8", - "@smui/select": "7.0.0-beta.14", + "@smui/select": "7.0.0-beta.15", "@smui/slider": "^7.0.0-beta.8", - "@smui/textfield": "^7.0.0-beta.8", + "@smui/textfield": "^7.0.0-beta.15", "@svelte-plugins/tooltips": "^0.1.6", "@sveltejs/adapter-node": "^1.3.1", "@sveltejs/kit": "^1.23.0", @@ -46,8 +46,8 @@ "d3-fetch": "^3.0.1", "eslint": "^8.46.0", "eslint-config-prettier": "^9.0.0", - "eslint-plugin-svelte": "^2.32.4", - "postcss": "^8.4.28", + "eslint-plugin-svelte": "^2.33.0", + "postcss": "^8.4.29", "prettier": "^2.8.0", "prettier-plugin-svelte": "^2.10.1", "simple-svelte-autocomplete": "^2.5.2", @@ -58,14 +58,14 @@ "svelte-highlight": "^7.3.0", "svelte-loading-spinners": "^0.3.4", "tailwindcss": "^3.3.3", - "tslib": "^2.4.1", + "tslib": "^2.6.2", "typescript": "^5.1.3", "uplot": "^1.6.25", "vite": "^4.4.9", "vitest": "^0.25.3" }, "dependencies": { - "@auth/core": "^0.12.0", + "@auth/core": "^0.13.0", "@auth/sveltekit": "^0.3.6", "@mdi/js": "^7.2.96", "amazon-cognito-identity-js": "^6.3.5", diff --git a/frontend/src/lib/api/metadata.ts b/frontend/src/lib/api/metadata.ts index 49b7571c..6dc26564 100644 --- a/frontend/src/lib/api/metadata.ts +++ b/frontend/src/lib/api/metadata.ts @@ -4,26 +4,19 @@ * can run then asynchronously and provide interactive updates while waiting * for more expensive computations like calculating metrics. */ -import { columns, metricRange, project, requestingHistogramCounts } from '$lib/stores'; +import { metricRange, requestingHistogramCounts } from '$lib/stores'; import { getMetricRange } from '$lib/util/util'; import { CancelablePromise, ZenoColumnType, ZenoService, type FilterPredicateGroup, + type HistogramBucket, type Metric, type ZenoColumn } from '$lib/zenoapi'; import { get } from 'svelte/store'; -export interface HistogramEntry { - bucket: number | string | boolean; - bucketEnd?: number | string | boolean | null; - count?: number; - filteredCount?: number; - metric?: number; -} - /** * Fetch metadata columns buckets for histograms. * @@ -33,30 +26,27 @@ export interface HistogramEntry { * @returns Histogram buckets for each column. */ export async function getHistograms( + project_uuid: string | undefined, completeColumns: ZenoColumn[], model: string | undefined -): Promise> { +): Promise> { + if (!project_uuid) { + return new Map(); + } const requestedHistograms = completeColumns.filter( (c) => (c.model === null || c.model === model) && c.columnType !== ZenoColumnType.DATA ); + requestingHistogramCounts.set(true); - const config = get(project); - if (!config) { - return Promise.reject('No project selected.'); - } - const res = await ZenoService.getHistogramBuckets(config.uuid, requestedHistograms); + const res = await ZenoService.getHistogramBuckets(project_uuid, requestedHistograms); requestingHistogramCounts.set(false); - const histograms = new Map( - [ - ...new Map(requestedHistograms.map((col, i) => [col.id, res[i]])) - ].filter((el) => el[1].length < 30) - ); - return histograms; + + return new Map(requestedHistograms.map((col, i) => [col.id, res[i]])); } // Since a user might change the selection before we get counts, // make this fetch request cancellable. -let histogramCountRequest: CancelablePromise>>; +let histogramRequest: CancelablePromise>>; /** * Fetch histogram counts for the buckets of metadata columns. * @@ -64,115 +54,52 @@ let histogramCountRequest: CancelablePromise>>; * @param filterPredicates Filter predicates to filter DataFrame by. * @returns Histogram counts for each column. */ -export async function getHistogramCounts( - histograms: Map, +export async function calculateHistograms( + project_uuid: string | undefined, + columns: ZenoColumn[], + histograms: Map, filterPredicates?: FilterPredicateGroup, - dataIds?: string[] -): Promise | undefined> { - const columnRequests = [...histograms.entries()].map(([k, v]) => ({ - column: get(columns).find((col) => col.id === k) ?? get(columns)[0], - buckets: v - })); - if (histogramCountRequest) { - histogramCountRequest.cancel(); - } - try { - const config = get(project); - if (!config) { - return Promise.reject('No project selected.'); - } - requestingHistogramCounts.set(true); - histogramCountRequest = ZenoService.calculateHistogramCounts(config.uuid, { - columnRequests, - filterPredicates, - dataIds - }); - const out = await histogramCountRequest; - requestingHistogramCounts.set(false); - - [...histograms.keys()].forEach((k, i) => { - const hist = histograms.get(k); - if (hist) { - histograms.set( - k, - hist.map((h, j) => { - if (filterPredicates === undefined) { - h.count = out[i][j]; - } - h.filteredCount = out[i][j]; - return h; - }) - ); - } - }); - return histograms; - } catch (e) { - return undefined; - } -} - -// Since a user might change the selection before we get metrics, -// make this fetch request cancellable. -let histogramMetricRequest: CancelablePromise>>; -/** - * Fetch histogram metrics for the buckets of metadata columns. - * - * @param histograms Histogram buckets for each column. - * @param filterPredicates Filter predicates to filter DataFrame by. - * @param model Model to fetch metrics for. - * @param metric Metric to calculate per bucket. - * @returns Histogram metrics for each column. - */ -export async function getHistogramMetrics( - histograms: Map, - model: string | undefined, - metric: Metric, - dataIds: string[] | undefined, - filterPredicates?: FilterPredicateGroup -): Promise | undefined> { - const config = get(project); - if (!config || !config.calculateHistogramMetrics) { - return undefined; + dataIds?: string[], + model?: string | null, + metric?: Metric | null +): Promise> { + if (!project_uuid) { + return new Map(); } const columnRequests = [...histograms.entries()].map(([k, v]) => ({ - column: get(columns).find((col) => col.id === k) ?? get(columns)[0], + column: columns.find((col) => col.id === k) ?? columns[0], buckets: v })); - if (histogramMetricRequest) { - histogramMetricRequest.cancel(); + if (histogramRequest) { + histogramRequest.cancel(); } try { - const config = get(project); - if (!config) { - return Promise.reject('No project selected.'); - } - histogramMetricRequest = ZenoService.calculateHistogramMetrics(config.uuid, { + requestingHistogramCounts.set(true); + histogramRequest = ZenoService.calculateHistograms(project_uuid, { columnRequests, filterPredicates, - model: model ?? null, + model, metric, dataIds }); - - requestingHistogramCounts.set(true); - const res = await histogramMetricRequest; + const out = await histogramRequest; requestingHistogramCounts.set(false); - if (res === undefined) { - return undefined; - } - if (get(metricRange)[0] === Infinity) { - metricRange.set(getMetricRange(res)); + metricRange.set(getMetricRange(out)); } [...histograms.keys()].forEach((k, i) => { const hist = histograms.get(k); - if (hist !== undefined) { + if (hist) { histograms.set( k, hist.map((h, j) => { - h.metric = res[i][j] || 0; + if (filterPredicates === undefined) { + h.size = out[i][j].size; + } + h.metric = out[i][j].metric; + h.filteredSize = out[i][j].size; return h; }) ); @@ -180,6 +107,6 @@ export async function getHistogramMetrics( }); return histograms; } catch (e) { - return undefined; + return histograms; } } diff --git a/frontend/src/lib/api/slice.ts b/frontend/src/lib/api/slice.ts index 5a513bd7..4731af67 100644 --- a/frontend/src/lib/api/slice.ts +++ b/frontend/src/lib/api/slice.ts @@ -1,78 +1,17 @@ import { project } from '$lib/stores'; -import { isPredicateGroup } from '$lib/util/typeCheck'; -import { - ZenoColumnType, - ZenoService, - type FilterPredicate, - type FilterPredicateGroup, - type GroupMetric, - type MetricKey -} from '$lib/zenoapi'; +import { ZenoService, type FilterPredicate, type GroupMetric, type MetricKey } from '$lib/zenoapi'; import { get } from 'svelte/store'; export function instanceOfFilterPredicate(object: object): object is FilterPredicate { return 'column' in object; } -export function setModelForFilterPredicateGroup( - pred: FilterPredicateGroup | FilterPredicate, - model: string -): FilterPredicate | FilterPredicateGroup { - if (instanceOfFilterPredicate(pred)) { - if ( - (pred.column.columnType === ZenoColumnType.FEATURE && - pred.column.model !== undefined && - pred.column.model !== null) || - pred.column.columnType === ZenoColumnType.OUTPUT - ) { - pred = { - ...pred, - column: { - ...pred.column, - model: model - } - }; - } - } else { - return { - ...pred, - predicates: pred.predicates.map((p) => setModelForFilterPredicateGroup(p, model)) - }; - } - return pred; -} - -function setModelForMetricKeys(metricKeys: MetricKey[]) { - return metricKeys.map((key) => { - if (key.slice.filterPredicates && key.slice.filterPredicates.predicates.length > 0) { - return { - ...key, - slice: { - ...key.slice, - filterPredicates: { - ...key.slice.filterPredicates, - predicates: key.slice.filterPredicates.predicates.map((pred) => { - return { - ...pred, - ...setModelForFilterPredicateGroup(pred, key.model) - }; - }) - } - } - }; - } - return { ...key }; - }); -} - const metricKeyCache = new Map(); export async function getMetricsForSlices(metricKeys: MetricKey[]): Promise { if (metricKeys.length === 0) { return null; } - // Update model in predicates if slices are dependent on feature or output columns. - metricKeys = setModelForMetricKeys(metricKeys); // Check if we have already fetched this metric key const keysToRequest: MetricKey[] = []; const requestIndices: number[] = []; @@ -105,16 +44,11 @@ export async function getMetricsForSlices(metricKeys: MetricKey[]): Promise { if (metricKeys.length === 0) { return undefined; } - // Update model in predicates if slices are dependent on feature columns. - if (!compare) { - metricKeys = setModelForMetricKeys(metricKeys); - } if (metricKeys.length > 0) { const config = get(project); if (!config) { @@ -126,21 +60,3 @@ export async function getMetricsForSlicesAndTags( }); } } - -// check if predicates contain model dependent columns (feature or output) -export function doesModelDependOnPredicates( - predicates: Array -) { - const isModelDependent: boolean[] = []; - predicates.forEach((p) => { - isModelDependent.push( - isPredicateGroup(p) - ? doesModelDependOnPredicates(p.predicates) - : (p.column.columnType === ZenoColumnType.FEATURE && - p.column.model !== undefined && - p.column.model !== null) || - p.column.columnType === ZenoColumnType.OUTPUT - ); - }); - return isModelDependent.includes(true); -} diff --git a/frontend/src/lib/api/table.ts b/frontend/src/lib/api/table.ts index 8f27482a..204f53bb 100644 --- a/frontend/src/lib/api/table.ts +++ b/frontend/src/lib/api/table.ts @@ -42,6 +42,7 @@ export async function getFilteredTable( } const res = await ZenoService.getFilteredTable(config.uuid, { columns: requestedColumns, + model: filterModels[0], diffColumn1, diffColumn2, filterPredicates, diff --git a/frontend/src/lib/components/chart/ChartHomeBlock.svelte b/frontend/src/lib/components/chart/ChartHomeBlock.svelte index aa3e387a..6109f71b 100644 --- a/frontend/src/lib/components/chart/ChartHomeBlock.svelte +++ b/frontend/src/lib/components/chart/ChartHomeBlock.svelte @@ -9,9 +9,8 @@ mdiViewGrid } from '@mdi/js'; - import { goto } from '$app/navigation'; - import { page } from '$app/stores'; - import { charts, project } from '$lib/stores'; + import { goto, invalidate } from '$app/navigation'; + import { project } from '$lib/stores'; import { clickOutside } from '$lib/util/clickOutside'; import { ChartType, ZenoService, type Chart } from '$lib/zenoapi'; import { Icon } from '@smui/button'; @@ -33,7 +32,7 @@ - {/if} + {#if $project?.editor} + + {/if} diff --git a/frontend/src/lib/components/chart/chart-page/encoding/chart-encoding/encoding-components/MetricsEncodingDropdown.svelte b/frontend/src/lib/components/chart/chart-page/encoding/chart-encoding/encoding-components/MetricsEncodingDropdown.svelte index 7b34e9e7..1d175aad 100644 --- a/frontend/src/lib/components/chart/chart-page/encoding/chart-encoding/encoding-components/MetricsEncodingDropdown.svelte +++ b/frontend/src/lib/components/chart/chart-page/encoding/chart-encoding/encoding-components/MetricsEncodingDropdown.svelte @@ -14,6 +14,7 @@ let options: { value: number; label: string }[] = []; let value = 0; + options.push({ value: -1, label: 'slice size' }); $metrics.forEach((m) => { options.push({ value: m.id, label: m.name }); }); diff --git a/frontend/src/lib/components/chart/chart-page/encoding/chart-encoding/encoding-components/MetricsEncodingMultiChoice.svelte b/frontend/src/lib/components/chart/chart-page/encoding/chart-encoding/encoding-components/MetricsEncodingMultiChoice.svelte index 03e39a5e..7effa9de 100644 --- a/frontend/src/lib/components/chart/chart-page/encoding/chart-encoding/encoding-components/MetricsEncodingMultiChoice.svelte +++ b/frontend/src/lib/components/chart/chart-page/encoding/chart-encoding/encoding-components/MetricsEncodingMultiChoice.svelte @@ -13,6 +13,7 @@ let value: number[] = []; // initial options & values + options.push({ value: -1, label: 'slice size' }); $metrics.forEach((m) => { options.push({ value: m.id, label: m.name }); }); diff --git a/frontend/src/lib/components/chart/chart-page/encoding/chart-encoding/encoding-components/SlicesEncodingDropdown.svelte b/frontend/src/lib/components/chart/chart-page/encoding/chart-encoding/encoding-components/SlicesEncodingDropdown.svelte index c21d01fa..a6930ac4 100644 --- a/frontend/src/lib/components/chart/chart-page/encoding/chart-encoding/encoding-components/SlicesEncodingDropdown.svelte +++ b/frontend/src/lib/components/chart/chart-page/encoding/chart-encoding/encoding-components/SlicesEncodingDropdown.svelte @@ -13,6 +13,7 @@ let options: { value: number; label: string }[] = []; let value = 0; + options.push({ value: -1, label: 'All instances' }); $slices.forEach((s) => { options.push({ value: s.id, label: s.sliceName }); }); diff --git a/frontend/src/lib/components/chart/chart-page/encoding/chart-encoding/encoding-components/SlicesEncodingMultiChoice.svelte b/frontend/src/lib/components/chart/chart-page/encoding/chart-encoding/encoding-components/SlicesEncodingMultiChoice.svelte index b67e28ef..47e19f68 100644 --- a/frontend/src/lib/components/chart/chart-page/encoding/chart-encoding/encoding-components/SlicesEncodingMultiChoice.svelte +++ b/frontend/src/lib/components/chart/chart-page/encoding/chart-encoding/encoding-components/SlicesEncodingMultiChoice.svelte @@ -12,6 +12,7 @@ let options: { value: number; label: string }[] = []; let value: number[] = []; + options.push({ value: -1, label: 'All instances' }); // initial options & values $slices.forEach((s) => { options.push({ value: s.id, label: s.sliceName }); diff --git a/frontend/src/lib/components/chart/chart-page/view-selection/ViewSelection.svelte b/frontend/src/lib/components/chart/chart-page/view-selection/ViewSelection.svelte index f8165e66..928bebc3 100644 --- a/frontend/src/lib/components/chart/chart-page/view-selection/ViewSelection.svelte +++ b/frontend/src/lib/components/chart/chart-page/view-selection/ViewSelection.svelte @@ -1,17 +1,14 @@ diff --git a/frontend/src/lib/components/chart/chart-types/table/Table.svelte b/frontend/src/lib/components/chart/chart-types/table/Table.svelte index ce2e8edc..49547cfc 100644 --- a/frontend/src/lib/components/chart/chart-types/table/Table.svelte +++ b/frontend/src/lib/components/chart/chart-types/table/Table.svelte @@ -72,15 +72,19 @@
-
-

fixed dimension:

-
+
+

fixed dimension:

+
{#if parameters.fixedChannel === SlicesMetricsOrModels.SLICES} sli.id === parameters.slices[0])} /> {:else if parameters.fixedChannel === SlicesMetricsOrModels.MODELS} {parameters.models[0]} {:else if parameters.fixedChannel === SlicesMetricsOrModels.METRICS} - {$metrics.find((met) => met.id === parameters.metrics[0])?.name} + {#if parameters.metrics[0] === -1} + slice size + {:else} + {$metrics.find((met) => met.id === parameters.metrics[0])?.name} + {/if} {/if}
diff --git a/frontend/src/lib/components/chart/chart-types/table/TableRow.svelte b/frontend/src/lib/components/chart/chart-types/table/TableRow.svelte index adddf8c9..64f66564 100644 --- a/frontend/src/lib/components/chart/chart-types/table/TableRow.svelte +++ b/frontend/src/lib/components/chart/chart-types/table/TableRow.svelte @@ -17,7 +17,11 @@ {#if parameters.yChannel === SlicesOrModels.SLICES} - sli.id === row)} /> + {#if row === -1} +

All instances

+ {:else} + sli.id === row)} /> + {/if} {:else} {row} {/if} diff --git a/frontend/src/lib/components/general/Header.svelte b/frontend/src/lib/components/general/Header.svelte index fb01d44a..055ac055 100644 --- a/frontend/src/lib/components/general/Header.svelte +++ b/frontend/src/lib/components/general/Header.svelte @@ -2,7 +2,7 @@ import { goto } from '$app/navigation'; import { page } from '$app/stores'; import ProjectPopup from '$lib/components/popups/ProjectPopup.svelte'; - import { authToken, collapseHeader, project } from '$lib/stores'; + import { authToken, collapseHeader, models, project } from '$lib/stores'; import { getProjectRouteFromURL } from '$lib/util/util'; import type { User } from '$lib/zenoapi'; import { @@ -13,6 +13,7 @@ mdiCog, mdiCompare, mdiCompassOutline, + mdiLogin, mdiLogout } from '@mdi/js'; import HeaderIcon from './HeaderIcon.svelte'; @@ -20,6 +21,8 @@ export let user: User | null; let projectEdit = false; + + $: currentTab = $page.url.href.split('/').pop(); {#if projectEdit && $project && user !== null} @@ -35,19 +38,21 @@
goto(`${getProjectRouteFromURL($page.url)}/explore`)} /> - goto(`${getProjectRouteFromURL($page.url)}/compare`)} - /> + {#if $models.length > 1} + goto(`${getProjectRouteFromURL($page.url)}/compare`)} + /> + {/if} goto(`${getProjectRouteFromURL($page.url)}/chart`)} /> @@ -55,38 +60,42 @@ {/if}
- {#if $page.url.href.includes('project')} + {#if currentTab?.includes('explore') || currentTab?.includes('compare')} collapseHeader.set(!$collapseHeader)} /> {/if}
- {#if $page.url.href.includes('project') && $project && $project.ownerName === user?.name} + {#if (currentTab?.includes('explore') || currentTab?.includes('compare') || currentTab?.includes('chart')) && $project?.ownerName === user?.name} (projectEdit = true)} /> {/if} - goto(`/account`)} - /> -
+ {#if $authToken} authToken.set(undefined)} + pageName={'account'} + tooltipContent={'Manage your account'} + icon={mdiAccount} + on:click={() => goto(`/account`)} /> - +
+ + + {:else} + goto(`/login`)} + /> + {/if}
diff --git a/frontend/src/lib/components/general/HeaderIcon.svelte b/frontend/src/lib/components/general/HeaderIcon.svelte index e2596580..5b8a6201 100644 --- a/frontend/src/lib/components/general/HeaderIcon.svelte +++ b/frontend/src/lib/components/general/HeaderIcon.svelte @@ -1,32 +1,29 @@ - + + + diff --git a/frontend/src/lib/components/instance-views/ComparisonView.svelte b/frontend/src/lib/components/instance-views/ComparisonView.svelte index 2b343bc0..267fb36f 100644 --- a/frontend/src/lib/components/instance-views/ComparisonView.svelte +++ b/frontend/src/lib/components/instance-views/ComparisonView.svelte @@ -1,6 +1,5 @@ -
- {#if table} - - - - - - - - +
+
-
A: {$model}
-
- - {$metric ? $metric.name + ':' : ''} - - - {metricA} - -
-
-
B: {$comparisonModel}
-
- - {$metric ? $metric.name + ':' : ''} - - - {metricB} - -
-
updateSort($model)} class="cursor-pointer p-3 min-w-[150px]"> - - updateSort($comparisonModel)} class="cursor-pointer p-3 min-w-[150px]"> - - updateSort('')} class="cursor-pointer p-3 min-w-[150px]"> - -
+ + + + + + + + {#await tablePromise then table} {#each table as tableContent} @@ -243,9 +229,10 @@ {/if} {#if $model !== undefined && $comparisonModel !== undefined} - - - + + {/each} -
+
A: {$model}
+
+ + {$metric ? $metric.name + ':' : ''} + + + {#await modelAResult then res} + {#if res !== undefined && res.length > 0} + {#if res[0].metric !== undefined && res[0].metric !== null} + {res[0].metric.toFixed(2)} + {/if} + {/if} + {/await} + +
+
+
B: {$comparisonModel}
+
+ + {$metric ? $metric.name + ':' : ''} + + + {#await modelBResult then res} + {#if res !== undefined && res.length > 0} + {#if res[0].metric !== undefined && res[0].metric !== null} + {res[0].metric.toFixed(2)} + {/if} + {/if} + {/await} + +
+
updateSort($model)} class="cursor-pointer p-3 min-w-[150px]"> + + updateSort($comparisonModel)} class="cursor-pointer p-3 min-w-[150px]"> + + updateSort('')} class="cursor-pointer p-3 min-w-[150px]"> + +
{modelValueAndDiff($model, tableContent)}{modelValueAndDiff($comparisonModel, tableContent)}{modelValueAndDiff($model, tableContent)}{modelValueAndDiff($comparisonModel, tableContent)}{Number(tableContent['diff']) ? Number(tableContent['diff']).toFixed(2) : tableContent['diff']}
- {/if} + {:catch e} +

error loading data: {e}

+ {/await} +
@@ -268,9 +257,11 @@ - {start + 1}- - {Math.min(end, modelAResult ? modelAResult[0].size : end)} of - {modelAResult ? modelAResult[0].size : ''} + {start + 1} - + {#await modelAResult then res} + {Math.min(end, res ? res[0].size : end)} of + {res ? res[0].size : ''} + {/await} = new Promise(() => undefined); + let modelAResult: Promise = new Promise(() => undefined); + let modelBResult: Promise = new Promise(() => undefined); + let numberOfInstances = 0; let viewOptions: Record | undefined = undefined; - let modelAResult: GroupMetric[] | undefined = undefined; - let modelBResult: GroupMetric[] | undefined = undefined; $: secureTagIds = $tagIds === undefined ? [] : $tagIds; $: secureSelectionIds = $selectionIds === undefined ? [] : $selectionIds; @@ -39,22 +40,22 @@ // change selected to table if a tag is edited $: selected = $editTag !== undefined ? 'table' : selected; $: if ($model) { - getMetricsForSlicesAndTags( + currentResult = getMetricsForSlicesAndTags( $model ? getMetricKeys($model, $metric, $selectionPredicates) : [], - [...new Set([...secureTagIds, ...secureSelectionIds])], - false - ).then((res) => (currentResult = res)); + [...new Set([...secureTagIds, ...secureSelectionIds])] + ); } - $: getCompareResults($model, $metric, $selectionPredicates).then((r) => (modelAResult = r)); - $: getCompareResults($comparisonModel, $metric, $selectionPredicates).then( - (r) => (modelBResult = r) - ); + $: modelAResult = compare + ? getCompareResults($model, $metric, $selectionPredicates) + : new Promise(() => undefined); + $: modelBResult = compare + ? getCompareResults($comparisonModel, $metric, $selectionPredicates) + : new Promise(() => undefined); onMount(() => { if ($project === undefined || $project.view === '') { selected = 'table'; - return; } }); @@ -91,8 +92,14 @@ const secureTagIds = $tagIds === undefined ? [] : $tagIds; const secureSelectionIds = $selectionIds === undefined ? [] : $selectionIds; const dataIds = [...new Set([...secureTagIds, ...secureSelectionIds])]; - return getMetricsForSlicesAndTags(getMetricKeys(model, metric, predicates), dataIds, true); + return getMetricsForSlicesAndTags(getMetricKeys(model, metric, predicates), dataIds); } + + $: currentResult.then((d) => { + if (d !== undefined && d.length > 0) { + numberOfInstances = d[0].size; + } + });
@@ -107,12 +114,12 @@ {/if} {:else if $editTag !== undefined} - + {:else} {#if selected === 'list'} - + {/if} {#if selected === 'table'} - + {/if} {/if} diff --git a/frontend/src/lib/components/instance-views/ListView.svelte b/frontend/src/lib/components/instance-views/ListView.svelte index 7a8ac77d..ea749db3 100644 --- a/frontend/src/lib/components/instance-views/ListView.svelte +++ b/frontend/src/lib/components/instance-views/ListView.svelte @@ -1,6 +1,5 @@ -{#if table} - {#if $project !== undefined && viewMap[$project.view] !== undefined} -
+
+ {#await tablePromise then table} + {#if $project !== undefined && viewMap[$project.view] !== undefined} {#each table as inst (inst['data_id'])}
{/each} -
- {/if} - - - - - - - {start + 1}- - {Math.min(end, currentResult ? currentResult[0].size : end)} of - {currentResult ? currentResult[0].size : ''} - + {/if} + {:catch e} +

error loading data: {e}

+ {/await} +
+ + + + + + + {start + 1} - + {Math.min(end, numberOfInstances)} of + {numberOfInstances} + - (currentPage = 0)} - disabled={currentPage === 0}>first_page - currentPage--} - disabled={currentPage === 0}>chevron_left - currentPage++} - disabled={currentPage >= lastPage}>chevron_right - (currentPage = lastPage)} - disabled={currentPage >= lastPage}>last_page - -{/if} + (currentPage = 0)} + disabled={currentPage === 0}>first_page + currentPage--} + disabled={currentPage === 0}>chevron_left + currentPage++} + disabled={currentPage >= lastPage}>chevron_right + (currentPage = lastPage)} + disabled={currentPage >= lastPage}>last_page + diff --git a/frontend/src/lib/components/instance-views/TableView.svelte b/frontend/src/lib/components/instance-views/TableView.svelte index 9eb074c7..5c4593a6 100644 --- a/frontend/src/lib/components/instance-views/TableView.svelte +++ b/frontend/src/lib/components/instance-views/TableView.svelte @@ -1,6 +1,5 @@ -{#if table} -
- - - - {#if $editTag !== undefined} - - {/if} - {#if $project !== undefined && viewMap[$project.view] !== undefined} - - {/if} - {#each columnHeader as header} - {#if header.name !== 'data_id'} - +
+
Includedinstance updateSort(header)}> -
- {header.name} - - {#if $sort[0] && $sort[0].name === header.name && $sort[1]} - keyboard_arrow_down - {:else if $sort[0] && $sort[0].name === header.name} - keyboard_arrow_up - {/if} - -
-
+ + + {#if $editTag !== undefined} + + {/if} + {#if $project !== undefined && viewMap[$project.view] !== undefined} + - - + + {/if} + {#each columnHeader as header} + {#if header.name !== 'data_id'} + + {/if} + {/each} + + + + {#await tablePromise then table} {#each table as tableContent (tableContent['data_id'])} {#if $editTag !== undefined} @@ -166,70 +168,80 @@ {/if} {#if $project !== undefined && viewMap[$project.view] !== undefined} {/if} {#each columnHeader as header} {#if header.dataType === MetadataType.CONTINUOUS} - + {:else} - + {/if} {/each} {/each} - -
Included (instanceHidden = !instanceHidden)} + > + instance + {#if instanceHidden} + hidden {/if} - {/each} -
updateSort(header)}> +
+ {header.name} + + {#if $sort[0] && $sort[0].name === header.name && $sort[1]} + keyboard_arrow_down + {:else if $sort[0] && $sort[0].name === header.name} + keyboard_arrow_up + {/if} + +
+
-
- -
+ {#if instanceHidden} +

...

+ {:else} +
+ +
+ {/if}
{parseFloat(`${tableContent[header.id]}`).toFixed(2)} + {parseFloat(`${tableContent[header.id]}`).toFixed(2)} + {tableContent[header.id]} + {tableContent[header.id]} +
-
- - - - - - - {start + 1}- - {Math.min(end, currentResult ? currentResult[0].size : end)} of - {currentResult ? currentResult[0].size : ''} - + {:catch e} +

error loading data: {e}

+ {/await} + + +
+ + + + + + + {start + 1} - + {Math.min(end, numberOfInstances)} of + {numberOfInstances} + - (currentPage = 0)} - disabled={currentPage === 0}>first_page - currentPage--} - disabled={currentPage === 0}>chevron_left - currentPage++} - disabled={currentPage >= lastPage}>chevron_right - (currentPage = lastPage)} - disabled={currentPage >= lastPage}>last_page - -{/if} + (currentPage = 0)} + disabled={currentPage === 0}>first_page + currentPage--} + disabled={currentPage === 0}>chevron_left + currentPage++} + disabled={currentPage >= lastPage}>chevron_right + (currentPage = lastPage)} + disabled={currentPage >= lastPage}>last_page +
diff --git a/frontend/src/lib/components/instance-views/views/views/AudioTranscription.svelte b/frontend/src/lib/components/instance-views/views/views/AudioTranscription.svelte index 51edb04c..5fe4066f 100644 --- a/frontend/src/lib/components/instance-views/views/views/AudioTranscription.svelte +++ b/frontend/src/lib/components/instance-views/views/views/AudioTranscription.svelte @@ -1,16 +1,21 @@
-
+ {#await resolveDataPoint(entry)} + + {:then audioResponse} + {@const audioURL = audioResponse.toString()} -
+ {/await} +

label: {entry['label']} diff --git a/frontend/src/lib/components/instance-views/views/views/TextClassification.svelte b/frontend/src/lib/components/instance-views/views/views/TextClassification.svelte index bf5107a9..5acae39b 100644 --- a/frontend/src/lib/components/instance-views/views/views/TextClassification.svelte +++ b/frontend/src/lib/components/instance-views/views/views/TextClassification.svelte @@ -8,22 +8,26 @@ export let modelColumn: string; -

+
{#await resolveDataPoint(entry)} {:then textData} - {textData} +

+ input: + {textData} +

{/await} -
{#if entry['label'] !== undefined} - label: - +

+ label: {entry['label']} - +

{/if} {#if modelColumn && entry[modelColumn] !== undefined} -
- output: - {entry[modelColumn]} +
+

+ output: + {entry[modelColumn]} +

{/if}
diff --git a/frontend/src/lib/components/metadata/ChipsWrapper.svelte b/frontend/src/lib/components/metadata/ChipsWrapper.svelte index 9f4a587b..38a3ea7a 100644 --- a/frontend/src/lib/components/metadata/ChipsWrapper.svelte +++ b/frontend/src/lib/components/metadata/ChipsWrapper.svelte @@ -1,6 +1,7 @@ {#if !$page.url.href.includes('compare')} -
-
-

Metadata

-
- - - -
- {#if $requestingHistogramCounts} - - {/if} -
- -
{#each $columns.filter((m) => m.columnType === ZenoColumnType.LABEL) as col (col.id)} {@const hist = metadataHistograms.get(col.id)} {#if hist} diff --git a/frontend/src/lib/components/metadata/HistogramsHeader.svelte b/frontend/src/lib/components/metadata/HistogramsHeader.svelte new file mode 100644 index 00000000..9d47b67e --- /dev/null +++ b/frontend/src/lib/components/metadata/HistogramsHeader.svelte @@ -0,0 +1,33 @@ + + +
+
+

Metadata

+
+ + + +
+ {#if $requestingHistogramCounts} + + {/if} +
+ +
diff --git a/frontend/src/lib/components/metadata/MetadataHeader.svelte b/frontend/src/lib/components/metadata/MetadataHeader.svelte index c6641cd9..51037882 100644 --- a/frontend/src/lib/components/metadata/MetadataHeader.svelte +++ b/frontend/src/lib/components/metadata/MetadataHeader.svelte @@ -9,32 +9,13 @@ model, models } from '$lib/stores'; - import type { ZenoColumn } from '$lib/zenoapi'; - import { onMount } from 'svelte'; - let comparisonColumnOptions: ZenoColumn[] = []; - - onMount(() => { - if ($model === undefined && $models.length > 0) { - model.set($models[0]); - } - if ($metric === undefined && $metrics.length > 0) { - metric.set($metrics[0]); - } - comparisonColumnOptions = $columns.filter((c) => c.model === $model); + let comparisonColumnOptions = $columns.filter((c) => c.model === $model); + if (!$comparisonColumn) { comparisonColumn.set(comparisonColumnOptions[0]); - }); - - $: exludeModels = $models.filter((m) => m !== $model); - $: if ($model === undefined || (!$models.includes($model) && $models.length > 0)) { - $model = $models[0]; - } - $: if ( - $comparisonModel === undefined || - (!$models.includes($comparisonModel) && $models.length > 1) - ) { - $model = $models[1]; } + + $: excludeModels = $models.filter((m) => m !== $model);
@@ -53,11 +34,13 @@
{/if} - {#if !$page.url.href.includes('compare') && $metric !== undefined} + {#if !$page.url.href.includes('compare') && $metrics.length > 0 && $metric !== undefined}
Metric
{/if}
-{#if $page.url.href.includes('compare') && $metric !== undefined} +{#if $page.url.href.includes('compare') && $metric !== undefined && $metrics.length > 0}
Metric - {#each comparisonColumnOptions as col} + {#each comparisonColumnOptions as col (col.id)} {/each} diff --git a/frontend/src/lib/components/metadata/MetadataPanel.svelte b/frontend/src/lib/components/metadata/MetadataPanel.svelte index b46cb873..86749b2c 100644 --- a/frontend/src/lib/components/metadata/MetadataPanel.svelte +++ b/frontend/src/lib/components/metadata/MetadataPanel.svelte @@ -1,9 +1,12 @@
- + {#if !compare} + + + {/if}