diff --git a/CHANGELOG.md b/CHANGELOG.md index c6c397b7..8a8ed5a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +## UNRELEASED + +* Revamp data model + * All objects unified in one main table + * Renamed surveys/projects -> Assessments + * Added object library table +* Revamp user experience + * Enter assessment data all on one page + * Manage object library separately from assessment data +* Revamp architecture + * Use HTMX to start implementing HATEOAS architecture + + ## v1.6.1 (2023-12-22) * Spelling fixes diff --git a/compose.dev.yml b/compose.dev.yml index 543f9543..b46c3bd9 100644 --- a/compose.dev.yml +++ b/compose.dev.yml @@ -38,7 +38,7 @@ services: ############################################################### # DEV DANGER ZONE: These settings are INSECURE IN PRODUCTION. # ############################################################### - # Enable the in-browser debugger: + # Enable the in-browser debugger and hot reloading: FLASK_DEBUG: "True" # In dev, we don't need to secure our sessions FLASK_SECRET_KEY: "supersecret" diff --git a/conda-lock.yml b/conda-lock.yml index e939b363..3d5e1fec 100644 --- a/conda-lock.yml +++ b/conda-lock.yml @@ -13,8 +13,8 @@ version: 1 metadata: content_hash: - linux-64: 3c85e729777cea39a5d837f199656760051edb8561c51b36f39a2e8b4e7df2b9 - osx-64: 4d575856ffd08afa420bcb0c0ef5f829441f3f12c72f16da8726ed981a167698 + linux-64: 0bb6ddfa648e8fc40c3e143bf574aa650d425b8723cfb85e30e0aa1654737588 + osx-64: 6ec08693e22debeeef9a823de83fd4f4c264e6c9c59fe83c8eb849fb3e27f4b3 channels: - url: conda-forge used_env_vars: [] @@ -35,407 +35,407 @@ package: sha256: fe51de6107f9edc7aa4f786a70f4a883943bc9d39b3bb7307c04c41410990726 category: main optional: false -- name: ca-certificates - version: 2023.11.17 +- name: _openmp_mutex + version: '4.5' manager: conda platform: linux-64 - dependencies: {} - url: https://conda.anaconda.org/conda-forge/linux-64/ca-certificates-2023.11.17-hbcca054_0.conda + dependencies: + _libgcc_mutex: '0.1' + libgomp: '>=7.5.0' + url: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 hash: - md5: 01ffc8d36f9eba0ce0b3c1955fa780ee - sha256: fb4b9f4b7d885002db0b93e22f44b5b03791ef3d4efdc9d0662185a0faafd6b6 + md5: 73aaf86a425cc6e73fcf236a5a46396d + sha256: fbe2c5e56a653bebb982eda4876a9178aedfc2b545f25d0ce9c4c0b508253d22 category: main optional: false -- name: ld_impl_linux-64 - version: '2.40' +- name: amqp + version: 5.2.0 manager: conda platform: linux-64 - dependencies: {} - url: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.40-h41732ed_0.conda + dependencies: + python: '>=3.6' + vine: '>=5.0.0,<6.0.0' + url: https://conda.anaconda.org/conda-forge/noarch/amqp-5.2.0-pyhd8ed1ab_1.conda hash: - md5: 7aca3059a1729aa76c597603f10b0dd3 - sha256: f6cc89d887555912d6c61b295d398cff9ec982a3417d38025c45d5dd9b9e79cd + md5: 23e198e6df09c2c441640f1bcd1c7822 + sha256: d445abfa5580fc4978eae39dd9268379cc6cffdb630108b38b03f0032899bc63 category: main optional: false -- name: libstdcxx-ng - version: 13.2.0 +- name: amqp + version: 5.2.0 manager: conda - platform: linux-64 - dependencies: {} - url: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-13.2.0-h7e041cc_3.conda + platform: osx-64 + dependencies: + python: '>=3.6' + vine: '>=5.0.0,<6.0.0' + url: https://conda.anaconda.org/conda-forge/noarch/amqp-5.2.0-pyhd8ed1ab_1.conda hash: - md5: 937eaed008f6bf2191c5fe76f87755e9 - sha256: 6c6c49efedcc5709a66f19fb6b26b69c6a5245310fd1d9a901fd5e38aaf7f882 + md5: 23e198e6df09c2c441640f1bcd1c7822 + sha256: d445abfa5580fc4978eae39dd9268379cc6cffdb630108b38b03f0032899bc63 category: main optional: false -- name: python_abi - version: '3.11' +- name: backports.zoneinfo + version: 0.2.1 manager: conda platform: linux-64 - dependencies: {} - url: https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.11-4_cp311.conda + dependencies: + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + url: https://conda.anaconda.org/conda-forge/linux-64/backports.zoneinfo-0.2.1-py311h38be061_8.conda hash: - md5: d786502c97404c94d7d58d258a445a65 - sha256: 0be3ac1bf852d64f553220c7e6457e9c047dfb7412da9d22fbaa67e60858b3cf + md5: 5384590f14dfe6ccd02811236afc9f8e + sha256: 1708c5e6729567f30ccde7761492cb43ee72fa2f7d5065b9102785278718505b category: main optional: false -- name: shellcheck - version: 0.8.0 +- name: backports.zoneinfo + version: 0.2.1 manager: conda - platform: linux-64 - dependencies: {} - url: https://conda.anaconda.org/conda-forge/linux-64/shellcheck-0.8.0-ha770c72_0.tar.bz2 + platform: osx-64 + dependencies: + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + url: https://conda.anaconda.org/conda-forge/osx-64/backports.zoneinfo-0.2.1-py311h6eed73b_8.conda hash: - md5: 0276737172793e3ec3dbc1631840ead7 - sha256: dfeecf516522c4b040ef9e4c6ec9c204d2b09f985f114c6a655522ed6407c27f + md5: 82f37234dbc0254423c109e9e21ce332 + sha256: f6064fc69833fed6d02738d29132bc87a6195098ec74257f53044de306694ff3 category: main optional: false -- name: tzdata - version: 2023c +- name: billiard + version: 4.2.0 manager: conda platform: linux-64 - dependencies: {} - url: https://conda.anaconda.org/conda-forge/noarch/tzdata-2023c-h71feb2d_0.conda + dependencies: + libgcc-ng: '>=12' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + url: https://conda.anaconda.org/conda-forge/linux-64/billiard-4.2.0-py311h459d7ec_0.conda hash: - md5: 939e3e74d8be4dac89ce83b20de2492a - sha256: 0449138224adfa125b220154408419ec37c06b0b49f63c5954724325903ecf55 + md5: d408435aabfb23b364dcca868386cf0b + sha256: fc947afbd0eb61ac2e98195ae26a1b43b89ffb91b71c43cb0db7b52f131bc60c category: main optional: false -- name: libgomp - version: 13.2.0 +- name: billiard + version: 4.2.0 manager: conda - platform: linux-64 + platform: osx-64 dependencies: - _libgcc_mutex: '0.1' - url: https://conda.anaconda.org/conda-forge/linux-64/libgomp-13.2.0-h807b86a_3.conda + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + url: https://conda.anaconda.org/conda-forge/osx-64/billiard-4.2.0-py311he705e18_0.conda hash: - md5: 7124cbb46b13d395bdde68f2d215c989 - sha256: 6ebedee39b6bbbc969715d0d7fa4b381cce67e1139862604ffa393f821c08e81 + md5: fec58d777f1c6d0d680dfbe33c8c959c + sha256: b148e75ee6374f5e9ad1174c66761ad46fa20248ce816cf7cdea7d39af500891 category: main optional: false -- name: _openmp_mutex - version: '4.5' +- name: blinker + version: 1.7.0 manager: conda platform: linux-64 dependencies: - _libgcc_mutex: '0.1' - libgomp: '>=7.5.0' - url: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 + python: '>=3.8' + url: https://conda.anaconda.org/conda-forge/noarch/blinker-1.7.0-pyhd8ed1ab_0.conda hash: - md5: 73aaf86a425cc6e73fcf236a5a46396d - sha256: fbe2c5e56a653bebb982eda4876a9178aedfc2b545f25d0ce9c4c0b508253d22 + md5: 550da20b2c2e38be9cc44bb819fda5d5 + sha256: c8d72c2af4f57898dfd5e4c62ae67f7fea1490a37c8b6855460a170d61591177 category: main optional: false -- name: libgcc-ng - version: 13.2.0 +- name: blinker + version: 1.7.0 manager: conda - platform: linux-64 + platform: osx-64 dependencies: - _libgcc_mutex: '0.1' - _openmp_mutex: '>=4.5' - url: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-13.2.0-h807b86a_3.conda + python: '>=3.8' + url: https://conda.anaconda.org/conda-forge/noarch/blinker-1.7.0-pyhd8ed1ab_0.conda hash: - md5: 23fdf1fef05baeb7eadc2aed5fb0011f - sha256: 5e88f658e07a30ab41b154b42c59f079b168acfa9551a75bdc972099453f4105 + md5: 550da20b2c2e38be9cc44bb819fda5d5 + sha256: c8d72c2af4f57898dfd5e4c62ae67f7fea1490a37c8b6855460a170d61591177 category: main optional: false -- name: bzip2 - version: 1.0.8 +- name: boto3 + version: 1.34.61 manager: conda platform: linux-64 dependencies: - libgcc-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hd590300_5.conda + botocore: '>=1.34.61,<1.35.0' + jmespath: '>=0.7.1,<2.0.0' + python: '>=3.8' + s3transfer: '>=0.10.0,<0.11.0' + url: https://conda.anaconda.org/conda-forge/noarch/boto3-1.34.61-pyhd8ed1ab_1.conda hash: - md5: 69b8b6202a07720f448be700e300ccf4 - sha256: 242c0c324507ee172c0e0dd2045814e746bb303d1eb78870d182ceb0abc726a8 + md5: 0e2e76e883b22b5688f2538f49f415c4 + sha256: a5460fbc566bc50d421f8fd2cd3b4467785eb604d57e30e5271bd6d00e58edad category: main optional: false -- name: keyutils - version: 1.6.1 +- name: boto3 + version: 1.34.61 manager: conda - platform: linux-64 + platform: osx-64 dependencies: - libgcc-ng: '>=10.3.0' - url: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.1-h166bdaf_0.tar.bz2 + python: '>=3.8' + jmespath: '>=0.7.1,<2.0.0' + s3transfer: '>=0.10.0,<0.11.0' + botocore: '>=1.34.61,<1.35.0' + url: https://conda.anaconda.org/conda-forge/noarch/boto3-1.34.61-pyhd8ed1ab_1.conda hash: - md5: 30186d27e2c9fa62b45fb1476b7200e3 - sha256: 150c05a6e538610ca7c43beb3a40d65c90537497a4f6a5f4d15ec0451b6f5ebb + md5: 0e2e76e883b22b5688f2538f49f415c4 + sha256: a5460fbc566bc50d421f8fd2cd3b4467785eb604d57e30e5271bd6d00e58edad category: main optional: false -- name: libexpat - version: 2.5.0 +- name: botocore + version: 1.34.61 manager: conda platform: linux-64 dependencies: - libgcc-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.5.0-hcb278e6_1.conda + jmespath: '>=0.7.1,<2.0.0' + python: '>=3.10' + python-dateutil: '>=2.1,<3.0.0' + urllib3: '>=1.25.4,<2.1' + url: https://conda.anaconda.org/conda-forge/noarch/botocore-1.34.61-pyge310_1234567_0.conda hash: - md5: 6305a3dd2752c76335295da4e581f2fd - sha256: 74c98a563777ae2ad71f1f74d458a8ab043cee4a513467c159ccf159d0e461f3 + md5: 9049a6fcebd20b5ec96bf9305b15f20f + sha256: a9387776de19e09f1235a85818ef5eb5df118e5ac49cd3f92f462091f84ae6ac category: main optional: false -- name: libffi - version: 3.4.2 +- name: botocore + version: 1.34.61 manager: conda - platform: linux-64 + platform: osx-64 dependencies: - libgcc-ng: '>=9.4.0' - url: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.2-h7f98852_5.tar.bz2 + python-dateutil: '>=2.1,<3.0.0' + jmespath: '>=0.7.1,<2.0.0' + python: '>=3.10' + urllib3: '>=1.25.4,<2.1' + url: https://conda.anaconda.org/conda-forge/noarch/botocore-1.34.61-pyge310_1234567_0.conda hash: - md5: d645c6d2ac96843a2bfaccd2d62b3ac3 - sha256: ab6e9856c21709b7b517e940ae7028ae0737546122f83c2aa5d692860c3b149e + md5: 9049a6fcebd20b5ec96bf9305b15f20f + sha256: a9387776de19e09f1235a85818ef5eb5df118e5ac49cd3f92f462091f84ae6ac category: main optional: false -- name: libnsl - version: 2.0.1 +- name: brotli-python + version: 1.1.0 manager: conda platform: linux-64 dependencies: libgcc-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hd590300_0.conda + libstdcxx-ng: '>=12' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + url: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.1.0-py311hb755f60_1.conda hash: - md5: 30fd6e37fe21f86f4bd26d6ee73eeec7 - sha256: 26d77a3bb4dceeedc2a41bd688564fe71bf2d149fdcf117049970bc02ff1add6 + md5: cce9e7c3f1c307f2a5fb08a2922d6164 + sha256: 559093679e9fdb6061b7b80ca0f9a31fe6ffc213f1dae65bc5c82e2cd1a94107 category: main optional: false -- name: libuuid - version: 2.38.1 +- name: brotli-python + version: 1.1.0 manager: conda - platform: linux-64 + platform: osx-64 dependencies: - libgcc-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda + libcxx: '>=15.0.7' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + url: https://conda.anaconda.org/conda-forge/osx-64/brotli-python-1.1.0-py311hdf8f085_1.conda hash: - md5: 40b61aab5c7ba9ff276c41cfffe6b80b - sha256: 787eb542f055a2b3de553614b25f09eefb0a0931b0c87dbcce6efdfd92f04f18 + md5: 546fdccabb90492fbaf2da4ffb78f352 + sha256: 0f5e0a7de58006f349220365e32db521a1fe494c37ee455e5ecf05b8fe567dcc category: main optional: false -- name: libzlib - version: 1.2.13 +- name: bump-my-version + version: 0.9.3 manager: conda platform: linux-64 dependencies: - libgcc-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.2.13-hd590300_5.conda + click: '' + pydantic: <2.0.0 + python: '>=3.8' + rich: '' + rich-click: '' + tomlkit: '' + url: https://conda.anaconda.org/conda-forge/noarch/bump-my-version-0.9.3-pyhd8ed1ab_0.conda hash: - md5: f36c115f1ee199da648e0597ec2047ad - sha256: 370c7c5893b737596fd6ca0d9190c9715d89d888b8c88537ae1ef168c25e82e4 + md5: 24f0c263af3783651f77b021a6ad97a8 + sha256: b5530371b54677cb72d93ae091052327b2c809a6066d85366af4f3f0728888eb category: main optional: false -- name: ncurses - version: '6.4' +- name: bump-my-version + version: 0.9.3 manager: conda - platform: linux-64 + platform: osx-64 dependencies: - libgcc-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.4-h59595ed_2.conda + click: '' + rich: '' + tomlkit: '' + rich-click: '' + python: '>=3.8' + pydantic: <2.0.0 + url: https://conda.anaconda.org/conda-forge/noarch/bump-my-version-0.9.3-pyhd8ed1ab_0.conda hash: - md5: 7dbaa197d7ba6032caf7ae7f32c1efa0 - sha256: 91cc03f14caf96243cead96c76fe91ab5925a695d892e83285461fb927dece5e + md5: 24f0c263af3783651f77b021a6ad97a8 + sha256: b5530371b54677cb72d93ae091052327b2c809a6066d85366af4f3f0728888eb category: main optional: false -- name: openssl - version: 3.2.0 +- name: bzip2 + version: 1.0.8 manager: conda platform: linux-64 dependencies: - ca-certificates: '' libgcc-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.2.0-hd590300_1.conda + url: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hd590300_5.conda hash: - md5: 603827b39ea2b835268adb8c821b8570 - sha256: 80efc6f429bd8e622d999652e5cba2ca56fcdb9c16a439d2ce9b4313116e4a87 + md5: 69b8b6202a07720f448be700e300ccf4 + sha256: 242c0c324507ee172c0e0dd2045814e746bb303d1eb78870d182ceb0abc726a8 category: main optional: false -- name: xz - version: 5.2.6 +- name: bzip2 + version: 1.0.8 manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/xz-5.2.6-h166bdaf_0.tar.bz2 + platform: osx-64 + dependencies: {} + url: https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-h10d778d_5.conda hash: - md5: 2161070d867d1b1204ea749c8eec4ef0 - sha256: 03a6d28ded42af8a347345f82f3eebdd6807a08526d47899a42d62d319609162 + md5: 6097a6ca9ada32699b5fc4312dd6ef18 + sha256: 61fb2b488928a54d9472113e1280b468a309561caa54f33825a3593da390b242 category: main optional: false -- name: yaml - version: 0.2.5 +- name: ca-certificates + version: 2024.2.2 manager: conda platform: linux-64 - dependencies: - libgcc-ng: '>=9.4.0' - url: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h7f98852_2.tar.bz2 + dependencies: {} + url: https://conda.anaconda.org/conda-forge/linux-64/ca-certificates-2024.2.2-hbcca054_0.conda hash: - md5: 4cb3ad778ec2d5a7acbdf254eb1c42ae - sha256: a4e34c710eeb26945bdbdaba82d3d74f60a78f54a874ec10d373811a5d217535 + md5: 2f4327a1cbe7f022401b236e915a5fef + sha256: 91d81bfecdbb142c15066df70cc952590ae8991670198f92c66b62019b251aeb category: main optional: false -- name: libedit - version: 3.1.20191231 +- name: ca-certificates + version: 2024.2.2 manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=7.5.0' - ncurses: '>=6.2,<7.0.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20191231-he28a2e2_2.tar.bz2 + platform: osx-64 + dependencies: {} + url: https://conda.anaconda.org/conda-forge/osx-64/ca-certificates-2024.2.2-h8857fd0_0.conda hash: - md5: 4d331e44109e3f0e19b4cb8f9b82f3e1 - sha256: a57d37c236d8f7c886e01656f4949d9dcca131d2a0728609c6f7fa338b65f1cf + md5: f2eacee8c33c43692f1ccfd33d0f50b1 + sha256: 54a794aedbb4796afeabdf54287b06b1d27f7b13b3814520925f4c2c80f58ca9 category: main optional: false -- name: libsqlite - version: 3.44.2 +- name: celery + version: 5.3.6 manager: conda platform: linux-64 dependencies: - libgcc-ng: '>=12' - libzlib: '>=1.2.13,<1.3.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.44.2-h2797004_0.conda + backports.zoneinfo: '>=0.2.1' + billiard: '>=4.2.0,<5.0' + click: '>=8.1.2,<9.0' + click-didyoumean: '>=0.3.0' + click-plugins: '>=1.1.1' + click-repl: '>=0.2.0' + importlib-metadata: '>=3.6' + kombu: '>=5.3.4,<6.0' + python: '>=3.8' + python-dateutil: '>=2.8.2' + python-tzdata: '>=2022.7' + vine: '>=5.1.0,<6.0' + url: https://conda.anaconda.org/conda-forge/noarch/celery-5.3.6-pyhd8ed1ab_0.conda hash: - md5: 3b6a9f225c3dbe0d24f4fedd4625c5bf - sha256: ee2c4d724a3ed60d5b458864d66122fb84c6ce1df62f735f90d8db17b66cd88a + md5: f3949418e3578d5cbea5df5d32c5b6ff + sha256: bafedd098b75f9baf88167e9bbbbd687fbbc0b06b1a056bdb3bf16221e329def category: main optional: false -- name: readline - version: '8.2' +- name: celery + version: 5.3.6 manager: conda - platform: linux-64 + platform: osx-64 dependencies: - libgcc-ng: '>=12' - ncurses: '>=6.3,<7.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8228510_1.conda + python: '>=3.8' + backports.zoneinfo: '>=0.2.1' + python-dateutil: '>=2.8.2' + importlib-metadata: '>=3.6' + click-plugins: '>=1.1.1' + click-repl: '>=0.2.0' + click: '>=8.1.2,<9.0' + click-didyoumean: '>=0.3.0' + python-tzdata: '>=2022.7' + billiard: '>=4.2.0,<5.0' + vine: '>=5.1.0,<6.0' + kombu: '>=5.3.4,<6.0' + url: https://conda.anaconda.org/conda-forge/noarch/celery-5.3.6-pyhd8ed1ab_0.conda hash: - md5: 47d31b792659ce70f470b5c82fdfb7a4 - sha256: 5435cf39d039387fbdc977b0a762357ea909a7694d9528ab40f005e9208744d7 + md5: f3949418e3578d5cbea5df5d32c5b6ff + sha256: bafedd098b75f9baf88167e9bbbbd687fbbc0b06b1a056bdb3bf16221e329def category: main optional: false -- name: tk - version: 8.6.13 +- name: certifi + version: 2024.2.2 manager: conda platform: linux-64 dependencies: - libgcc-ng: '>=12' - libzlib: '>=1.2.13,<1.3.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h4845f30_101.conda + python: '>=3.7' + url: https://conda.anaconda.org/conda-forge/noarch/certifi-2024.2.2-pyhd8ed1ab_0.conda hash: - md5: d453b98d9c83e71da0741bb0ff4d76bc - sha256: e0569c9caa68bf476bead1bed3d79650bb080b532c64a4af7d8ca286c08dea4e + md5: 0876280e409658fc6f9e75d035960333 + sha256: f1faca020f988696e6b6ee47c82524c7806380b37cfdd1def32f92c326caca54 category: main optional: false -- name: krb5 - version: 1.21.2 +- name: certifi + version: 2024.2.2 manager: conda - platform: linux-64 + platform: osx-64 dependencies: - keyutils: '>=1.6.1,<2.0a0' - libedit: '>=3.1.20191231,<4.0a0' - libgcc-ng: '>=12' - libstdcxx-ng: '>=12' - openssl: '>=3.1.2,<4.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.2-h659d440_0.conda + python: '>=3.7' + url: https://conda.anaconda.org/conda-forge/noarch/certifi-2024.2.2-pyhd8ed1ab_0.conda hash: - md5: cd95826dbd331ed1be26bdf401432844 - sha256: 259bfaae731989b252b7d2228c1330ef91b641c9d68ff87dae02cbae682cb3e4 + md5: 0876280e409658fc6f9e75d035960333 + sha256: f1faca020f988696e6b6ee47c82524c7806380b37cfdd1def32f92c326caca54 category: main optional: false -- name: python - version: 3.11.7 +- name: cffi + version: 1.16.0 manager: conda platform: linux-64 dependencies: - bzip2: '>=1.0.8,<2.0a0' - ld_impl_linux-64: '>=2.36.1' - libexpat: '>=2.5.0,<3.0a0' libffi: '>=3.4,<4.0a0' libgcc-ng: '>=12' - libnsl: '>=2.0.1,<2.1.0a0' - libsqlite: '>=3.44.2,<4.0a0' - libuuid: '>=2.38.1,<3.0a0' - libzlib: '>=1.2.13,<1.3.0a0' - ncurses: '>=6.4,<7.0a0' - openssl: '>=3.2.0,<4.0a0' - readline: '>=8.2,<9.0a0' - tk: '>=8.6.13,<8.7.0a0' - tzdata: '' - xz: '>=5.2.6,<6.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/python-3.11.7-hab00c5b_0_cpython.conda - hash: - md5: bf281a975393266ab95734a8cfd532ec - sha256: 3b758231658ab3db25e5f6e3e589419ac0d13f63e2927e75ee34f4b9dd31bc1f - category: main - optional: false -- name: backports.zoneinfo - version: 0.2.1 - manager: conda - platform: linux-64 - dependencies: - python: '>=3.11,<3.12.0a0' - python_abi: 3.11.* - url: https://conda.anaconda.org/conda-forge/linux-64/backports.zoneinfo-0.2.1-py311h38be061_8.conda - hash: - md5: 5384590f14dfe6ccd02811236afc9f8e - sha256: 1708c5e6729567f30ccde7761492cb43ee72fa2f7d5065b9102785278718505b - category: main - optional: false -- name: billiard - version: 4.1.0 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' + pycparser: '' python: '>=3.11,<3.12.0a0' python_abi: 3.11.* - url: https://conda.anaconda.org/conda-forge/linux-64/billiard-4.1.0-py311h459d7ec_1.conda - hash: - md5: f4042c39668430f920ab9f5b42ca062e - sha256: b3a3bd417f6e68134d59c7a64508b3d73d7a78894625ed14a9dc827c640beb2e - category: main - optional: false -- name: blinker - version: 1.7.0 - manager: conda - platform: linux-64 - dependencies: - python: '>=3.8' - url: https://conda.anaconda.org/conda-forge/noarch/blinker-1.7.0-pyhd8ed1ab_0.conda + url: https://conda.anaconda.org/conda-forge/linux-64/cffi-1.16.0-py311hb3a22ac_0.conda hash: - md5: 550da20b2c2e38be9cc44bb819fda5d5 - sha256: c8d72c2af4f57898dfd5e4c62ae67f7fea1490a37c8b6855460a170d61591177 + md5: b3469563ac5e808b0cd92810d0697043 + sha256: b71c94528ca0c35133da4b7ef69b51a0b55eeee570376057f3d2ad60c3ab1444 category: main optional: false -- name: brotli-python - version: 1.1.0 +- name: cffi + version: 1.16.0 manager: conda - platform: linux-64 + platform: osx-64 dependencies: - libgcc-ng: '>=12' - libstdcxx-ng: '>=12' + libffi: '>=3.4,<4.0a0' + pycparser: '' python: '>=3.11,<3.12.0a0' python_abi: 3.11.* - url: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.1.0-py311hb755f60_1.conda + url: https://conda.anaconda.org/conda-forge/osx-64/cffi-1.16.0-py311hc0b63fd_0.conda hash: - md5: cce9e7c3f1c307f2a5fb08a2922d6164 - sha256: 559093679e9fdb6061b7b80ca0f9a31fe6ffc213f1dae65bc5c82e2cd1a94107 + md5: 15d07b82223cac96af629e5e747ba27a + sha256: 1f13a5fa7f310fdbd27f5eddceb9e62cfb10012c58a58c923dd6f51fa979748a category: main optional: false -- name: certifi - version: 2023.11.17 +- name: cfgv + version: 3.3.1 manager: conda platform: linux-64 dependencies: - python: '>=3.7' - url: https://conda.anaconda.org/conda-forge/noarch/certifi-2023.11.17-pyhd8ed1ab_0.conda + python: '>=3.6.1' + url: https://conda.anaconda.org/conda-forge/noarch/cfgv-3.3.1-pyhd8ed1ab_0.tar.bz2 hash: - md5: 2011bcf45376341dd1d690263fdbc789 - sha256: afa22b77128a812cb57bc707c297d926561bd225a3d9dd74205d87a3b2d14a96 + md5: ebb5f5f7dc4f1a3780ef7ea7738db08c + sha256: fbc03537a27ef756162c49b1d0608bf7ab12fa5e38ceb8563d6f4859e835ac5c category: main optional: false - name: cfgv version: 3.3.1 manager: conda - platform: linux-64 + platform: osx-64 dependencies: python: '>=3.6.1' url: https://conda.anaconda.org/conda-forge/noarch/cfgv-3.3.1-pyhd8ed1ab_0.tar.bz2 @@ -456,6 +456,18 @@ package: sha256: 20cae47d31fdd58d99c4d2e65fbdcefa0b0de0c84e455ba9d6356a4bdbc4b5b9 category: main optional: false +- name: charset-normalizer + version: 3.3.2 + manager: conda + platform: osx-64 + dependencies: + python: '>=3.7' + url: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.3.2-pyhd8ed1ab_0.conda + hash: + md5: 7f4a9e3fcff3f6356ae99244a014da6a + sha256: 20cae47d31fdd58d99c4d2e65fbdcefa0b0de0c84e455ba9d6356a4bdbc4b5b9 + category: main + optional: false - name: click version: 8.1.7 manager: conda @@ -469,1352 +481,1166 @@ package: sha256: f0016cbab6ac4138a429e28dbcb904a90305b34b3fe41a9b89d697c90401caec category: main optional: false -- name: colorama - version: 0.4.6 +- name: click + version: 8.1.7 manager: conda - platform: linux-64 + platform: osx-64 dependencies: - python: '>=3.7' - url: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_0.tar.bz2 + __unix: '' + python: '>=3.8' + url: https://conda.anaconda.org/conda-forge/noarch/click-8.1.7-unix_pyh707e725_0.conda hash: - md5: 3faab06a954c2a04039983f2c4a50d99 - sha256: 2c1b2e9755ce3102bca8d69e8f26e4f087ece73f50418186aee7c74bef8e1698 + md5: f3ad426304898027fc619827ff428eca + sha256: f0016cbab6ac4138a429e28dbcb904a90305b34b3fe41a9b89d697c90401caec category: main optional: false -- name: distlib - version: 0.3.8 +- name: click-didyoumean + version: 0.3.0 manager: conda platform: linux-64 dependencies: - python: 2.7|>=3.6 - url: https://conda.anaconda.org/conda-forge/noarch/distlib-0.3.8-pyhd8ed1ab_0.conda + click: '>=7' + python: '>=3.6.2,<4.0.0' + url: https://conda.anaconda.org/conda-forge/noarch/click-didyoumean-0.3.0-pyhd8ed1ab_0.tar.bz2 hash: - md5: db16c66b759a64dc5183d69cc3745a52 - sha256: 3ff11acdd5cc2f80227682966916e878e45ced94f59c402efb94911a5774e84e + md5: a49aa7bc1a82485f6a0a8849dcfefcad + sha256: e1af96905976a83f5545b0914830e272415e2e66428165fdf178e73dc4bc9cff category: main optional: false -- name: exceptiongroup - version: 1.2.0 +- name: click-didyoumean + version: 0.3.0 manager: conda - platform: linux-64 + platform: osx-64 dependencies: - python: '>=3.7' - url: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.0-pyhd8ed1ab_0.conda + click: '>=7' + python: '>=3.6.2,<4.0.0' + url: https://conda.anaconda.org/conda-forge/noarch/click-didyoumean-0.3.0-pyhd8ed1ab_0.tar.bz2 hash: - md5: f6c211fee3c98229652b60a9a42ef363 - sha256: cf83dcaf9006015c8ccab3fc6770f478464a66a8769e1763ca5d7dff09d11d08 + md5: a49aa7bc1a82485f6a0a8849dcfefcad + sha256: e1af96905976a83f5545b0914830e272415e2e66428165fdf178e73dc4bc9cff category: main optional: false -- name: filelock - version: 3.13.1 +- name: click-plugins + version: 1.1.1 manager: conda platform: linux-64 dependencies: - python: '>=3.7' - url: https://conda.anaconda.org/conda-forge/noarch/filelock-3.13.1-pyhd8ed1ab_0.conda + click: '>=3.0' + python: '' + url: https://conda.anaconda.org/conda-forge/noarch/click-plugins-1.1.1-py_0.tar.bz2 hash: - md5: 0c1729b74a8152fde6a38ba0a2ab9f45 - sha256: 4d742d91412d1f163e5399d2b50c5d479694ebcd309127abb549ca3977f89d2b + md5: 4fd2c6b53934bd7d96d1f3fdaf99b79f + sha256: ddef6e559dde6673ee504b0e29dd814d36e22b6b9b1f519fa856ee268905bf92 category: main optional: false -- name: greenlet - version: 3.0.2 +- name: click-plugins + version: 1.1.1 manager: conda - platform: linux-64 + platform: osx-64 dependencies: - libgcc-ng: '>=12' - libstdcxx-ng: '>=12' - python: '>=3.11,<3.12.0a0' - python_abi: 3.11.* - url: https://conda.anaconda.org/conda-forge/linux-64/greenlet-3.0.2-py311hb755f60_0.conda + python: '' + click: '>=3.0' + url: https://conda.anaconda.org/conda-forge/noarch/click-plugins-1.1.1-py_0.tar.bz2 hash: - md5: 59bf81c0e43e96ff1ebb5140ccc135b5 - sha256: e11f4bf977c21d3c70bb772c57bb800319f1353a8f0bd947f7561e28eae8d808 + md5: 4fd2c6b53934bd7d96d1f3fdaf99b79f + sha256: ddef6e559dde6673ee504b0e29dd814d36e22b6b9b1f519fa856ee268905bf92 category: main optional: false -- name: idna - version: '3.6' +- name: click-repl + version: 0.3.0 manager: conda platform: linux-64 dependencies: + click: '' + prompt_toolkit: '' python: '>=3.6' - url: https://conda.anaconda.org/conda-forge/noarch/idna-3.6-pyhd8ed1ab_0.conda + six: '' + url: https://conda.anaconda.org/conda-forge/noarch/click-repl-0.3.0-pyhd8ed1ab_0.conda hash: - md5: 1a76f09108576397c41c0b0c5bd84134 - sha256: 6ee4c986d69ce61e60a20b2459b6f2027baeba153f0a64995fd3cb47c2cc7e07 + md5: 27eb8f68250666c1a19d1b6ec9d12c4e + sha256: 69c16e0b89e1fb4fc444af4798ef1222b3075d074cbcbe70b9af793668200a14 category: main optional: false -- name: iniconfig - version: 2.0.0 +- name: click-repl + version: 0.3.0 manager: conda - platform: linux-64 + platform: osx-64 dependencies: - python: '>=3.7' - url: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_0.conda + six: '' + click: '' + prompt_toolkit: '' + python: '>=3.6' + url: https://conda.anaconda.org/conda-forge/noarch/click-repl-0.3.0-pyhd8ed1ab_0.conda hash: - md5: f800d2da156d08e289b14e87e43c1ae5 - sha256: 38740c939b668b36a50ef455b077e8015b8c9cf89860d421b3fff86048f49666 + md5: 27eb8f68250666c1a19d1b6ec9d12c4e + sha256: 69c16e0b89e1fb4fc444af4798ef1222b3075d074cbcbe70b9af793668200a14 category: main optional: false -- name: invoke - version: 2.0.0 +- name: colorama + version: 0.4.6 manager: conda platform: linux-64 dependencies: - python: '>=3.6' - url: https://conda.anaconda.org/conda-forge/noarch/invoke-2.0.0-pyhd8ed1ab_0.conda + python: '>=3.7' + url: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_0.tar.bz2 hash: - md5: c282e4567f7b800a855f7092b3c44a5a - sha256: 637e51d8df0159bae9d67f840a24a5fc7973d48fac09418b01e2fb6f3c03224b + md5: 3faab06a954c2a04039983f2c4a50d99 + sha256: 2c1b2e9755ce3102bca8d69e8f26e4f087ece73f50418186aee7c74bef8e1698 category: main optional: false -- name: itsdangerous - version: 2.1.2 +- name: colorama + version: 0.4.6 manager: conda - platform: linux-64 + platform: osx-64 dependencies: python: '>=3.7' - url: https://conda.anaconda.org/conda-forge/noarch/itsdangerous-2.1.2-pyhd8ed1ab_0.tar.bz2 + url: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_0.tar.bz2 hash: - md5: 3c3de74912f11d2b590184f03c7cd09b - sha256: 31e3492686b4e92b53db9b48bc0eb03873b1caaf28629fee7d2d47627a2c56d3 + md5: 3faab06a954c2a04039983f2c4a50d99 + sha256: 2c1b2e9755ce3102bca8d69e8f26e4f087ece73f50418186aee7c74bef8e1698 category: main optional: false -- name: jmespath - version: 1.0.1 +- name: cryptography + version: 42.0.5 manager: conda platform: linux-64 dependencies: - python: '>=3.7' - url: https://conda.anaconda.org/conda-forge/noarch/jmespath-1.0.1-pyhd8ed1ab_0.tar.bz2 - hash: - md5: 2cfa3e1cf3fb51bb9b17acc5b5e9ea11 - sha256: 95ac5f9ee95fd4e34dc051746fc86016d3d4f6abefed113e2ede049d59ec2991 - category: main - optional: false -- name: libpq - version: '16.1' - manager: conda - platform: linux-64 - dependencies: - krb5: '>=1.21.2,<1.22.0a0' + cffi: '>=1.12' libgcc-ng: '>=12' - openssl: '>=3.2.0,<4.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/libpq-16.1-h33b98f1_7.conda + openssl: '>=3.2.1,<4.0a0' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + url: https://conda.anaconda.org/conda-forge/linux-64/cryptography-42.0.5-py311h63ff55d_0.conda hash: - md5: 675317e46167caea24542d85c72f19a3 - sha256: 833fd96338dffc6784fb5f79ab805fa5a4c2cabf5c08c4f1d5caf4e290e39c28 + md5: 76909c8c7b915f0af4f35e80da5f9a87 + sha256: d3531a63f2bf9e234a8ebbbcef3dffc0721c8320166e3b86c05e05aef8c02480 category: main optional: false -- name: loguru - version: 0.6.0 +- name: cryptography + version: 42.0.5 manager: conda - platform: linux-64 + platform: osx-64 dependencies: + __osx: '>=10.12' + cffi: '>=1.12' + openssl: '>=3.2.1,<4.0a0' python: '>=3.11,<3.12.0a0' python_abi: 3.11.* - url: https://conda.anaconda.org/conda-forge/linux-64/loguru-0.6.0-py311h38be061_2.tar.bz2 + url: https://conda.anaconda.org/conda-forge/osx-64/cryptography-42.0.5-py311hda9c3b6_0.conda hash: - md5: 8495a2a957bbeacad2c25f277f407f24 - sha256: 202d08801360bd4da179428107cb12ed31816d42acf5d5df179fb2aa55014a1a + md5: 493a350a1d4d8099eb16943489a2c664 + sha256: 28fb0c60e30a3236477c6476683f5f167036e30b829881895e0b98d28337f6b7 category: main optional: false -- name: markupsafe - version: 2.1.3 +- name: distlib + version: 0.3.8 manager: conda platform: linux-64 dependencies: - libgcc-ng: '>=12' - python: '>=3.11,<3.12.0a0' - python_abi: 3.11.* - url: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-2.1.3-py311h459d7ec_1.conda + python: 2.7|>=3.6 + url: https://conda.anaconda.org/conda-forge/noarch/distlib-0.3.8-pyhd8ed1ab_0.conda hash: - md5: 71120b5155a0c500826cf81536721a15 - sha256: e1a9930f35e39bf65bc293e24160b83ebf9f800f02749f65358e1c04882ee6b0 + md5: db16c66b759a64dc5183d69cc3745a52 + sha256: 3ff11acdd5cc2f80227682966916e878e45ced94f59c402efb94911a5774e84e category: main optional: false -- name: mdurl - version: 0.1.0 +- name: distlib + version: 0.3.8 manager: conda - platform: linux-64 + platform: osx-64 dependencies: - python: '>=3.6' - url: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.0-pyhd8ed1ab_0.tar.bz2 + python: 2.7|>=3.6 + url: https://conda.anaconda.org/conda-forge/noarch/distlib-0.3.8-pyhd8ed1ab_0.conda hash: - md5: f8dab71fdc13b1bf29a01248b156d268 - sha256: c678b9194e025b1fb665bec30ee20aab93399203583875b1dcc0a3b52a8f5523 + md5: db16c66b759a64dc5183d69cc3745a52 + sha256: 3ff11acdd5cc2f80227682966916e878e45ced94f59c402efb94911a5774e84e category: main optional: false -- name: mypy_extensions - version: 1.0.0 +- name: exceptiongroup + version: 1.2.0 manager: conda platform: linux-64 dependencies: - python: '>=3.5' - url: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.0.0-pyha770c72_0.conda + python: '>=3.7' + url: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.0-pyhd8ed1ab_2.conda hash: - md5: 4eccaeba205f0aed9ac3a9ea58568ca3 - sha256: f240217476e148e825420c6bc3a0c0efb08c0718b7042fae960400c02af858a3 + md5: 8d652ea2ee8eaee02ed8dc820bc794aa + sha256: a6ae416383bda0e3ed14eaa187c653e22bec94ff2aa3b56970cdf0032761e80d category: main optional: false -- name: packaging - version: '23.2' +- name: exceptiongroup + version: 1.2.0 manager: conda - platform: linux-64 + platform: osx-64 dependencies: python: '>=3.7' - url: https://conda.anaconda.org/conda-forge/noarch/packaging-23.2-pyhd8ed1ab_0.conda + url: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.0-pyhd8ed1ab_2.conda hash: - md5: 79002079284aa895f883c6b7f3f88fd6 - sha256: 69b3ace6cca2dab9047b2c24926077d81d236bef45329d264b394001e3c3e52f + md5: 8d652ea2ee8eaee02ed8dc820bc794aa + sha256: a6ae416383bda0e3ed14eaa187c653e22bec94ff2aa3b56970cdf0032761e80d category: main optional: false -- name: platformdirs - version: 4.1.0 +- name: filelock + version: 3.13.1 manager: conda platform: linux-64 dependencies: - python: '>=3.8' - url: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.1.0-pyhd8ed1ab_0.conda + python: '>=3.7' + url: https://conda.anaconda.org/conda-forge/noarch/filelock-3.13.1-pyhd8ed1ab_0.conda hash: - md5: 45a5065664da0d1dfa8f8cd2eaf05ab9 - sha256: 9e4ff17ce802159ed31344eb913eaa877688226765b77947b102b42255a53853 + md5: 0c1729b74a8152fde6a38ba0a2ab9f45 + sha256: 4d742d91412d1f163e5399d2b50c5d479694ebcd309127abb549ca3977f89d2b category: main optional: false -- name: pluggy - version: 1.3.0 +- name: filelock + version: 3.13.1 manager: conda - platform: linux-64 + platform: osx-64 dependencies: - python: '>=3.8' - url: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.3.0-pyhd8ed1ab_0.conda + python: '>=3.7' + url: https://conda.anaconda.org/conda-forge/noarch/filelock-3.13.1-pyhd8ed1ab_0.conda hash: - md5: 2390bd10bed1f3fdc7a537fb5a447d8d - sha256: 7bf2ad9d747e71f1e93d0863c2c8061dd0f2fe1e582f28d292abfb40264a2eb5 + md5: 0c1729b74a8152fde6a38ba0a2ab9f45 + sha256: 4d742d91412d1f163e5399d2b50c5d479694ebcd309127abb549ca3977f89d2b category: main optional: false -- name: psutil - version: 5.9.7 +- name: flask + version: 2.2.5 manager: conda platform: linux-64 dependencies: - libgcc-ng: '>=12' - python: '>=3.11,<3.12.0a0' - python_abi: 3.11.* - url: https://conda.anaconda.org/conda-forge/linux-64/psutil-5.9.7-py311h459d7ec_0.conda + celery: '>=5.2.7' + click: '>=8.0' + importlib-metadata: '>=3.6.0' + itsdangerous: '>=2.0' + jinja2: '>=3.0' + python: '>=3.7' + werkzeug: '>=2.2.2' + url: https://conda.anaconda.org/conda-forge/noarch/flask-2.2.5-pyhd8ed1ab_0.conda hash: - md5: b4f2d78860bf9c8887b528c10995b427 - sha256: a2f08e5bcca5e067c452708d5ce6389c1012c7b6139ffe26b3386674f036eb27 + md5: 66c558a28b9b47c1e2aa8e8cb80b9037 + sha256: 9356ae47278788439770eb9d05bfeb72c39a53a8b10a0e534b612f99b9c18e9b category: main optional: false -- name: pycparser - version: '2.21' +- name: flask + version: 2.2.5 manager: conda - platform: linux-64 + platform: osx-64 dependencies: - python: 2.7.*|>=3.4 - url: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.21-pyhd8ed1ab_0.tar.bz2 + python: '>=3.7' + jinja2: '>=3.0' + click: '>=8.0' + importlib-metadata: '>=3.6.0' + itsdangerous: '>=2.0' + werkzeug: '>=2.2.2' + celery: '>=5.2.7' + url: https://conda.anaconda.org/conda-forge/noarch/flask-2.2.5-pyhd8ed1ab_0.conda hash: - md5: 076becd9e05608f8dc72757d5f3a91ff - sha256: 74c63fd03f1f1ea2b54e8bc529fd1a600aaafb24027b738d0db87909ee3a33dc + md5: 66c558a28b9b47c1e2aa8e8cb80b9037 + sha256: 9356ae47278788439770eb9d05bfeb72c39a53a8b10a0e534b612f99b9c18e9b category: main optional: false -- name: pygments - version: 2.17.2 +- name: flask-dance + version: 7.0.0 manager: conda platform: linux-64 dependencies: + flask: '>=2.0.3' + oauthlib: '>=3.2' python: '>=3.7' - url: https://conda.anaconda.org/conda-forge/noarch/pygments-2.17.2-pyhd8ed1ab_0.conda + requests: '>=2.0' + requests-oauthlib: '>=1.0.0' + urlobject: '' + werkzeug: '' + url: https://conda.anaconda.org/conda-forge/noarch/flask-dance-7.0.0-pyhd8ed1ab_0.conda hash: - md5: 140a7f159396547e9799aa98f9f0742e - sha256: af5f8867450dc292f98ea387d4d8945fc574284677c8f60eaa9846ede7387257 + md5: a0007a17aa03b39c941f921f082afe9e + sha256: 7f3b75ba7f321c9058e0adbd4113fe3bfad3fc28ea266fb42f7636091cd4ca34 category: main optional: false -- name: pyjwt - version: 2.8.0 +- name: flask-dance + version: 7.0.0 manager: conda - platform: linux-64 + platform: osx-64 dependencies: - python: '>=3.6' - url: https://conda.anaconda.org/conda-forge/noarch/pyjwt-2.8.0-pyhd8ed1ab_0.conda + werkzeug: '' + urlobject: '' + python: '>=3.7' + requests-oauthlib: '>=1.0.0' + requests: '>=2.0' + flask: '>=2.0.3' + oauthlib: '>=3.2' + url: https://conda.anaconda.org/conda-forge/noarch/flask-dance-7.0.0-pyhd8ed1ab_0.conda hash: - md5: 912c0194f898fdb783021fd25f913c31 - sha256: 88ac94c42ade15113397e30d1831dd341399b5262fb5330b9240f915c33cd232 + md5: a0007a17aa03b39c941f921f082afe9e + sha256: 7f3b75ba7f321c9058e0adbd4113fe3bfad3fc28ea266fb42f7636091cd4ca34 category: main optional: false -- name: pysocks - version: 1.7.1 +- name: flask-login + version: 0.6.3 manager: conda platform: linux-64 dependencies: - __unix: '' - python: '>=3.8' - url: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha2e5f31_6.tar.bz2 + flask: '>=1.0.4' + python: '>=3.7' + werkzeug: '>=1.0.1' + url: https://conda.anaconda.org/conda-forge/noarch/flask-login-0.6.3-pyhd8ed1ab_1.conda hash: - md5: 2a7de29fb590ca14b5243c4c812c8025 - sha256: a42f826e958a8d22e65b3394f437af7332610e43ee313393d1cf143f0a2d274b + md5: 1d3523e7b18b38e4be992bf462d99e40 + sha256: 23ffed52801132e3e434c82a544ef1b77efbc025878a1a1cd0fbcc4c361cd9de category: main optional: false -- name: python-dotenv - version: 1.0.0 +- name: flask-login + version: 0.6.3 manager: conda - platform: linux-64 + platform: osx-64 dependencies: - python: '>=3.8' - url: https://conda.anaconda.org/conda-forge/noarch/python-dotenv-1.0.0-pyhd8ed1ab_1.conda + python: '>=3.7' + flask: '>=1.0.4' + werkzeug: '>=1.0.1' + url: https://conda.anaconda.org/conda-forge/noarch/flask-login-0.6.3-pyhd8ed1ab_1.conda hash: - md5: 111e7f9edd31865e2659fa9aad8ec8fd - sha256: bc5663f224ff6d8a399ec6bd8517e0c0f87a69ead438f82e5ce5c30f00077586 + md5: 1d3523e7b18b38e4be992bf462d99e40 + sha256: 23ffed52801132e3e434c82a544ef1b77efbc025878a1a1cd0fbcc4c361cd9de category: main optional: false -- name: python-tzdata - version: '2023.3' +- name: flask-sqlalchemy + version: 3.0.3 manager: conda platform: linux-64 dependencies: - python: '>=3.6' - url: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2023.3-pyhd8ed1ab_0.conda + flask: '>=2.2' + python: '>=3.7' + sqlalchemy: '>=1.4.18' + url: https://conda.anaconda.org/conda-forge/noarch/flask-sqlalchemy-3.0.3-pyhd8ed1ab_0.conda hash: - md5: 2590495f608a63625e165915fb4e2e34 - sha256: 0108888507014fb24573c31e4deceb61c99e63d37776dddcadd7c89b2ecae0b6 + md5: 6a301e2aa76aef91787e15f59ab03390 + sha256: 460693607ab0e2feb600bb755851c0c19db8d6dbd1c7636904c8de6f2c63040d category: main optional: false -- name: pytz - version: 2023.3.post1 +- name: flask-sqlalchemy + version: 3.0.3 manager: conda - platform: linux-64 + platform: osx-64 dependencies: - python: '>=3.6' - url: https://conda.anaconda.org/conda-forge/noarch/pytz-2023.3.post1-pyhd8ed1ab_0.conda + python: '>=3.7' + flask: '>=2.2' + sqlalchemy: '>=1.4.18' + url: https://conda.anaconda.org/conda-forge/noarch/flask-sqlalchemy-3.0.3-pyhd8ed1ab_0.conda hash: - md5: c93346b446cd08c169d843ae5fc0da97 - sha256: 6b680e63d69aaf087cd43ca765a23838723ef59b0a328799e6363eb13f52c49e + md5: 6a301e2aa76aef91787e15f59ab03390 + sha256: 460693607ab0e2feb600bb755851c0c19db8d6dbd1c7636904c8de6f2c63040d category: main optional: false -- name: pyyaml - version: 6.0.1 +- name: flask-wtf + version: 1.2.1 manager: conda platform: linux-64 dependencies: - libgcc-ng: '>=12' - python: '>=3.11,<3.12.0a0' - python_abi: 3.11.* - yaml: '>=0.2.5,<0.3.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.1-py311h459d7ec_1.conda + flask: '' + itsdangerous: '' + python: '>=3.8' + wtforms: '' + url: https://conda.anaconda.org/conda-forge/noarch/flask-wtf-1.2.1-pyhd8ed1ab_0.conda hash: - md5: 52719a74ad130de8fb5d047dc91f247a - sha256: 28729ef1ffa7f6f9dfd54345a47c7faac5d34296d66a2b9891fb147f4efe1348 + md5: e7b75c25f98d7b55943466683e400529 + sha256: cee583240eec93d27f262f3bb8cdecf8334e502d81923295d98f378b15fe26d5 category: main optional: false -- name: setuptools - version: 68.2.2 +- name: flask-wtf + version: 1.2.1 manager: conda - platform: linux-64 + platform: osx-64 dependencies: - python: '>=3.7' - url: https://conda.anaconda.org/conda-forge/noarch/setuptools-68.2.2-pyhd8ed1ab_0.conda + flask: '' + itsdangerous: '' + wtforms: '' + python: '>=3.8' + url: https://conda.anaconda.org/conda-forge/noarch/flask-wtf-1.2.1-pyhd8ed1ab_0.conda hash: - md5: fc2166155db840c634a1291a5c35a709 - sha256: 851901b1f8f2049edb36a675f0c3f9a98e1495ef4eb214761b048c6f696a06f7 + md5: e7b75c25f98d7b55943466683e400529 + sha256: cee583240eec93d27f262f3bb8cdecf8334e502d81923295d98f378b15fe26d5 category: main optional: false -- name: six - version: 1.16.0 +- name: gmp + version: 6.3.0 manager: conda - platform: linux-64 + platform: osx-64 dependencies: - python: '' - url: https://conda.anaconda.org/conda-forge/noarch/six-1.16.0-pyh6c4a22f_0.tar.bz2 + libcxx: '>=16' + url: https://conda.anaconda.org/conda-forge/osx-64/gmp-6.3.0-h73e2aa4_1.conda hash: - md5: e5f25f8dbc060e9a8d912e432202afc2 - sha256: a85c38227b446f42c5b90d9b642f2c0567880c15d72492d8da074a59c8f91dd6 + md5: 92f8d748d95d97f92fc26cfac9bb5b6e + sha256: 1a5b117908deb5a12288aba84dd0cb913f779c31c75f5a57d1a00e659e8fa3d3 category: main optional: false -- name: tomli - version: 2.0.1 +- name: greenlet + version: 3.0.3 manager: conda platform: linux-64 dependencies: - python: '>=3.7' - url: https://conda.anaconda.org/conda-forge/noarch/tomli-2.0.1-pyhd8ed1ab_0.tar.bz2 + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + url: https://conda.anaconda.org/conda-forge/linux-64/greenlet-3.0.3-py311hb755f60_0.conda hash: - md5: 5844808ffab9ebdb694585b50ba02a96 - sha256: 4cd48aba7cd026d17e86886af48d0d2ebc67ed36f87f6534f4b67138f5a5a58f + md5: 6f4b03b4d1e0da0962ea02113382677c + sha256: e6228b46b15ee3f54592c8a1fc1bf3846d519719ac65c238c20e21eb431971ec category: main optional: false -- name: tomlkit - version: 0.12.3 +- name: greenlet + version: 3.0.3 manager: conda - platform: linux-64 + platform: osx-64 dependencies: - python: '>=3.7' - url: https://conda.anaconda.org/conda-forge/noarch/tomlkit-0.12.3-pyha770c72_0.conda + libcxx: '>=15' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + url: https://conda.anaconda.org/conda-forge/osx-64/greenlet-3.0.3-py311hdd0406b_0.conda hash: - md5: 074d0ce7a6261ab8b497c3518796ef3e - sha256: 53cc436ab92d38683df1320e4468a8b978428e800195bf1c8c2460e90b0bc117 + md5: 4f538c462a4fed3aa89ea7993506c478 + sha256: 472971e11c0a4bffdad925523ae524361567a6428e7d5e38a76fadc59eaa8efc category: main optional: false -- name: types-markdown - version: 3.5.0.3 +- name: gunicorn + version: 21.2.0 manager: conda platform: linux-64 dependencies: - python: '>=3.6' - url: https://conda.anaconda.org/conda-forge/noarch/types-markdown-3.5.0.3-pyhd8ed1ab_0.conda + packaging: '' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + setuptools: '>=3.0' + url: https://conda.anaconda.org/conda-forge/linux-64/gunicorn-21.2.0-py311h38be061_1.conda hash: - md5: 9a78be2f67528371edc1bf3f908530b8 - sha256: a64b305419e2f0d537c42aa9c30285e31e41b852904e85d25243f9bc9a4c6d2d + md5: 6ffcdfe45b9b37a4a735d03a71b1a708 + sha256: b5d204fe26ad7e4b2a4b40761b12ec1f54fa63e498001ad14a75821e0f19297d category: main optional: false -- name: typing_extensions - version: 4.9.0 +- name: gunicorn + version: 21.2.0 manager: conda - platform: linux-64 + platform: osx-64 dependencies: - python: '>=3.8' - url: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.9.0-pyha770c72_0.conda + packaging: '' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + setuptools: '>=3.0' + url: https://conda.anaconda.org/conda-forge/osx-64/gunicorn-21.2.0-py311h6eed73b_1.conda hash: - md5: a92a6440c3fe7052d63244f3aba2a4a7 - sha256: f3c5be8673bfd905c4665efcb27fa50192f24f84fa8eff2f19cba5d09753d905 + md5: f818ed3365dbee71c75368587fc976b3 + sha256: dd5e241c4f1ad7982ea6aa5bc2cb5c03ce35af6a73ecc1a86c4dbb621ed12c68 category: main optional: false -- name: urlobject - version: 2.4.3 +- name: identify + version: 2.5.35 manager: conda platform: linux-64 dependencies: - python: '' - url: https://conda.anaconda.org/conda-forge/noarch/urlobject-2.4.3-py_1.tar.bz2 + python: '>=3.6' + ukkonen: '' + url: https://conda.anaconda.org/conda-forge/noarch/identify-2.5.35-pyhd8ed1ab_0.conda hash: - md5: 91140cccf570095e69d84b58075bc9b2 - sha256: 21354dad71b83bf6d0224f962565018c055f2d7f60205851491b9ff8f1d04936 + md5: 9472bfd206a2b7bb8143835e37667054 + sha256: 971683b13d1b820157bef9993c63dd8b0611d2d60fc4b522da163aee2e70e518 category: main optional: false -- name: vine - version: 5.0.0 +- name: identify + version: 2.5.35 manager: conda - platform: linux-64 + platform: osx-64 dependencies: + ukkonen: '' python: '>=3.6' - url: https://conda.anaconda.org/conda-forge/noarch/vine-5.0.0-pyhd8ed1ab_1.tar.bz2 + url: https://conda.anaconda.org/conda-forge/noarch/identify-2.5.35-pyhd8ed1ab_0.conda hash: - md5: 7cb5b514698cfd164ad5103e58e1e201 - sha256: 53e1239bd2ef11f75d381ba6944d8339a247e42d088de87b9d9f751ddf7176bc + md5: 9472bfd206a2b7bb8143835e37667054 + sha256: 971683b13d1b820157bef9993c63dd8b0611d2d60fc4b522da163aee2e70e518 category: main optional: false -- name: wcwidth - version: 0.2.12 +- name: idna + version: '3.6' manager: conda platform: linux-64 dependencies: - python: '>=3.8' - url: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.2.12-pyhd8ed1ab_0.conda + python: '>=3.6' + url: https://conda.anaconda.org/conda-forge/noarch/idna-3.6-pyhd8ed1ab_0.conda hash: - md5: bf4a1d1a97ca27b0b65bacd9e238b484 - sha256: ca757d0fc2dbd422af9d3238a8b4b630a6e11df3707a447bd89540656770d1d7 + md5: 1a76f09108576397c41c0b0c5bd84134 + sha256: 6ee4c986d69ce61e60a20b2459b6f2027baeba153f0a64995fd3cb47c2cc7e07 category: main optional: false -- name: wheel - version: 0.42.0 +- name: idna + version: '3.6' manager: conda - platform: linux-64 + platform: osx-64 dependencies: - python: '>=3.7' - url: https://conda.anaconda.org/conda-forge/noarch/wheel-0.42.0-pyhd8ed1ab_0.conda + python: '>=3.6' + url: https://conda.anaconda.org/conda-forge/noarch/idna-3.6-pyhd8ed1ab_0.conda hash: - md5: 1cdea58981c5cbc17b51973bcaddcea7 - sha256: 80be0ccc815ce22f80c141013302839b0ed938a2edb50b846cf48d8a8c1cfa01 + md5: 1a76f09108576397c41c0b0c5bd84134 + sha256: 6ee4c986d69ce61e60a20b2459b6f2027baeba153f0a64995fd3cb47c2cc7e07 category: main optional: false -- name: zipp - version: 3.17.0 +- name: importlib-metadata + version: 7.0.2 manager: conda platform: linux-64 dependencies: python: '>=3.8' - url: https://conda.anaconda.org/conda-forge/noarch/zipp-3.17.0-pyhd8ed1ab_0.conda + zipp: '>=0.5' + url: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-7.0.2-pyha770c72_0.conda hash: - md5: 2e4d6bc0b14e10f895fc6791a7d9b26a - sha256: bced1423fdbf77bca0a735187d05d9b9812d2163f60ab426fc10f11f92ecbe26 + md5: b050a4bb0e90ebd6e7fa4093d6346867 + sha256: 9a26136d2cc81ccac209d6ae24281ceba3365fe34e34b2c45570f2a96e9d9c1b category: main optional: false -- name: amqp - version: 5.2.0 +- name: importlib-metadata + version: 7.0.2 manager: conda - platform: linux-64 + platform: osx-64 dependencies: - python: '>=3.6' - vine: 5.0.0 - url: https://conda.anaconda.org/conda-forge/noarch/amqp-5.2.0-pyhd8ed1ab_0.conda + python: '>=3.8' + zipp: '>=0.5' + url: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-7.0.2-pyha770c72_0.conda hash: - md5: 7ab12cc04a0596a8ab937e9d211503cc - sha256: 9993eb1df7e4a9a4e59c82a87495ab03762c94801f7c4bb8db922ca28681c126 + md5: b050a4bb0e90ebd6e7fa4093d6346867 + sha256: 9a26136d2cc81ccac209d6ae24281ceba3365fe34e34b2c45570f2a96e9d9c1b category: main optional: false -- name: cffi - version: 1.16.0 +- name: iniconfig + version: 2.0.0 manager: conda platform: linux-64 dependencies: - libffi: '>=3.4,<4.0a0' - libgcc-ng: '>=12' - pycparser: '' - python: '>=3.11,<3.12.0a0' - python_abi: 3.11.* - url: https://conda.anaconda.org/conda-forge/linux-64/cffi-1.16.0-py311hb3a22ac_0.conda + python: '>=3.7' + url: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_0.conda hash: - md5: b3469563ac5e808b0cd92810d0697043 - sha256: b71c94528ca0c35133da4b7ef69b51a0b55eeee570376057f3d2ad60c3ab1444 + md5: f800d2da156d08e289b14e87e43c1ae5 + sha256: 38740c939b668b36a50ef455b077e8015b8c9cf89860d421b3fff86048f49666 category: main optional: false -- name: click-didyoumean - version: 0.3.0 +- name: iniconfig + version: 2.0.0 manager: conda - platform: linux-64 + platform: osx-64 dependencies: - click: '>=7' - python: '>=3.6.2,<4.0.0' - url: https://conda.anaconda.org/conda-forge/noarch/click-didyoumean-0.3.0-pyhd8ed1ab_0.tar.bz2 + python: '>=3.7' + url: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_0.conda hash: - md5: a49aa7bc1a82485f6a0a8849dcfefcad - sha256: e1af96905976a83f5545b0914830e272415e2e66428165fdf178e73dc4bc9cff + md5: f800d2da156d08e289b14e87e43c1ae5 + sha256: 38740c939b668b36a50ef455b077e8015b8c9cf89860d421b3fff86048f49666 category: main optional: false -- name: click-plugins - version: 1.1.1 +- name: invoke + version: 2.0.0 manager: conda platform: linux-64 dependencies: - click: '>=3.0' - python: '' - url: https://conda.anaconda.org/conda-forge/noarch/click-plugins-1.1.1-py_0.tar.bz2 + python: '>=3.6' + url: https://conda.anaconda.org/conda-forge/noarch/invoke-2.0.0-pyhd8ed1ab_0.conda hash: - md5: 4fd2c6b53934bd7d96d1f3fdaf99b79f - sha256: ddef6e559dde6673ee504b0e29dd814d36e22b6b9b1f519fa856ee268905bf92 + md5: c282e4567f7b800a855f7092b3c44a5a + sha256: 637e51d8df0159bae9d67f840a24a5fc7973d48fac09418b01e2fb6f3c03224b category: main optional: false -- name: gunicorn - version: 21.2.0 +- name: invoke + version: 2.0.0 manager: conda - platform: linux-64 + platform: osx-64 dependencies: - packaging: '' - python: '>=3.11,<3.12.0a0' - python_abi: 3.11.* - setuptools: '>=3.0' - url: https://conda.anaconda.org/conda-forge/linux-64/gunicorn-21.2.0-py311h38be061_1.conda + python: '>=3.6' + url: https://conda.anaconda.org/conda-forge/noarch/invoke-2.0.0-pyhd8ed1ab_0.conda hash: - md5: 6ffcdfe45b9b37a4a735d03a71b1a708 - sha256: b5d204fe26ad7e4b2a4b40761b12ec1f54fa63e498001ad14a75821e0f19297d + md5: c282e4567f7b800a855f7092b3c44a5a + sha256: 637e51d8df0159bae9d67f840a24a5fc7973d48fac09418b01e2fb6f3c03224b category: main optional: false -- name: importlib-metadata - version: 7.0.0 +- name: itsdangerous + version: 2.1.2 manager: conda platform: linux-64 dependencies: - python: '>=3.8' - zipp: '>=0.5' - url: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-7.0.0-pyha770c72_0.conda + python: '>=3.7' + url: https://conda.anaconda.org/conda-forge/noarch/itsdangerous-2.1.2-pyhd8ed1ab_0.tar.bz2 hash: - md5: a941237cd06538837b25cd245fcd25d8 - sha256: 9731e82a00d36b182dc515e31723e711ac82890bb1ca86c6a17a4b471135564f + md5: 3c3de74912f11d2b590184f03c7cd09b + sha256: 31e3492686b4e92b53db9b48bc0eb03873b1caaf28629fee7d2d47627a2c56d3 category: main optional: false -- name: jinja2 - version: 3.1.2 +- name: itsdangerous + version: 2.1.2 manager: conda - platform: linux-64 + platform: osx-64 dependencies: - markupsafe: '>=2.0' python: '>=3.7' - url: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.2-pyhd8ed1ab_1.tar.bz2 + url: https://conda.anaconda.org/conda-forge/noarch/itsdangerous-2.1.2-pyhd8ed1ab_0.tar.bz2 hash: - md5: c8490ed5c70966d232fdd389d0dbed37 - sha256: b045faba7130ab263db6a8fdc96b1a3de5fcf85c4a607c5f11a49e76851500b5 + md5: 3c3de74912f11d2b590184f03c7cd09b + sha256: 31e3492686b4e92b53db9b48bc0eb03873b1caaf28629fee7d2d47627a2c56d3 category: main optional: false -- name: markdown-it-py - version: 3.0.0 +- name: jinja2 + version: 3.1.3 manager: conda platform: linux-64 dependencies: - mdurl: '>=0.1,<1' - python: '>=3.8' - url: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-3.0.0-pyhd8ed1ab_0.conda + markupsafe: '>=2.0' + python: '>=3.7' + url: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.3-pyhd8ed1ab_0.conda hash: - md5: 93a8e71256479c62074356ef6ebf501b - sha256: c041b0eaf7a6af3344d5dd452815cdc148d6284fec25a4fa3f4263b3a021e962 + md5: e7d8df6509ba635247ff9aea31134262 + sha256: fd517b7dd3a61eca34f8a6f9f92f306397149cae1204fce72ac3d227107dafdc category: main optional: false -- name: mypy - version: 1.7.1 +- name: jinja2 + version: 3.1.3 manager: conda - platform: linux-64 + platform: osx-64 dependencies: - libgcc-ng: '>=12' - mypy_extensions: '>=1.0.0' - psutil: '>=4.0' - python: '>=3.11,<3.12.0a0' - python_abi: 3.11.* - typing_extensions: '>=4.1.0' - url: https://conda.anaconda.org/conda-forge/linux-64/mypy-1.7.1-py311h459d7ec_1.conda + python: '>=3.7' + markupsafe: '>=2.0' + url: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.3-pyhd8ed1ab_0.conda hash: - md5: 0a861b29266afe8c315ecdcb92f9e692 - sha256: 8c9365ecf8e56666253f987ba83d1dcde141ef3d0f73300a92adf5aa63c8e5bd + md5: e7d8df6509ba635247ff9aea31134262 + sha256: fd517b7dd3a61eca34f8a6f9f92f306397149cae1204fce72ac3d227107dafdc category: main optional: false -- name: nodeenv - version: 1.8.0 +- name: jmespath + version: 1.0.1 manager: conda platform: linux-64 dependencies: - python: 2.7|>=3.7 - setuptools: '' - url: https://conda.anaconda.org/conda-forge/noarch/nodeenv-1.8.0-pyhd8ed1ab_0.conda + python: '>=3.7' + url: https://conda.anaconda.org/conda-forge/noarch/jmespath-1.0.1-pyhd8ed1ab_0.tar.bz2 hash: - md5: 2a75b296096adabbabadd5e9782e5fcc - sha256: 1320306234552717149f36f825ddc7e27ea295f24829e9db4cc6ceaff0b032bd + md5: 2cfa3e1cf3fb51bb9b17acc5b5e9ea11 + sha256: 95ac5f9ee95fd4e34dc051746fc86016d3d4f6abefed113e2ede049d59ec2991 category: main optional: false -- name: pip - version: 23.3.2 +- name: jmespath + version: 1.0.1 manager: conda - platform: linux-64 + platform: osx-64 dependencies: python: '>=3.7' - setuptools: '' - wheel: '' - url: https://conda.anaconda.org/conda-forge/noarch/pip-23.3.2-pyhd8ed1ab_0.conda + url: https://conda.anaconda.org/conda-forge/noarch/jmespath-1.0.1-pyhd8ed1ab_0.tar.bz2 hash: - md5: 8591c748f98dcc02253003533bc2e4b1 - sha256: 29096d1d53c61aeef518729add2f405df86b3629d1d738a35b15095e6a02eeed + md5: 2cfa3e1cf3fb51bb9b17acc5b5e9ea11 + sha256: 95ac5f9ee95fd4e34dc051746fc86016d3d4f6abefed113e2ede049d59ec2991 category: main optional: false -- name: prompt-toolkit - version: 3.0.42 +- name: keyutils + version: 1.6.1 manager: conda platform: linux-64 dependencies: - python: '>=3.7' - wcwidth: '' - url: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.42-pyha770c72_0.conda + libgcc-ng: '>=10.3.0' + url: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.1-h166bdaf_0.tar.bz2 hash: - md5: 0bf64bf10eee21f46ac83c161917fa86 - sha256: 58525b2a9305fb154b2b0d43a48b9a6495441b80e4fbea44f2a34a597d2cef16 + md5: 30186d27e2c9fa62b45fb1476b7200e3 + sha256: 150c05a6e538610ca7c43beb3a40d65c90537497a4f6a5f4d15ec0451b6f5ebb category: main optional: false -- name: psycopg2 - version: 2.9.9 +- name: kombu + version: 5.3.5 manager: conda platform: linux-64 dependencies: - libgcc-ng: '>=12' - libpq: '>=16.1,<17.0a0' + amqp: '>=5.1.1,<6.0.0' + boto3: '>=1.26.143' python: '>=3.11,<3.12.0a0' python_abi: 3.11.* - url: https://conda.anaconda.org/conda-forge/linux-64/psycopg2-2.9.9-py311h03dec38_0.conda + vine: '' + url: https://conda.anaconda.org/conda-forge/linux-64/kombu-5.3.5-py311h38be061_0.conda hash: - md5: 3cc2decd316838bce14d73818e0bf7a4 - sha256: 4e78d9fe1799d028d9a2da3636a3a68a531aeca5d2c679d4fc78627a426b11cb + md5: 00508fb0af467a4b7ac70f4bf3825c81 + sha256: 9da60412961f4e0b224f08825a041c8e1446753fb733265606b81f5746568cb9 category: main optional: false -- name: pytest - version: 7.4.3 +- name: kombu + version: 5.3.5 manager: conda - platform: linux-64 + platform: osx-64 dependencies: - colorama: '' - exceptiongroup: '>=1.0.0rc8' - iniconfig: '' - packaging: '' - pluggy: '>=0.12,<2.0' - python: '>=3.7' - tomli: '>=1.0.0' - url: https://conda.anaconda.org/conda-forge/noarch/pytest-7.4.3-pyhd8ed1ab_0.conda + amqp: '>=5.1.1,<6.0.0' + boto3: '>=1.26.143' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + vine: '' + url: https://conda.anaconda.org/conda-forge/osx-64/kombu-5.3.5-py311h6eed73b_0.conda hash: - md5: 5bdca0aca30b0ee62bb84854e027eae0 - sha256: 14e948e620ec87d9e62a8d9c21d40084b4805a939cfee322be7d457379dc96a0 + md5: 87d591ff96c71329c6430b5ad63d9d85 + sha256: 917244687c06ecf3c3754075eb546f0d897b284eaf134c1ca29151ddadecdaaf category: main optional: false -- name: python-dateutil - version: 2.8.2 +- name: krb5 + version: 1.21.2 manager: conda platform: linux-64 dependencies: - python: '>=3.6' - six: '>=1.5' - url: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.8.2-pyhd8ed1ab_0.tar.bz2 + keyutils: '>=1.6.1,<2.0a0' + libedit: '>=3.1.20191231,<4.0a0' + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + openssl: '>=3.1.2,<4.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.2-h659d440_0.conda hash: - md5: dd999d1cc9f79e67dbb855c8924c7984 - sha256: 54d7785c7678166aa45adeaccfc1d2b8c3c799ca2dc05d4a82bb39b1968bd7da - category: main + md5: cd95826dbd331ed1be26bdf401432844 + sha256: 259bfaae731989b252b7d2228c1330ef91b641c9d68ff87dae02cbae682cb3e4 + category: main optional: false -- name: sqlalchemy - version: 1.4.49 +- name: krb5 + version: 1.21.2 manager: conda - platform: linux-64 + platform: osx-64 dependencies: - greenlet: '!=0.4.17' - libgcc-ng: '>=12' - python: '>=3.11,<3.12.0a0' - python_abi: 3.11.* - url: https://conda.anaconda.org/conda-forge/linux-64/sqlalchemy-1.4.49-py311h459d7ec_1.conda + libcxx: '>=15.0.7' + libedit: '>=3.1.20191231,<4.0a0' + openssl: '>=3.1.2,<4.0a0' + url: https://conda.anaconda.org/conda-forge/osx-64/krb5-1.21.2-hb884880_0.conda hash: - md5: 17392bcb4ceac1b2c95db9d54b4ac018 - sha256: 542dea4823e2e1283936fbd25c9f3fa960ec6df2dd54589b192b4dac68af7295 + md5: 80505a68783f01dc8d7308c075261b2f + sha256: 081ae2008a21edf57c048f331a17c65d1ccb52d6ca2f87ee031a73eff4dc0fc6 category: main optional: false -- name: typing-extensions - version: 4.9.0 +- name: ld_impl_linux-64 + version: '2.40' manager: conda platform: linux-64 - dependencies: - typing_extensions: 4.9.0 - url: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.9.0-hd8ed1ab_0.conda + dependencies: {} + url: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.40-h41732ed_0.conda hash: - md5: c16524c1b7227dc80b36b4fa6f77cc86 - sha256: d795c1eb1db4ea147f01ece74e5a504d7c2e8d5ee8c11ec987884967dd938f9c + md5: 7aca3059a1729aa76c597603f10b0dd3 + sha256: f6cc89d887555912d6c61b295d398cff9ec982a3417d38025c45d5dd9b9e79cd category: main optional: false -- name: urllib3 - version: 1.26.18 +- name: libcxx + version: 16.0.6 manager: conda - platform: linux-64 - dependencies: - brotli-python: '>=1.0.9' - pysocks: '>=1.5.6,<2.0,!=1.5.7' - python: '>=3.7' - url: https://conda.anaconda.org/conda-forge/noarch/urllib3-1.26.18-pyhd8ed1ab_0.conda + platform: osx-64 + dependencies: {} + url: https://conda.anaconda.org/conda-forge/osx-64/libcxx-16.0.6-hd57cbcb_0.conda hash: - md5: bf61cfd2a7f212efba378167a07d4a6a - sha256: 1cc0bab65a6ad0f5a8bd7657760a4fb4e670d30377f9dab88b792977cb3687e7 + md5: 7d6972792161077908b62971802f289a + sha256: 9063271847cf05f3a6cc6cae3e7f0ced032ab5f3a3c9d3f943f876f39c5c2549 category: main optional: false -- name: virtualenv - version: 20.25.0 +- name: libedit + version: 3.1.20191231 manager: conda platform: linux-64 dependencies: - distlib: <1,>=0.3.7 - filelock: <4,>=3.12.2 - platformdirs: <5,>=3.9.1 - python: '>=3.8' - url: https://conda.anaconda.org/conda-forge/noarch/virtualenv-20.25.0-pyhd8ed1ab_0.conda + libgcc-ng: '>=7.5.0' + ncurses: '>=6.2,<7.0.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20191231-he28a2e2_2.tar.bz2 hash: - md5: c119653cba436d8183c27bf6d190e587 - sha256: 50827c3721a9dbf973b568709d4381add2a6552fa562f26a385c5edc16a534af + md5: 4d331e44109e3f0e19b4cb8f9b82f3e1 + sha256: a57d37c236d8f7c886e01656f4949d9dcca131d2a0728609c6f7fa338b65f1cf category: main optional: false -- name: werkzeug - version: 2.2.3 +- name: libedit + version: 3.1.20191231 manager: conda - platform: linux-64 + platform: osx-64 dependencies: - markupsafe: '>=2.1.1' - python: '>=3.7' - url: https://conda.anaconda.org/conda-forge/noarch/werkzeug-2.2.3-pyhd8ed1ab_0.conda + ncurses: '>=6.2,<7.0.0a0' + url: https://conda.anaconda.org/conda-forge/osx-64/libedit-3.1.20191231-h0678c8f_2.tar.bz2 hash: - md5: 94dea682ba9d8e091e1f46872886fb1f - sha256: 38f68541580b74ed91396a67766fb96088f7b161bfd203675ed7a257d642e2e3 + md5: 6016a8a1d0e63cac3de2c352cd40208b + sha256: dbd3c3f2eca1d21c52e4c03b21930bbce414c4592f8ce805801575b9e9256095 category: main optional: false -- name: wtforms - version: 3.0.1 +- name: libexpat + version: 2.6.1 manager: conda platform: linux-64 dependencies: - markupsafe: '' - python: '>=3.7' - url: https://conda.anaconda.org/conda-forge/noarch/wtforms-3.0.1-pyhd8ed1ab_0.tar.bz2 + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.6.1-h59595ed_0.conda hash: - md5: 62a19155ee3896566849634586c1813d - sha256: fc6bc71fc1bd5966ad024a1c828652b9a1ceee06ea514a94968b9b53492e3abe + md5: 476fb82aba5358a08d52ec44e286ce33 + sha256: 1c993845e8c25545565f50ab74511276a519e969acc406603e3f4539a14288b2 category: main optional: false -- name: annotated-types - version: 0.6.0 +- name: libexpat + version: 2.6.1 manager: conda - platform: linux-64 - dependencies: - python: '>=3.7' - typing-extensions: '>=4.0.0' - url: https://conda.anaconda.org/conda-forge/noarch/annotated-types-0.6.0-pyhd8ed1ab_0.conda + platform: osx-64 + dependencies: {} + url: https://conda.anaconda.org/conda-forge/osx-64/libexpat-2.6.1-h73e2aa4_0.conda hash: - md5: 997c29372bdbe2afee073dff71f35923 - sha256: 3a2c98154d95cfd54daba6b7d507d31f5ba07ac2ad955c44eb041b66563193cd + md5: 1596476cfdb3183d820afb9293f412e1 + sha256: 677471f90d844fcf6c9eae49ecbb812bdd87a6a272ffa69fcc322d6b71b4c060 category: main optional: false -- name: botocore - version: 1.34.2 +- name: libffi + version: 3.4.2 manager: conda platform: linux-64 dependencies: - jmespath: '>=0.7.1,<2.0.0' - python: '>=3.8' - python-dateutil: '>=2.1,<3.0.0' - urllib3: '>=1.25.4,<1.27' - url: https://conda.anaconda.org/conda-forge/noarch/botocore-1.34.2-pyhd8ed1ab_0.conda + libgcc-ng: '>=9.4.0' + url: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.2-h7f98852_5.tar.bz2 hash: - md5: 90d807074ce872ffb1e20ea0e7c76c10 - sha256: e404de75c98ee31d06b52c631078df97f77e50ebcb644758c22ec86fcca2d6c4 + md5: d645c6d2ac96843a2bfaccd2d62b3ac3 + sha256: ab6e9856c21709b7b517e940ae7028ae0737546122f83c2aa5d692860c3b149e category: main optional: false -- name: cryptography - version: 41.0.7 +- name: libffi + version: 3.4.2 manager: conda - platform: linux-64 - dependencies: - cffi: '>=1.12' - libgcc-ng: '>=12' - openssl: '>=3.1.4,<4.0a0' - python: '>=3.11,<3.12.0a0' - python_abi: 3.11.* - url: https://conda.anaconda.org/conda-forge/linux-64/cryptography-41.0.7-py311hcb13ee4_1.conda + platform: osx-64 + dependencies: {} + url: https://conda.anaconda.org/conda-forge/osx-64/libffi-3.4.2-h0d85af4_5.tar.bz2 hash: - md5: ca6e04ac7262ecaec846e483d6fdc6c8 - sha256: 0959d015727ae5f55f385556a0a19b9f6036752ea05f78a99cb534803e325cab + md5: ccb34fb14960ad8b125962d3d79b31a9 + sha256: 7a2d27a936ceee6942ea4d397f9c7d136f12549d86f7617e8b6bad51e01a941f category: main optional: false -- name: markdown - version: 3.5.1 +- name: libgcc-ng + version: 13.2.0 manager: conda platform: linux-64 dependencies: - importlib-metadata: '>=4.4' - python: '>=3.6' - url: https://conda.anaconda.org/conda-forge/noarch/markdown-3.5.1-pyhd8ed1ab_0.conda + _libgcc_mutex: '0.1' + _openmp_mutex: '>=4.5' + url: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-13.2.0-h807b86a_5.conda hash: - md5: 323495027ffa625701129acebf861412 - sha256: 35e8990504cf8dc7e2bb63efd855fb0a0d5c0d77bf79403235e757c24a83ec1d + md5: d4ff227c46917d3b4565302a2bbb276b + sha256: d32f78bfaac282cfe5205f46d558704ad737b8dbf71f9227788a5ca80facaba4 category: main optional: false -- name: prompt_toolkit - version: 3.0.42 +- name: libgomp + version: 13.2.0 manager: conda platform: linux-64 dependencies: - prompt-toolkit: '>=3.0.42,<3.0.43.0a0' - url: https://conda.anaconda.org/conda-forge/noarch/prompt_toolkit-3.0.42-hd8ed1ab_0.conda + _libgcc_mutex: '0.1' + url: https://conda.anaconda.org/conda-forge/linux-64/libgomp-13.2.0-h807b86a_5.conda hash: - md5: 85a2189ecd2fcdd86e92b2d4ea8fe461 - sha256: fd2185d501bf34cb4c121f2f5ade9157ac75e1644a9da81355c4c8f9c1b82d4d + md5: d211c42b9ce49aee3734fdc828731689 + sha256: 0d3d4b1b0134283ea02d58e8eb5accf3655464cf7159abf098cc694002f8d34e category: main optional: false -- name: pydantic - version: 1.10.13 +- name: libnsl + version: 2.0.1 manager: conda platform: linux-64 dependencies: libgcc-ng: '>=12' - python: '>=3.11,<3.12.0a0' - python_abi: 3.11.* - typing-extensions: '>=4.2.0' - url: https://conda.anaconda.org/conda-forge/linux-64/pydantic-1.10.13-py311h459d7ec_1.conda + url: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hd590300_0.conda hash: - md5: 8a92f40420211897a35841861e7e8348 - sha256: f2d3a838fc90699c5dcd537aff10c78b33bd755232d0b21b26247cbf185cced7 + md5: 30fd6e37fe21f86f4bd26d6ee73eeec7 + sha256: 26d77a3bb4dceeedc2a41bd688564fe71bf2d149fdcf117049970bc02ff1add6 category: main optional: false -- name: pydantic-core - version: 2.14.5 +- name: libpq + version: '16.2' manager: conda platform: linux-64 dependencies: + krb5: '>=1.21.2,<1.22.0a0' libgcc-ng: '>=12' - python: '>=3.11,<3.12.0a0' - python_abi: 3.11.* - typing-extensions: '>=4.6.0' - url: https://conda.anaconda.org/conda-forge/linux-64/pydantic-core-2.14.5-py311h46250e7_0.conda + openssl: '>=3.2.1,<4.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/libpq-16.2-h33b98f1_0.conda hash: - md5: 9b2d1233d958079649cc8f91d814e04f - sha256: c546a042316c34bf6b9c5e16da4e6993f6712554c0ac5ee3f49260260789c38f + md5: fe0e297faf462ee579c95071a5211665 + sha256: 352748b0499a22e2a8e103f071b8d9357e1fb710c0aec0f79895d3ba03dccb03 category: main optional: false -- name: requests - version: 2.31.0 +- name: libpq + version: '16.2' manager: conda - platform: linux-64 + platform: osx-64 dependencies: - certifi: '>=2017.4.17' - charset-normalizer: '>=2,<4' - idna: '>=2.5,<4' - python: '>=3.7' - urllib3: '>=1.21.1,<3' - url: https://conda.anaconda.org/conda-forge/noarch/requests-2.31.0-pyhd8ed1ab_0.conda + krb5: '>=1.21.2,<1.22.0a0' + openssl: '>=3.2.1,<4.0a0' + url: https://conda.anaconda.org/conda-forge/osx-64/libpq-16.2-ha925e61_0.conda hash: - md5: a30144e4156cdbb236f99ebb49828f8b - sha256: 9f629d6fd3c8ac5f2a198639fe7af87c4db2ac9235279164bfe0fcb49d8c4bad + md5: 8b81f4feaa3744271fcf2822ad1489f1 + sha256: 537b3816ac66f12c56fc62a67d896703b68f7588a5d83ab98009731de82eb742 category: main optional: false -- name: rich - version: 13.7.0 +- name: libsqlite + version: 3.45.2 manager: conda platform: linux-64 dependencies: - markdown-it-py: '>=2.2.0' - pygments: '>=2.13.0,<3.0.0' - python: '>=3.7.0' - typing_extensions: '>=4.0.0,<5.0.0' - url: https://conda.anaconda.org/conda-forge/noarch/rich-13.7.0-pyhd8ed1ab_0.conda + libgcc-ng: '>=12' + libzlib: '>=1.2.13,<1.3.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.45.2-h2797004_0.conda hash: - md5: d7a11d4f3024b2f4a6e0ae7377dd61e9 - sha256: 4bb25bf1f5664772b2c4c2e3878aa6e7dc2695f97e3da4ee8e47c51e179913bb + md5: 866983a220e27a80cb75e85cb30466a1 + sha256: 8cdbeb7902729e319510a82d7c642402981818702b58812af265ef55d1315473 category: main optional: false -- name: ukkonen - version: 1.0.1 +- name: libsqlite + version: 3.45.2 manager: conda - platform: linux-64 + platform: osx-64 dependencies: - cffi: '' - libgcc-ng: '>=12' - libstdcxx-ng: '>=12' - python: '>=3.11,<3.12.0a0' - python_abi: 3.11.* - url: https://conda.anaconda.org/conda-forge/linux-64/ukkonen-1.0.1-py311h9547e67_4.conda + libzlib: '>=1.2.13,<1.3.0a0' + url: https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.45.2-h92b6c6a_0.conda hash: - md5: 586da7df03b68640de14dc3e8bcbf76f - sha256: c2d33e998f637b594632eba3727529171a06eb09896e36aa42f1ebcb03779472 + md5: 086f56e13a96a6cfb1bf640505ae6b70 + sha256: 320ec73a4e3dd377757a2595770b8137ec4583df4d7782472d76377cdbdc4543 category: main optional: false -- name: click-repl - version: 0.3.0 +- name: libstdcxx-ng + version: 13.2.0 manager: conda platform: linux-64 - dependencies: - click: '' - prompt_toolkit: '' - python: '>=3.6' - six: '' - url: https://conda.anaconda.org/conda-forge/noarch/click-repl-0.3.0-pyhd8ed1ab_0.conda + dependencies: {} + url: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-13.2.0-h7e041cc_5.conda hash: - md5: 27eb8f68250666c1a19d1b6ec9d12c4e - sha256: 69c16e0b89e1fb4fc444af4798ef1222b3075d074cbcbe70b9af793668200a14 + md5: f6f6600d18a4047b54f803cf708b868a + sha256: a56c5b11f1e73a86e120e6141a42d9e935a99a2098491ac9e15347a1476ce777 category: main optional: false -- name: identify - version: 2.5.33 +- name: libuuid + version: 2.38.1 manager: conda platform: linux-64 dependencies: - python: '>=3.6' - ukkonen: '' - url: https://conda.anaconda.org/conda-forge/noarch/identify-2.5.33-pyhd8ed1ab_0.conda + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda hash: - md5: 93c8f8ceb83827d88deeba796f07fba7 - sha256: ce2a64c18221af96226be23278d81f22ff9f64b3c047d8865590f6718915303f + md5: 40b61aab5c7ba9ff276c41cfffe6b80b + sha256: 787eb542f055a2b3de553614b25f09eefb0a0931b0c87dbcce6efdfd92f04f18 category: main optional: false -- name: oauthlib - version: 3.2.2 +- name: libxcrypt + version: 4.4.36 manager: conda platform: linux-64 dependencies: - blinker: '' - cryptography: '' - pyjwt: '>=1.0.0' - python: '>=3.6' - url: https://conda.anaconda.org/conda-forge/noarch/oauthlib-3.2.2-pyhd8ed1ab_0.tar.bz2 + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda hash: - md5: 8f882b197fd9c4941a787926baea4868 - sha256: 0cfd5146a91d3974f4abfc2a45de890371d510a77238fe553e036ec8c031dc5b + md5: 5aa797f8787fe7a17d1b0821485b5adc + sha256: 6ae68e0b86423ef188196fff6207ed0c8195dd84273cb5623b85aa08033a410c category: main optional: false -- name: pydantic-settings - version: 2.1.0 +- name: libzlib + version: 1.2.13 manager: conda platform: linux-64 dependencies: - pydantic: '>=2.3.0' - python: '>=3.8' - python-dotenv: '>=0.21.0' - url: https://conda.anaconda.org/conda-forge/noarch/pydantic-settings-2.1.0-pyhd8ed1ab_1.conda + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.2.13-hd590300_5.conda hash: - md5: 2a63052c1180846d4a6aaa9df7e113a3 - sha256: 2c80df34463dabec383b37dc19da48f84a1ea97f3d828d6d0dd220110da5f4e1 + md5: f36c115f1ee199da648e0597ec2047ad + sha256: 370c7c5893b737596fd6ca0d9190c9715d89d888b8c88537ae1ef168c25e82e4 category: main optional: false -- name: pyopenssl - version: 23.3.0 +- name: libzlib + version: 1.2.13 manager: conda - platform: linux-64 - dependencies: - cryptography: '>=41.0.5,<42' - python: '>=3.7' - url: https://conda.anaconda.org/conda-forge/noarch/pyopenssl-23.3.0-pyhd8ed1ab_0.conda + platform: osx-64 + dependencies: {} + url: https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.2.13-h8a1eda9_5.conda hash: - md5: 7819533e674dbbc51468f3228b9b1bb6 - sha256: f7e04c4a49b1593140231d70801e2204e314e26d7141bfbdc8089d04114c0010 + md5: 4a3ad23f6e16f99c04e166767193d700 + sha256: fc58ad7f47ffea10df1f2165369978fba0a1cc32594aad778f5eec725f334867 category: main optional: false -- name: rich-click - version: 1.7.2 +- name: loguru + version: 0.6.0 manager: conda platform: linux-64 dependencies: - click: '>=7,<9' - python: '>=3.7' - rich: '>=10' - url: https://conda.anaconda.org/conda-forge/noarch/rich-click-1.7.2-pyhd8ed1ab_0.conda + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + url: https://conda.anaconda.org/conda-forge/linux-64/loguru-0.6.0-py311h38be061_2.tar.bz2 hash: - md5: fe1eae76a71246959cefd09b5b00869b - sha256: 4bfddbc5716a09bfea1c3345b1eed0ddb36d0e55f357afb60ca3e73c9231cc68 + md5: 8495a2a957bbeacad2c25f277f407f24 + sha256: 202d08801360bd4da179428107cb12ed31816d42acf5d5df179fb2aa55014a1a category: main optional: false -- name: s3transfer - version: 0.9.0 +- name: loguru + version: 0.6.0 manager: conda - platform: linux-64 + platform: osx-64 dependencies: - botocore: '>=1.33.2,<2.0a.0' - python: '>=3.8' - url: https://conda.anaconda.org/conda-forge/noarch/s3transfer-0.9.0-pyhd8ed1ab_0.conda + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + url: https://conda.anaconda.org/conda-forge/osx-64/loguru-0.6.0-py311h6eed73b_2.tar.bz2 hash: - md5: 27ad14e5fc6a13f05b90140debc72cd2 - sha256: c9fc315d830238113160471467259740593966bcf59f17287a0baeaf1f6a76d8 + md5: fc9dde292b3ec1251e63aac5434cc47c + sha256: f74d200ffa50c455fbe631ca6564fe5b80c64ab314ec832528cfcdfad2a119d7 category: main optional: false -- name: boto3 - version: 1.34.2 +- name: markdown + version: 3.5.2 manager: conda platform: linux-64 dependencies: - botocore: '>=1.34.2,<1.35.0' - jmespath: '>=0.7.1,<2.0.0' - python: '>=3.8' - s3transfer: '>=0.9.0,<0.10.0' - url: https://conda.anaconda.org/conda-forge/noarch/boto3-1.34.2-pyhd8ed1ab_0.conda + importlib-metadata: '>=4.4' + python: '>=3.6' + url: https://conda.anaconda.org/conda-forge/noarch/markdown-3.5.2-pyhd8ed1ab_0.conda hash: - md5: 6f47ce30284317cb85960c4668cc5bd9 - sha256: c0444d9d386a1991fadca8b31b9dfc302e5d781b8a2d29faff2c47286eff477c + md5: db7b48fa4eeb0c21b2f3f5b1f7d9ebcf + sha256: fbc70dc01b361fe46e7b4e102e725f99ba60bf9903c2fa86b53ad6b70ded677a category: main optional: false -- name: bump-my-version - version: 0.9.3 +- name: markdown + version: 3.5.2 manager: conda - platform: linux-64 + platform: osx-64 dependencies: - click: '' - pydantic: <2.0.0 - python: '>=3.8' - rich: '' - rich-click: '' - tomlkit: '' - url: https://conda.anaconda.org/conda-forge/noarch/bump-my-version-0.9.3-pyhd8ed1ab_0.conda + python: '>=3.6' + importlib-metadata: '>=4.4' + url: https://conda.anaconda.org/conda-forge/noarch/markdown-3.5.2-pyhd8ed1ab_0.conda hash: - md5: 24f0c263af3783651f77b021a6ad97a8 - sha256: b5530371b54677cb72d93ae091052327b2c809a6066d85366af4f3f0728888eb + md5: db7b48fa4eeb0c21b2f3f5b1f7d9ebcf + sha256: fbc70dc01b361fe46e7b4e102e725f99ba60bf9903c2fa86b53ad6b70ded677a category: main optional: false -- name: pre-commit - version: 3.6.0 +- name: markdown-it-py + version: 3.0.0 manager: conda platform: linux-64 dependencies: - cfgv: '>=2.0.0' - identify: '>=1.0.0' - nodeenv: '>=0.11.1' - python: '>=3.9' - pyyaml: '>=5.1' - virtualenv: '>=20.10.0' - url: https://conda.anaconda.org/conda-forge/noarch/pre-commit-3.6.0-pyha770c72_0.conda + mdurl: '>=0.1,<1' + python: '>=3.8' + url: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-3.0.0-pyhd8ed1ab_0.conda hash: - md5: 473a7cfca197da0a10cff3f6dded7d4b - sha256: 7d1f4b4a2eb4946b5808769642c5f643788c3a9e090f1c02a6c63f8794fb3d54 + md5: 93a8e71256479c62074356ef6ebf501b + sha256: c041b0eaf7a6af3344d5dd452815cdc148d6284fec25a4fa3f4263b3a021e962 category: main optional: false -- name: requests-oauthlib - version: 1.3.1 +- name: markdown-it-py + version: 3.0.0 manager: conda - platform: linux-64 + platform: osx-64 dependencies: - oauthlib: '>=3.0.0' - python: '>=3.4' - requests: '>=2.0.0' - url: https://conda.anaconda.org/conda-forge/noarch/requests-oauthlib-1.3.1-pyhd8ed1ab_0.tar.bz2 + python: '>=3.8' + mdurl: '>=0.1,<1' + url: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-3.0.0-pyhd8ed1ab_0.conda hash: - md5: 61b279f051eef9c89d58f4d813e75e04 - sha256: 889e3c1b84467b64046776db95dc4c5ea4dad5afaa5ec18ad811bd95c63286b0 + md5: 93a8e71256479c62074356ef6ebf501b + sha256: c041b0eaf7a6af3344d5dd452815cdc148d6284fec25a4fa3f4263b3a021e962 category: main optional: false -- name: kombu - version: 5.3.4 +- name: markupsafe + version: 2.1.5 manager: conda platform: linux-64 dependencies: - amqp: '>=5.1.1,<6.0.0' - boto3: '>=1.26.143' + libgcc-ng: '>=12' python: '>=3.11,<3.12.0a0' python_abi: 3.11.* - vine: '' - url: https://conda.anaconda.org/conda-forge/linux-64/kombu-5.3.4-py311h38be061_0.conda + url: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-2.1.5-py311h459d7ec_0.conda hash: - md5: f087b47105e900808597549e7eaf4237 - sha256: 7c49b1464cfe67f79f083eadb578ebe5f5facda153be0fbfa1a24938f30476be + md5: a322b4185121935c871d201ae00ac143 + sha256: 14912e557a6576e03f65991be89e9d289c6e301921b6ecfb4e7186ba974f453d category: main optional: false -- name: celery - version: 5.3.4 +- name: markupsafe + version: 2.1.5 manager: conda - platform: linux-64 + platform: osx-64 dependencies: - backports.zoneinfo: '>=0.2.1' - billiard: '>=4.1.0,<5.0' - click: '>=8.1.2,<9.0' - click-didyoumean: '>=0.3.0' - click-plugins: '>=1.1.1' - click-repl: '>=0.2.0' - importlib-metadata: '>=3.6' - kombu: '>=5.3.2,<6.0' - python: '>=3.7' - python-dateutil: '>=2.8.2' - python-tzdata: '>=2022.7' - pytz: '>=2021.3' - vine: '>=5.0.0,<6.0' - url: https://conda.anaconda.org/conda-forge/noarch/celery-5.3.4-pyhd8ed1ab_1.conda + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + url: https://conda.anaconda.org/conda-forge/osx-64/markupsafe-2.1.5-py311he705e18_0.conda hash: - md5: 64a66acef34f7cca60b929b8228951f1 - sha256: b93109413da956e6e2a37b0a852ccb15a45f706c7ce718f9ea094c660b18b5e3 + md5: 75abe7e2e3a0874a49d7c175115f443f + sha256: 83a2b764a4946a04e693a4dd8fe5a35bf093a378da9ce18bf0689cd5dcb3c3fe category: main optional: false -- name: flask - version: 2.2.5 +- name: mdurl + version: 0.1.2 manager: conda platform: linux-64 dependencies: - celery: '>=5.2.7' - click: '>=8.0' - importlib-metadata: '>=3.6.0' - itsdangerous: '>=2.0' - jinja2: '>=3.0' - python: '>=3.7' - werkzeug: '>=2.2.2' - url: https://conda.anaconda.org/conda-forge/noarch/flask-2.2.5-pyhd8ed1ab_0.conda + python: '>=3.6' + url: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_0.conda hash: - md5: 66c558a28b9b47c1e2aa8e8cb80b9037 - sha256: 9356ae47278788439770eb9d05bfeb72c39a53a8b10a0e534b612f99b9c18e9b + md5: 776a8dd9e824f77abac30e6ef43a8f7a + sha256: 64073dfb6bb429d52fff30891877b48c7ec0f89625b1bf844905b66a81cce6e1 category: main optional: false -- name: flask-dance - version: 7.0.0 +- name: mdurl + version: 0.1.2 manager: conda - platform: linux-64 + platform: osx-64 dependencies: - flask: '>=2.0.3' - oauthlib: '>=3.2' - python: '>=3.7' - requests: '>=2.0' - requests-oauthlib: '>=1.0.0' - urlobject: '' - werkzeug: '' - url: https://conda.anaconda.org/conda-forge/noarch/flask-dance-7.0.0-pyhd8ed1ab_0.conda + python: '>=3.6' + url: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_0.conda hash: - md5: a0007a17aa03b39c941f921f082afe9e - sha256: 7f3b75ba7f321c9058e0adbd4113fe3bfad3fc28ea266fb42f7636091cd4ca34 + md5: 776a8dd9e824f77abac30e6ef43a8f7a + sha256: 64073dfb6bb429d52fff30891877b48c7ec0f89625b1bf844905b66a81cce6e1 category: main optional: false -- name: flask-login - version: 0.6.3 +- name: mypy + version: 1.7.1 manager: conda platform: linux-64 dependencies: - flask: '>=1.0.4' - python: '>=3.7' - werkzeug: '>=1.0.1' - url: https://conda.anaconda.org/conda-forge/noarch/flask-login-0.6.3-pyhd8ed1ab_1.conda + libgcc-ng: '>=12' + mypy_extensions: '>=1.0.0' + psutil: '>=4.0' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + typing_extensions: '>=4.1.0' + url: https://conda.anaconda.org/conda-forge/linux-64/mypy-1.7.1-py311h459d7ec_1.conda hash: - md5: 1d3523e7b18b38e4be992bf462d99e40 - sha256: 23ffed52801132e3e434c82a544ef1b77efbc025878a1a1cd0fbcc4c361cd9de + md5: 0a861b29266afe8c315ecdcb92f9e692 + sha256: 8c9365ecf8e56666253f987ba83d1dcde141ef3d0f73300a92adf5aa63c8e5bd category: main optional: false -- name: flask-sqlalchemy - version: 3.0.3 +- name: mypy + version: 1.7.1 manager: conda - platform: linux-64 + platform: osx-64 dependencies: - flask: '>=2.2' - python: '>=3.7' - sqlalchemy: '>=1.4.18' - url: https://conda.anaconda.org/conda-forge/noarch/flask-sqlalchemy-3.0.3-pyhd8ed1ab_0.conda + mypy_extensions: '>=1.0.0' + psutil: '>=4.0' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + typing_extensions: '>=4.1.0' + url: https://conda.anaconda.org/conda-forge/osx-64/mypy-1.7.1-py311he705e18_1.conda hash: - md5: 6a301e2aa76aef91787e15f59ab03390 - sha256: 460693607ab0e2feb600bb755851c0c19db8d6dbd1c7636904c8de6f2c63040d + md5: 153e0baec2f2d6c0bb6c9692f623ecdb + sha256: f9c25d9de38057167a10d4873e8ad7bcb9b9a8b5d1d3195421dac3445fb16b26 category: main optional: false -- name: flask-wtf - version: 1.2.1 +- name: mypy_extensions + version: 1.0.0 manager: conda platform: linux-64 dependencies: - flask: '' - itsdangerous: '' - python: '>=3.8' - wtforms: '' - url: https://conda.anaconda.org/conda-forge/noarch/flask-wtf-1.2.1-pyhd8ed1ab_0.conda - hash: - md5: e7b75c25f98d7b55943466683e400529 - sha256: cee583240eec93d27f262f3bb8cdecf8334e502d81923295d98f378b15fe26d5 - category: main - optional: false -- name: bootstrap-flask - version: 2.3.3 - manager: pip - platform: linux-64 - dependencies: - flask: '*' - wtforms: '*' - url: https://files.pythonhosted.org/packages/95/e4/7f3dd2dba1ef70ddbf1bf6a58ea8245c20b2d44bd81e8b37215e812f5d0e/Bootstrap_Flask-2.3.3-py2.py3-none-any.whl - hash: - sha256: 4c3d1f1b8ee19081dfc80e03aba5cb19cd92a8d54ba0ba98cee69acffded91d0 - category: main - optional: false -- name: wtforms-sqlalchemy - version: '0.3' - manager: pip - platform: linux-64 - dependencies: - wtforms: '>=1.0.5' - sqlalchemy: '>=0.7.10' - url: https://files.pythonhosted.org/packages/62/71/a5624ff63f4024d27489263b56e3ab6a5122087eb432d14617cfed90ec3d/WTForms_SQLAlchemy-0.3-py3-none-any.whl - hash: - sha256: 90195d7592bf256d82498c42c79d416832e4a4e6fbca4f1e745a018f66d26c47 - category: main - optional: false -- name: bzip2 - version: 1.0.8 - manager: conda - platform: osx-64 - dependencies: {} - url: https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-h10d778d_5.conda - hash: - md5: 6097a6ca9ada32699b5fc4312dd6ef18 - sha256: 61fb2b488928a54d9472113e1280b468a309561caa54f33825a3593da390b242 - category: main - optional: false -- name: ca-certificates - version: 2023.11.17 - manager: conda - platform: osx-64 - dependencies: {} - url: https://conda.anaconda.org/conda-forge/osx-64/ca-certificates-2023.11.17-h8857fd0_0.conda - hash: - md5: c687e9d14c49e3d3946d50a413cdbf16 - sha256: 7e05d80a97beb7cb7492fae38584a68d51f338a5eddf73a14b5bd266597db90e - category: main - optional: false -- name: libcxx - version: 16.0.6 - manager: conda - platform: osx-64 - dependencies: {} - url: https://conda.anaconda.org/conda-forge/osx-64/libcxx-16.0.6-hd57cbcb_0.conda - hash: - md5: 7d6972792161077908b62971802f289a - sha256: 9063271847cf05f3a6cc6cae3e7f0ced032ab5f3a3c9d3f943f876f39c5c2549 - category: main - optional: false -- name: libexpat - version: 2.5.0 - manager: conda - platform: osx-64 - dependencies: {} - url: https://conda.anaconda.org/conda-forge/osx-64/libexpat-2.5.0-hf0c8a7f_1.conda - hash: - md5: 6c81cb022780ee33435cca0127dd43c9 - sha256: 80024bd9f44d096c4cc07fb2bac76b5f1f7553390112dab3ad6acb16a05f0b96 - category: main - optional: false -- name: libffi - version: 3.4.2 - manager: conda - platform: osx-64 - dependencies: {} - url: https://conda.anaconda.org/conda-forge/osx-64/libffi-3.4.2-h0d85af4_5.tar.bz2 - hash: - md5: ccb34fb14960ad8b125962d3d79b31a9 - sha256: 7a2d27a936ceee6942ea4d397f9c7d136f12549d86f7617e8b6bad51e01a941f - category: main - optional: false -- name: libzlib - version: 1.2.13 - manager: conda - platform: osx-64 - dependencies: {} - url: https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.2.13-h8a1eda9_5.conda - hash: - md5: 4a3ad23f6e16f99c04e166767193d700 - sha256: fc58ad7f47ffea10df1f2165369978fba0a1cc32594aad778f5eec725f334867 - category: main - optional: false -- name: python_abi - version: '3.11' - manager: conda - platform: osx-64 - dependencies: {} - url: https://conda.anaconda.org/conda-forge/osx-64/python_abi-3.11-4_cp311.conda - hash: - md5: fef7a52f0eca6bae9e8e2e255bc86394 - sha256: f56dfe2a57b3b27bad3f9527f943548e8b2526e949d9d6fc0a383020d9359afe - category: main - optional: false -- name: tzdata - version: 2023c - manager: conda - platform: osx-64 - dependencies: {} - url: https://conda.anaconda.org/conda-forge/noarch/tzdata-2023c-h71feb2d_0.conda - hash: - md5: 939e3e74d8be4dac89ce83b20de2492a - sha256: 0449138224adfa125b220154408419ec37c06b0b49f63c5954724325903ecf55 - category: main - optional: false -- name: xz - version: 5.2.6 - manager: conda - platform: osx-64 - dependencies: {} - url: https://conda.anaconda.org/conda-forge/osx-64/xz-5.2.6-h775f41a_0.tar.bz2 - hash: - md5: a72f9d4ea13d55d745ff1ed594747f10 - sha256: eb09823f34cc2dd663c0ec4ab13f246f45dcd52e5b8c47b9864361de5204a1c8 - category: main - optional: false -- name: yaml - version: 0.2.5 - manager: conda - platform: osx-64 - dependencies: {} - url: https://conda.anaconda.org/conda-forge/osx-64/yaml-0.2.5-h0d85af4_2.tar.bz2 + python: '>=3.5' + url: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.0.0-pyha770c72_0.conda hash: - md5: d7e08fcf8259d742156188e8762b4d20 - sha256: 5301417e2c8dea45b401ffee8df3957d2447d4ce80c83c5ff151fc6bfe1c4148 + md5: 4eccaeba205f0aed9ac3a9ea58568ca3 + sha256: f240217476e148e825420c6bc3a0c0efb08c0718b7042fae960400c02af858a3 category: main optional: false -- name: gmp - version: 6.3.0 +- name: mypy_extensions + version: 1.0.0 manager: conda platform: osx-64 dependencies: - __osx: '>=10.9' - libcxx: '>=16.0.6' - url: https://conda.anaconda.org/conda-forge/osx-64/gmp-6.3.0-h93d8f39_0.conda + python: '>=3.5' + url: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.0.0-pyha770c72_0.conda hash: - md5: a4ffd4bfd88659cbecbd36b61594bf0d - sha256: 49443e6c41070e3967936c7f09b7686d3dd715f3351918c4edfd8072e1776013 + md5: 4eccaeba205f0aed9ac3a9ea58568ca3 + sha256: f240217476e148e825420c6bc3a0c0efb08c0718b7042fae960400c02af858a3 category: main optional: false -- name: libsqlite - version: 3.44.2 +- name: ncurses + version: '6.4' manager: conda - platform: osx-64 + platform: linux-64 dependencies: - libzlib: '>=1.2.13,<1.3.0a0' - url: https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.44.2-h92b6c6a_0.conda + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.4-h59595ed_2.conda hash: - md5: d4419f90019e6a2b152cd4d32f73a82f - sha256: 8a317d2aa6352feba951ca09d5bf34f565f9dd10bb14ff842b8650baa321d781 + md5: 7dbaa197d7ba6032caf7ae7f32c1efa0 + sha256: 91cc03f14caf96243cead96c76fe91ab5925a695d892e83285461fb927dece5e category: main optional: false - name: ncurses @@ -1829,448 +1655,391 @@ package: sha256: ea0fca66bbb52a1ef0687d466518fe120b5f279684effd6fd336a7b0dddc423a category: main optional: false -- name: openssl - version: 3.2.0 - manager: conda - platform: osx-64 - dependencies: - ca-certificates: '' - url: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.2.0-hd75f5a5_1.conda - hash: - md5: 06cb561619487c88891839b9beb5244c - sha256: 99161bf349f5dc80322f2a2c188588d11efa662566e4e19f2ac0a36d9fa3de25 - category: main - optional: false -- name: tk - version: 8.6.13 - manager: conda - platform: osx-64 - dependencies: - libzlib: '>=1.2.13,<1.3.0a0' - url: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-h1abcd95_1.conda - hash: - md5: bf830ba5afc507c6232d4ef0fb1a882d - sha256: 30412b2e9de4ff82d8c2a7e5d06a15f4f4fef1809a72138b6ccb53a33b26faf5 - category: main - optional: false -- name: libedit - version: 3.1.20191231 - manager: conda - platform: osx-64 - dependencies: - ncurses: '>=6.2,<7.0.0a0' - url: https://conda.anaconda.org/conda-forge/osx-64/libedit-3.1.20191231-h0678c8f_2.tar.bz2 - hash: - md5: 6016a8a1d0e63cac3de2c352cd40208b - sha256: dbd3c3f2eca1d21c52e4c03b21930bbce414c4592f8ce805801575b9e9256095 - category: main - optional: false -- name: readline - version: '8.2' +- name: nodeenv + version: 1.8.0 manager: conda - platform: osx-64 + platform: linux-64 dependencies: - ncurses: '>=6.3,<7.0a0' - url: https://conda.anaconda.org/conda-forge/osx-64/readline-8.2-h9e318b2_1.conda + python: 2.7|>=3.7 + setuptools: '' + url: https://conda.anaconda.org/conda-forge/noarch/nodeenv-1.8.0-pyhd8ed1ab_0.conda hash: - md5: f17f77f2acf4d344734bda76829ce14e - sha256: 41e7d30a097d9b060037f0c6a2b1d4c4ae7e942c06c943d23f9d481548478568 + md5: 2a75b296096adabbabadd5e9782e5fcc + sha256: 1320306234552717149f36f825ddc7e27ea295f24829e9db4cc6ceaff0b032bd category: main optional: false -- name: shellcheck - version: 0.8.0 +- name: nodeenv + version: 1.8.0 manager: conda platform: osx-64 dependencies: - gmp: '>=6.2.1,<7.0a0' - url: https://conda.anaconda.org/conda-forge/osx-64/shellcheck-0.8.0-h7dd6a17_0.tar.bz2 + setuptools: '' + python: 2.7|>=3.7 + url: https://conda.anaconda.org/conda-forge/noarch/nodeenv-1.8.0-pyhd8ed1ab_0.conda hash: - md5: b5a30a0ff9f7b85fe4e3195bc12d4b66 - sha256: 9ae08b408c7c352be4dd927b50eb7c2b11d9053409f21c8ed608ac04636edcf6 + md5: 2a75b296096adabbabadd5e9782e5fcc + sha256: 1320306234552717149f36f825ddc7e27ea295f24829e9db4cc6ceaff0b032bd category: main optional: false -- name: krb5 - version: 1.21.2 +- name: oauthlib + version: 3.2.2 manager: conda - platform: osx-64 + platform: linux-64 dependencies: - libcxx: '>=15.0.7' - libedit: '>=3.1.20191231,<4.0a0' - openssl: '>=3.1.2,<4.0a0' - url: https://conda.anaconda.org/conda-forge/osx-64/krb5-1.21.2-hb884880_0.conda + blinker: '' + cryptography: '' + pyjwt: '>=1.0.0' + python: '>=3.6' + url: https://conda.anaconda.org/conda-forge/noarch/oauthlib-3.2.2-pyhd8ed1ab_0.tar.bz2 hash: - md5: 80505a68783f01dc8d7308c075261b2f - sha256: 081ae2008a21edf57c048f331a17c65d1ccb52d6ca2f87ee031a73eff4dc0fc6 + md5: 8f882b197fd9c4941a787926baea4868 + sha256: 0cfd5146a91d3974f4abfc2a45de890371d510a77238fe553e036ec8c031dc5b category: main optional: false -- name: python - version: 3.11.7 +- name: oauthlib + version: 3.2.2 manager: conda platform: osx-64 dependencies: - bzip2: '>=1.0.8,<2.0a0' - libexpat: '>=2.5.0,<3.0a0' - libffi: '>=3.4,<4.0a0' - libsqlite: '>=3.44.2,<4.0a0' - libzlib: '>=1.2.13,<1.3.0a0' - ncurses: '>=6.4,<7.0a0' - openssl: '>=3.2.0,<4.0a0' - readline: '>=8.2,<9.0a0' - tk: '>=8.6.13,<8.7.0a0' - tzdata: '' - xz: '>=5.2.6,<6.0a0' - url: https://conda.anaconda.org/conda-forge/osx-64/python-3.11.7-h9f0c242_0_cpython.conda + cryptography: '' + blinker: '' + python: '>=3.6' + pyjwt: '>=1.0.0' + url: https://conda.anaconda.org/conda-forge/noarch/oauthlib-3.2.2-pyhd8ed1ab_0.tar.bz2 hash: - md5: 0c5df462c5510f1cc3fae7ba627d63d2 - sha256: 2ad1b68caa61682c6a893b0474f45354cda45e38185945e6b439aa18dff6e9fd + md5: 8f882b197fd9c4941a787926baea4868 + sha256: 0cfd5146a91d3974f4abfc2a45de890371d510a77238fe553e036ec8c031dc5b category: main optional: false -- name: backports.zoneinfo - version: 0.2.1 +- name: openssl + version: 3.2.1 manager: conda - platform: osx-64 + platform: linux-64 dependencies: - python: '>=3.11,<3.12.0a0' - python_abi: 3.11.* - url: https://conda.anaconda.org/conda-forge/osx-64/backports.zoneinfo-0.2.1-py311h6eed73b_8.conda + ca-certificates: '' + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.2.1-hd590300_0.conda hash: - md5: 82f37234dbc0254423c109e9e21ce332 - sha256: f6064fc69833fed6d02738d29132bc87a6195098ec74257f53044de306694ff3 + md5: 51a753e64a3027bd7e23a189b1f6e91e + sha256: c02c12bdb898daacf7eb3d09859f93ea8f285fd1a6132ff6ff0493ab52c7fe57 category: main optional: false -- name: billiard - version: 4.1.0 +- name: openssl + version: 3.2.1 manager: conda platform: osx-64 dependencies: - python: '>=3.11,<3.12.0a0' - python_abi: 3.11.* - url: https://conda.anaconda.org/conda-forge/osx-64/billiard-4.1.0-py311h2725bcf_1.conda + ca-certificates: '' + url: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.2.1-hd75f5a5_0.conda hash: - md5: 60ad2305d46ca7281d2a9a05dabf194f - sha256: 3a8f8c8423cc3148c694084d78270a2dd32090de9e22d003d4fba0e9386a5b73 + md5: 3033be9a59fd744172b03971b9ccd081 + sha256: 20c1b1a34a1831c24d37ed1500ca07300171184af0c66598f3c5ca901634d713 category: main optional: false -- name: blinker - version: 1.7.0 +- name: packaging + version: '24.0' manager: conda - platform: osx-64 + platform: linux-64 dependencies: python: '>=3.8' - url: https://conda.anaconda.org/conda-forge/noarch/blinker-1.7.0-pyhd8ed1ab_0.conda + url: https://conda.anaconda.org/conda-forge/noarch/packaging-24.0-pyhd8ed1ab_0.conda hash: - md5: 550da20b2c2e38be9cc44bb819fda5d5 - sha256: c8d72c2af4f57898dfd5e4c62ae67f7fea1490a37c8b6855460a170d61591177 + md5: 248f521b64ce055e7feae3105e7abeb8 + sha256: a390182d74c31dfd713c16db888c92c277feeb6d1fe96ff9d9c105f9564be48a category: main optional: false -- name: brotli-python - version: 1.1.0 +- name: packaging + version: '24.0' manager: conda platform: osx-64 dependencies: - libcxx: '>=15.0.7' - python: '>=3.11,<3.12.0a0' - python_abi: 3.11.* - url: https://conda.anaconda.org/conda-forge/osx-64/brotli-python-1.1.0-py311hdf8f085_1.conda + python: '>=3.8' + url: https://conda.anaconda.org/conda-forge/noarch/packaging-24.0-pyhd8ed1ab_0.conda hash: - md5: 546fdccabb90492fbaf2da4ffb78f352 - sha256: 0f5e0a7de58006f349220365e32db521a1fe494c37ee455e5ecf05b8fe567dcc + md5: 248f521b64ce055e7feae3105e7abeb8 + sha256: a390182d74c31dfd713c16db888c92c277feeb6d1fe96ff9d9c105f9564be48a category: main optional: false -- name: certifi - version: 2023.11.17 +- name: pip + version: '24.0' manager: conda - platform: osx-64 + platform: linux-64 dependencies: python: '>=3.7' - url: https://conda.anaconda.org/conda-forge/noarch/certifi-2023.11.17-pyhd8ed1ab_0.conda - hash: - md5: 2011bcf45376341dd1d690263fdbc789 - sha256: afa22b77128a812cb57bc707c297d926561bd225a3d9dd74205d87a3b2d14a96 - category: main - optional: false -- name: cfgv - version: 3.3.1 - manager: conda - platform: osx-64 - dependencies: - python: '>=3.6.1' - url: https://conda.anaconda.org/conda-forge/noarch/cfgv-3.3.1-pyhd8ed1ab_0.tar.bz2 + setuptools: '' + wheel: '' + url: https://conda.anaconda.org/conda-forge/noarch/pip-24.0-pyhd8ed1ab_0.conda hash: - md5: ebb5f5f7dc4f1a3780ef7ea7738db08c - sha256: fbc03537a27ef756162c49b1d0608bf7ab12fa5e38ceb8563d6f4859e835ac5c + md5: f586ac1e56c8638b64f9c8122a7b8a67 + sha256: b7c1c5d8f13e8cb491c4bd1d0d1896a4cf80fc47de01059ad77509112b664a4a category: main optional: false -- name: charset-normalizer - version: 3.3.2 +- name: pip + version: '24.0' manager: conda platform: osx-64 dependencies: + setuptools: '' + wheel: '' python: '>=3.7' - url: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.3.2-pyhd8ed1ab_0.conda + url: https://conda.anaconda.org/conda-forge/noarch/pip-24.0-pyhd8ed1ab_0.conda hash: - md5: 7f4a9e3fcff3f6356ae99244a014da6a - sha256: 20cae47d31fdd58d99c4d2e65fbdcefa0b0de0c84e455ba9d6356a4bdbc4b5b9 + md5: f586ac1e56c8638b64f9c8122a7b8a67 + sha256: b7c1c5d8f13e8cb491c4bd1d0d1896a4cf80fc47de01059ad77509112b664a4a category: main optional: false -- name: click - version: 8.1.7 +- name: platformdirs + version: 4.2.0 manager: conda - platform: osx-64 + platform: linux-64 dependencies: - __unix: '' python: '>=3.8' - url: https://conda.anaconda.org/conda-forge/noarch/click-8.1.7-unix_pyh707e725_0.conda - hash: - md5: f3ad426304898027fc619827ff428eca - sha256: f0016cbab6ac4138a429e28dbcb904a90305b34b3fe41a9b89d697c90401caec - category: main - optional: false -- name: colorama - version: 0.4.6 - manager: conda - platform: osx-64 - dependencies: - python: '>=3.7' - url: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_0.tar.bz2 + url: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.2.0-pyhd8ed1ab_0.conda hash: - md5: 3faab06a954c2a04039983f2c4a50d99 - sha256: 2c1b2e9755ce3102bca8d69e8f26e4f087ece73f50418186aee7c74bef8e1698 - category: main - optional: false -- name: distlib - version: 0.3.8 - manager: conda - platform: osx-64 - dependencies: - python: 2.7|>=3.6 - url: https://conda.anaconda.org/conda-forge/noarch/distlib-0.3.8-pyhd8ed1ab_0.conda - hash: - md5: db16c66b759a64dc5183d69cc3745a52 - sha256: 3ff11acdd5cc2f80227682966916e878e45ced94f59c402efb94911a5774e84e + md5: a0bc3eec34b0fab84be6b2da94e98e20 + sha256: 2ebfb971236ab825dd79dd6086ea742a9901008ffb9c6222c1f2b5172a8039d3 category: main optional: false -- name: exceptiongroup - version: 1.2.0 +- name: platformdirs + version: 4.2.0 manager: conda platform: osx-64 dependencies: - python: '>=3.7' - url: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.0-pyhd8ed1ab_0.conda + python: '>=3.8' + url: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.2.0-pyhd8ed1ab_0.conda hash: - md5: f6c211fee3c98229652b60a9a42ef363 - sha256: cf83dcaf9006015c8ccab3fc6770f478464a66a8769e1763ca5d7dff09d11d08 + md5: a0bc3eec34b0fab84be6b2da94e98e20 + sha256: 2ebfb971236ab825dd79dd6086ea742a9901008ffb9c6222c1f2b5172a8039d3 category: main optional: false -- name: filelock - version: 3.13.1 +- name: pluggy + version: 1.4.0 manager: conda - platform: osx-64 + platform: linux-64 dependencies: - python: '>=3.7' - url: https://conda.anaconda.org/conda-forge/noarch/filelock-3.13.1-pyhd8ed1ab_0.conda + python: '>=3.8' + url: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.4.0-pyhd8ed1ab_0.conda hash: - md5: 0c1729b74a8152fde6a38ba0a2ab9f45 - sha256: 4d742d91412d1f163e5399d2b50c5d479694ebcd309127abb549ca3977f89d2b + md5: 139e9feb65187e916162917bb2484976 + sha256: 6edfd2c41938ea772096c674809bfcf2ebb9bef7e82de6c7ea0b966b86bfb4d0 category: main optional: false -- name: greenlet - version: 3.0.2 +- name: pluggy + version: 1.4.0 manager: conda platform: osx-64 dependencies: - __osx: '>=10.9' - libcxx: '>=16.0.6' - python: '>=3.11,<3.12.0a0' - python_abi: 3.11.* - url: https://conda.anaconda.org/conda-forge/osx-64/greenlet-3.0.2-py311hd39e593_0.conda + python: '>=3.8' + url: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.4.0-pyhd8ed1ab_0.conda hash: - md5: 6005605b11c7db35cd5d4d04e645ecd3 - sha256: 1120b061eb3216f3c2fd4db8a2150c5185742941c1a66c1e05a8bb26bfa9d026 + md5: 139e9feb65187e916162917bb2484976 + sha256: 6edfd2c41938ea772096c674809bfcf2ebb9bef7e82de6c7ea0b966b86bfb4d0 category: main optional: false -- name: idna - version: '3.6' +- name: pre-commit + version: 3.6.2 manager: conda - platform: osx-64 + platform: linux-64 dependencies: - python: '>=3.6' - url: https://conda.anaconda.org/conda-forge/noarch/idna-3.6-pyhd8ed1ab_0.conda + cfgv: '>=2.0.0' + identify: '>=1.0.0' + nodeenv: '>=0.11.1' + python: '>=3.9' + pyyaml: '>=5.1' + virtualenv: '>=20.10.0' + url: https://conda.anaconda.org/conda-forge/noarch/pre-commit-3.6.2-pyha770c72_0.conda hash: - md5: 1a76f09108576397c41c0b0c5bd84134 - sha256: 6ee4c986d69ce61e60a20b2459b6f2027baeba153f0a64995fd3cb47c2cc7e07 + md5: 61534ee57ffdf26d7b1b514d33daccc4 + sha256: 8eb9f5965c37d2bbee9302e16cc7c5517ee06491986356112be13431a043681e category: main optional: false -- name: iniconfig - version: 2.0.0 +- name: pre-commit + version: 3.6.2 manager: conda platform: osx-64 dependencies: - python: '>=3.7' - url: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_0.conda + python: '>=3.9' + pyyaml: '>=5.1' + identify: '>=1.0.0' + nodeenv: '>=0.11.1' + cfgv: '>=2.0.0' + virtualenv: '>=20.10.0' + url: https://conda.anaconda.org/conda-forge/noarch/pre-commit-3.6.2-pyha770c72_0.conda hash: - md5: f800d2da156d08e289b14e87e43c1ae5 - sha256: 38740c939b668b36a50ef455b077e8015b8c9cf89860d421b3fff86048f49666 + md5: 61534ee57ffdf26d7b1b514d33daccc4 + sha256: 8eb9f5965c37d2bbee9302e16cc7c5517ee06491986356112be13431a043681e category: main optional: false -- name: invoke - version: 2.0.0 +- name: prompt-toolkit + version: 3.0.42 manager: conda - platform: osx-64 + platform: linux-64 dependencies: - python: '>=3.6' - url: https://conda.anaconda.org/conda-forge/noarch/invoke-2.0.0-pyhd8ed1ab_0.conda + python: '>=3.7' + wcwidth: '' + url: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.42-pyha770c72_0.conda hash: - md5: c282e4567f7b800a855f7092b3c44a5a - sha256: 637e51d8df0159bae9d67f840a24a5fc7973d48fac09418b01e2fb6f3c03224b + md5: 0bf64bf10eee21f46ac83c161917fa86 + sha256: 58525b2a9305fb154b2b0d43a48b9a6495441b80e4fbea44f2a34a597d2cef16 category: main optional: false -- name: itsdangerous - version: 2.1.2 +- name: prompt-toolkit + version: 3.0.42 manager: conda platform: osx-64 dependencies: + wcwidth: '' python: '>=3.7' - url: https://conda.anaconda.org/conda-forge/noarch/itsdangerous-2.1.2-pyhd8ed1ab_0.tar.bz2 + url: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.42-pyha770c72_0.conda hash: - md5: 3c3de74912f11d2b590184f03c7cd09b - sha256: 31e3492686b4e92b53db9b48bc0eb03873b1caaf28629fee7d2d47627a2c56d3 + md5: 0bf64bf10eee21f46ac83c161917fa86 + sha256: 58525b2a9305fb154b2b0d43a48b9a6495441b80e4fbea44f2a34a597d2cef16 category: main optional: false -- name: jmespath - version: 1.0.1 +- name: prompt_toolkit + version: 3.0.42 manager: conda - platform: osx-64 + platform: linux-64 dependencies: - python: '>=3.7' - url: https://conda.anaconda.org/conda-forge/noarch/jmespath-1.0.1-pyhd8ed1ab_0.tar.bz2 + prompt-toolkit: '>=3.0.42,<3.0.43.0a0' + url: https://conda.anaconda.org/conda-forge/noarch/prompt_toolkit-3.0.42-hd8ed1ab_0.conda hash: - md5: 2cfa3e1cf3fb51bb9b17acc5b5e9ea11 - sha256: 95ac5f9ee95fd4e34dc051746fc86016d3d4f6abefed113e2ede049d59ec2991 + md5: 85a2189ecd2fcdd86e92b2d4ea8fe461 + sha256: fd2185d501bf34cb4c121f2f5ade9157ac75e1644a9da81355c4c8f9c1b82d4d category: main optional: false -- name: libpq - version: '16.1' +- name: prompt_toolkit + version: 3.0.42 manager: conda platform: osx-64 dependencies: - krb5: '>=1.21.2,<1.22.0a0' - openssl: '>=3.2.0,<4.0a0' - url: https://conda.anaconda.org/conda-forge/osx-64/libpq-16.1-ha925e61_7.conda + prompt-toolkit: '>=3.0.42,<3.0.43.0a0' + url: https://conda.anaconda.org/conda-forge/noarch/prompt_toolkit-3.0.42-hd8ed1ab_0.conda hash: - md5: ad8be4619f57504e8c243e00ab315ad6 - sha256: addce7453b4ac80faf46ecec76db74d98f57d9711624df600399c56e147b7903 + md5: 85a2189ecd2fcdd86e92b2d4ea8fe461 + sha256: fd2185d501bf34cb4c121f2f5ade9157ac75e1644a9da81355c4c8f9c1b82d4d category: main optional: false -- name: loguru - version: 0.6.0 +- name: psutil + version: 5.9.8 manager: conda - platform: osx-64 + platform: linux-64 dependencies: + libgcc-ng: '>=12' python: '>=3.11,<3.12.0a0' python_abi: 3.11.* - url: https://conda.anaconda.org/conda-forge/osx-64/loguru-0.6.0-py311h6eed73b_2.tar.bz2 + url: https://conda.anaconda.org/conda-forge/linux-64/psutil-5.9.8-py311h459d7ec_0.conda hash: - md5: fc9dde292b3ec1251e63aac5434cc47c - sha256: f74d200ffa50c455fbe631ca6564fe5b80c64ab314ec832528cfcdfad2a119d7 + md5: 9bc62d25dcf64eec484974a3123c9d57 + sha256: 467788418a2c71fb3df9ac0a6282ae693d1070a6cb47cb59bdb529b53acaee1c category: main optional: false -- name: markupsafe - version: 2.1.3 +- name: psutil + version: 5.9.8 manager: conda platform: osx-64 dependencies: python: '>=3.11,<3.12.0a0' python_abi: 3.11.* - url: https://conda.anaconda.org/conda-forge/osx-64/markupsafe-2.1.3-py311h2725bcf_1.conda + url: https://conda.anaconda.org/conda-forge/osx-64/psutil-5.9.8-py311he705e18_0.conda hash: - md5: 52ee86f482b552e547e2b1d6c01adf55 - sha256: 5a8f8caa89eeba6ea6e9e96d3e7c109b675bc3c6ed4b109b8931757da2411d48 + md5: 31aa294c58b3058c179a7a9593e99e18 + sha256: fcff83f4d265294b54821656a10be62421da377885ab2e9811a80eb76419b3fe category: main optional: false -- name: mdurl - version: 0.1.0 +- name: psycopg2 + version: 2.9.9 manager: conda - platform: osx-64 + platform: linux-64 dependencies: - python: '>=3.6' - url: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.0-pyhd8ed1ab_0.tar.bz2 + libgcc-ng: '>=12' + libpq: '>=16.1,<17.0a0' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + url: https://conda.anaconda.org/conda-forge/linux-64/psycopg2-2.9.9-py311h03dec38_0.conda hash: - md5: f8dab71fdc13b1bf29a01248b156d268 - sha256: c678b9194e025b1fb665bec30ee20aab93399203583875b1dcc0a3b52a8f5523 + md5: 3cc2decd316838bce14d73818e0bf7a4 + sha256: 4e78d9fe1799d028d9a2da3636a3a68a531aeca5d2c679d4fc78627a426b11cb category: main optional: false -- name: mypy_extensions - version: 1.0.0 +- name: psycopg2 + version: 2.9.9 manager: conda platform: osx-64 dependencies: - python: '>=3.5' - url: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.0.0-pyha770c72_0.conda + libpq: '>=16.1,<17.0a0' + openssl: '>=3.2.0,<4.0a0' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + url: https://conda.anaconda.org/conda-forge/osx-64/psycopg2-2.9.9-py311h187f0af_0.conda hash: - md5: 4eccaeba205f0aed9ac3a9ea58568ca3 - sha256: f240217476e148e825420c6bc3a0c0efb08c0718b7042fae960400c02af858a3 + md5: 2177c8943bbf9bfc45421ecaebd5be11 + sha256: 73c0cf543b0ddd41993956969f665999f5801e027e3d3524604892baedbd2626 category: main optional: false -- name: packaging - version: '23.2' +- name: pycparser + version: '2.21' manager: conda - platform: osx-64 + platform: linux-64 dependencies: - python: '>=3.7' - url: https://conda.anaconda.org/conda-forge/noarch/packaging-23.2-pyhd8ed1ab_0.conda + python: 2.7.*|>=3.4 + url: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.21-pyhd8ed1ab_0.tar.bz2 hash: - md5: 79002079284aa895f883c6b7f3f88fd6 - sha256: 69b3ace6cca2dab9047b2c24926077d81d236bef45329d264b394001e3c3e52f + md5: 076becd9e05608f8dc72757d5f3a91ff + sha256: 74c63fd03f1f1ea2b54e8bc529fd1a600aaafb24027b738d0db87909ee3a33dc category: main optional: false -- name: platformdirs - version: 4.1.0 +- name: pycparser + version: '2.21' manager: conda platform: osx-64 dependencies: - python: '>=3.8' - url: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.1.0-pyhd8ed1ab_0.conda + python: 2.7.*|>=3.4 + url: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.21-pyhd8ed1ab_0.tar.bz2 hash: - md5: 45a5065664da0d1dfa8f8cd2eaf05ab9 - sha256: 9e4ff17ce802159ed31344eb913eaa877688226765b77947b102b42255a53853 + md5: 076becd9e05608f8dc72757d5f3a91ff + sha256: 74c63fd03f1f1ea2b54e8bc529fd1a600aaafb24027b738d0db87909ee3a33dc category: main optional: false -- name: pluggy - version: 1.3.0 +- name: pydantic + version: 1.10.13 manager: conda - platform: osx-64 + platform: linux-64 dependencies: - python: '>=3.8' - url: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.3.0-pyhd8ed1ab_0.conda + libgcc-ng: '>=12' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + typing-extensions: '>=4.2.0' + url: https://conda.anaconda.org/conda-forge/linux-64/pydantic-1.10.13-py311h459d7ec_1.conda hash: - md5: 2390bd10bed1f3fdc7a537fb5a447d8d - sha256: 7bf2ad9d747e71f1e93d0863c2c8061dd0f2fe1e582f28d292abfb40264a2eb5 + md5: 8a92f40420211897a35841861e7e8348 + sha256: f2d3a838fc90699c5dcd537aff10c78b33bd755232d0b21b26247cbf185cced7 category: main optional: false -- name: psutil - version: 5.9.7 +- name: pydantic + version: 1.10.13 manager: conda platform: osx-64 dependencies: python: '>=3.11,<3.12.0a0' python_abi: 3.11.* - url: https://conda.anaconda.org/conda-forge/osx-64/psutil-5.9.7-py311he705e18_0.conda + typing-extensions: '>=4.2.0' + url: https://conda.anaconda.org/conda-forge/osx-64/pydantic-1.10.13-py311he705e18_1.conda hash: - md5: d99e96df7c4457d2b1b83d69491f2966 - sha256: 43ab1bbe0a13828c4996ae2e115e5ca991673700075f5754cdae06b661589762 + md5: ca0cd7b41964ce9a7b80290ea85e22e9 + sha256: c55ab5f7d182421a5c11f70afc32425fa192f1e40de5c301f685b25bdc3391a8 category: main optional: false -- name: pycparser - version: '2.21' +- name: pygments + version: 2.17.2 manager: conda - platform: osx-64 + platform: linux-64 dependencies: - python: 2.7.*|>=3.4 - url: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.21-pyhd8ed1ab_0.tar.bz2 + python: '>=3.7' + url: https://conda.anaconda.org/conda-forge/noarch/pygments-2.17.2-pyhd8ed1ab_0.conda hash: - md5: 076becd9e05608f8dc72757d5f3a91ff - sha256: 74c63fd03f1f1ea2b54e8bc529fd1a600aaafb24027b738d0db87909ee3a33dc + md5: 140a7f159396547e9799aa98f9f0742e + sha256: af5f8867450dc292f98ea387d4d8945fc574284677c8f60eaa9846ede7387257 category: main optional: false - name: pygments @@ -2285,22 +2054,60 @@ package: sha256: af5f8867450dc292f98ea387d4d8945fc574284677c8f60eaa9846ede7387257 category: main optional: false +- name: pyjwt + version: 2.8.0 + manager: conda + platform: linux-64 + dependencies: + python: '>=3.7' + url: https://conda.anaconda.org/conda-forge/noarch/pyjwt-2.8.0-pyhd8ed1ab_1.conda + hash: + md5: 74f76d4868dbba5870f2cf1d9b12d8f3 + sha256: d7cb7fbafd767e938db10820c76a9c16d91faf5a081842159cc185787879eb07 + category: main + optional: false - name: pyjwt version: 2.8.0 manager: conda platform: osx-64 dependencies: - python: '>=3.6' - url: https://conda.anaconda.org/conda-forge/noarch/pyjwt-2.8.0-pyhd8ed1ab_0.conda + python: '>=3.7' + url: https://conda.anaconda.org/conda-forge/noarch/pyjwt-2.8.0-pyhd8ed1ab_1.conda + hash: + md5: 74f76d4868dbba5870f2cf1d9b12d8f3 + sha256: d7cb7fbafd767e938db10820c76a9c16d91faf5a081842159cc185787879eb07 + category: main + optional: false +- name: pyopenssl + version: 24.0.0 + manager: conda + platform: linux-64 + dependencies: + cryptography: '>=41.0.5,<43' + python: '>=3.7' + url: https://conda.anaconda.org/conda-forge/noarch/pyopenssl-24.0.0-pyhd8ed1ab_0.conda hash: - md5: 912c0194f898fdb783021fd25f913c31 - sha256: 88ac94c42ade15113397e30d1831dd341399b5262fb5330b9240f915c33cd232 + md5: b50aec2c744a5c493c09cce9e2e7533e + sha256: bacd1d38585f447e2809e7621283661da7c97cfa20f545edb0ac5838356ed87b + category: main + optional: false +- name: pyopenssl + version: 24.0.0 + manager: conda + platform: osx-64 + dependencies: + python: '>=3.7' + cryptography: '>=41.0.5,<43' + url: https://conda.anaconda.org/conda-forge/noarch/pyopenssl-24.0.0-pyhd8ed1ab_0.conda + hash: + md5: b50aec2c744a5c493c09cce9e2e7533e + sha256: bacd1d38585f447e2809e7621283661da7c97cfa20f545edb0ac5838356ed87b category: main optional: false - name: pysocks version: 1.7.1 manager: conda - platform: osx-64 + platform: linux-64 dependencies: __unix: '' python: '>=3.8' @@ -2310,396 +2117,458 @@ package: sha256: a42f826e958a8d22e65b3394f437af7332610e43ee313393d1cf143f0a2d274b category: main optional: false -- name: python-dotenv - version: 1.0.0 +- name: pysocks + version: 1.7.1 manager: conda platform: osx-64 dependencies: + __unix: '' python: '>=3.8' - url: https://conda.anaconda.org/conda-forge/noarch/python-dotenv-1.0.0-pyhd8ed1ab_1.conda + url: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha2e5f31_6.tar.bz2 + hash: + md5: 2a7de29fb590ca14b5243c4c812c8025 + sha256: a42f826e958a8d22e65b3394f437af7332610e43ee313393d1cf143f0a2d274b + category: main + optional: false +- name: pytest + version: 7.4.4 + manager: conda + platform: linux-64 + dependencies: + colorama: '' + exceptiongroup: '>=1.0.0rc8' + iniconfig: '' + packaging: '' + pluggy: '>=0.12,<2.0' + python: '>=3.7' + tomli: '>=1.0.0' + url: https://conda.anaconda.org/conda-forge/noarch/pytest-7.4.4-pyhd8ed1ab_0.conda hash: - md5: 111e7f9edd31865e2659fa9aad8ec8fd - sha256: bc5663f224ff6d8a399ec6bd8517e0c0f87a69ead438f82e5ce5c30f00077586 + md5: a9d145de8c5f064b5fa68fb34725d9f4 + sha256: 8979721b7f86b183d21103f3ec2734783847d317c1b754f462f407efc7c60886 category: main optional: false -- name: python-tzdata - version: '2023.3' +- name: pytest + version: 7.4.4 manager: conda platform: osx-64 dependencies: - python: '>=3.6' - url: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2023.3-pyhd8ed1ab_0.conda + packaging: '' + colorama: '' + iniconfig: '' + python: '>=3.7' + exceptiongroup: '>=1.0.0rc8' + tomli: '>=1.0.0' + pluggy: '>=0.12,<2.0' + url: https://conda.anaconda.org/conda-forge/noarch/pytest-7.4.4-pyhd8ed1ab_0.conda + hash: + md5: a9d145de8c5f064b5fa68fb34725d9f4 + sha256: 8979721b7f86b183d21103f3ec2734783847d317c1b754f462f407efc7c60886 + category: main + optional: false +- name: python + version: 3.11.8 + manager: conda + platform: linux-64 + dependencies: + bzip2: '>=1.0.8,<2.0a0' + ld_impl_linux-64: '>=2.36.1' + libexpat: '>=2.5.0,<3.0a0' + libffi: '>=3.4,<4.0a0' + libgcc-ng: '>=12' + libnsl: '>=2.0.1,<2.1.0a0' + libsqlite: '>=3.45.1,<4.0a0' + libuuid: '>=2.38.1,<3.0a0' + libxcrypt: '>=4.4.36' + libzlib: '>=1.2.13,<1.3.0a0' + ncurses: '>=6.4,<7.0a0' + openssl: '>=3.2.1,<4.0a0' + readline: '>=8.2,<9.0a0' + tk: '>=8.6.13,<8.7.0a0' + tzdata: '' + xz: '>=5.2.6,<6.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/python-3.11.8-hab00c5b_0_cpython.conda hash: - md5: 2590495f608a63625e165915fb4e2e34 - sha256: 0108888507014fb24573c31e4deceb61c99e63d37776dddcadd7c89b2ecae0b6 + md5: 2fdc314ee058eda0114738a9309d3683 + sha256: f33559d7127b6a892854bc3b2b4be1406c3be9537d658cb13edae57c8c0b5a11 category: main optional: false -- name: pytz - version: 2023.3.post1 +- name: python + version: 3.11.8 manager: conda platform: osx-64 dependencies: - python: '>=3.6' - url: https://conda.anaconda.org/conda-forge/noarch/pytz-2023.3.post1-pyhd8ed1ab_0.conda + bzip2: '>=1.0.8,<2.0a0' + libexpat: '>=2.5.0,<3.0a0' + libffi: '>=3.4,<4.0a0' + libsqlite: '>=3.45.1,<4.0a0' + libzlib: '>=1.2.13,<1.3.0a0' + ncurses: '>=6.4,<7.0a0' + openssl: '>=3.2.1,<4.0a0' + readline: '>=8.2,<9.0a0' + tk: '>=8.6.13,<8.7.0a0' + tzdata: '' + xz: '>=5.2.6,<6.0a0' + url: https://conda.anaconda.org/conda-forge/osx-64/python-3.11.8-h9f0c242_0_cpython.conda hash: - md5: c93346b446cd08c169d843ae5fc0da97 - sha256: 6b680e63d69aaf087cd43ca765a23838723ef59b0a328799e6363eb13f52c49e + md5: 22bda10a0f425564a538aed9a0e8a9df + sha256: 645dad20b46041ecd6a85eccbb3291fa1ad7921eea065c0081efff78c3d7e27a category: main optional: false -- name: pyyaml - version: 6.0.1 +- name: python-dateutil + version: 2.9.0 manager: conda - platform: osx-64 + platform: linux-64 dependencies: - python: '>=3.11,<3.12.0a0' - python_abi: 3.11.* - yaml: '>=0.2.5,<0.3.0a0' - url: https://conda.anaconda.org/conda-forge/osx-64/pyyaml-6.0.1-py311h2725bcf_1.conda + python: '>=3.7' + six: '>=1.5' + url: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0-pyhd8ed1ab_0.conda hash: - md5: 9283f991b5e5856a99f8aabba9927df5 - sha256: 8ce2ba443414170a2570514d0ce6d03625a847e91af9763d48dc58c338e6f7f3 + md5: 2cf4264fffb9e6eff6031c5b6884d61c + sha256: f3ceef02ac164a8d3a080d0d32f8e2ebe10dd29e3a685d240e38b3599e146320 category: main optional: false -- name: setuptools - version: 68.2.2 +- name: python-dateutil + version: 2.9.0 manager: conda platform: osx-64 dependencies: python: '>=3.7' - url: https://conda.anaconda.org/conda-forge/noarch/setuptools-68.2.2-pyhd8ed1ab_0.conda + six: '>=1.5' + url: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0-pyhd8ed1ab_0.conda hash: - md5: fc2166155db840c634a1291a5c35a709 - sha256: 851901b1f8f2049edb36a675f0c3f9a98e1495ef4eb214761b048c6f696a06f7 + md5: 2cf4264fffb9e6eff6031c5b6884d61c + sha256: f3ceef02ac164a8d3a080d0d32f8e2ebe10dd29e3a685d240e38b3599e146320 category: main optional: false -- name: six - version: 1.16.0 +- name: python-tzdata + version: '2024.1' manager: conda - platform: osx-64 + platform: linux-64 dependencies: - python: '' - url: https://conda.anaconda.org/conda-forge/noarch/six-1.16.0-pyh6c4a22f_0.tar.bz2 + python: '>=3.6' + url: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2024.1-pyhd8ed1ab_0.conda hash: - md5: e5f25f8dbc060e9a8d912e432202afc2 - sha256: a85c38227b446f42c5b90d9b642f2c0567880c15d72492d8da074a59c8f91dd6 + md5: 98206ea9954216ee7540f0c773f2104d + sha256: 9da9a849d53705dee450b83507df1ca8ffea5f83bd21a215202221f1c492f8ad category: main optional: false -- name: tomli - version: 2.0.1 +- name: python-tzdata + version: '2024.1' manager: conda platform: osx-64 dependencies: - python: '>=3.7' - url: https://conda.anaconda.org/conda-forge/noarch/tomli-2.0.1-pyhd8ed1ab_0.tar.bz2 + python: '>=3.6' + url: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2024.1-pyhd8ed1ab_0.conda hash: - md5: 5844808ffab9ebdb694585b50ba02a96 - sha256: 4cd48aba7cd026d17e86886af48d0d2ebc67ed36f87f6534f4b67138f5a5a58f + md5: 98206ea9954216ee7540f0c773f2104d + sha256: 9da9a849d53705dee450b83507df1ca8ffea5f83bd21a215202221f1c492f8ad category: main optional: false -- name: tomlkit - version: 0.12.3 +- name: python_abi + version: '3.11' manager: conda - platform: osx-64 - dependencies: - python: '>=3.7' - url: https://conda.anaconda.org/conda-forge/noarch/tomlkit-0.12.3-pyha770c72_0.conda + platform: linux-64 + dependencies: {} + url: https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.11-4_cp311.conda hash: - md5: 074d0ce7a6261ab8b497c3518796ef3e - sha256: 53cc436ab92d38683df1320e4468a8b978428e800195bf1c8c2460e90b0bc117 + md5: d786502c97404c94d7d58d258a445a65 + sha256: 0be3ac1bf852d64f553220c7e6457e9c047dfb7412da9d22fbaa67e60858b3cf category: main optional: false -- name: types-markdown - version: 3.5.0.3 +- name: python_abi + version: '3.11' manager: conda platform: osx-64 - dependencies: - python: '>=3.6' - url: https://conda.anaconda.org/conda-forge/noarch/types-markdown-3.5.0.3-pyhd8ed1ab_0.conda + dependencies: {} + url: https://conda.anaconda.org/conda-forge/osx-64/python_abi-3.11-4_cp311.conda hash: - md5: 9a78be2f67528371edc1bf3f908530b8 - sha256: a64b305419e2f0d537c42aa9c30285e31e41b852904e85d25243f9bc9a4c6d2d + md5: fef7a52f0eca6bae9e8e2e255bc86394 + sha256: f56dfe2a57b3b27bad3f9527f943548e8b2526e949d9d6fc0a383020d9359afe category: main optional: false -- name: typing_extensions - version: 4.9.0 +- name: pyyaml + version: 6.0.1 manager: conda - platform: osx-64 + platform: linux-64 dependencies: - python: '>=3.8' - url: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.9.0-pyha770c72_0.conda + libgcc-ng: '>=12' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + yaml: '>=0.2.5,<0.3.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.1-py311h459d7ec_1.conda hash: - md5: a92a6440c3fe7052d63244f3aba2a4a7 - sha256: f3c5be8673bfd905c4665efcb27fa50192f24f84fa8eff2f19cba5d09753d905 + md5: 52719a74ad130de8fb5d047dc91f247a + sha256: 28729ef1ffa7f6f9dfd54345a47c7faac5d34296d66a2b9891fb147f4efe1348 category: main optional: false -- name: urlobject - version: 2.4.3 +- name: pyyaml + version: 6.0.1 manager: conda platform: osx-64 dependencies: - python: '' - url: https://conda.anaconda.org/conda-forge/noarch/urlobject-2.4.3-py_1.tar.bz2 + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + yaml: '>=0.2.5,<0.3.0a0' + url: https://conda.anaconda.org/conda-forge/osx-64/pyyaml-6.0.1-py311h2725bcf_1.conda hash: - md5: 91140cccf570095e69d84b58075bc9b2 - sha256: 21354dad71b83bf6d0224f962565018c055f2d7f60205851491b9ff8f1d04936 + md5: 9283f991b5e5856a99f8aabba9927df5 + sha256: 8ce2ba443414170a2570514d0ce6d03625a847e91af9763d48dc58c338e6f7f3 category: main optional: false -- name: vine - version: 5.0.0 +- name: readline + version: '8.2' manager: conda - platform: osx-64 + platform: linux-64 dependencies: - python: '>=3.6' - url: https://conda.anaconda.org/conda-forge/noarch/vine-5.0.0-pyhd8ed1ab_1.tar.bz2 + libgcc-ng: '>=12' + ncurses: '>=6.3,<7.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8228510_1.conda hash: - md5: 7cb5b514698cfd164ad5103e58e1e201 - sha256: 53e1239bd2ef11f75d381ba6944d8339a247e42d088de87b9d9f751ddf7176bc + md5: 47d31b792659ce70f470b5c82fdfb7a4 + sha256: 5435cf39d039387fbdc977b0a762357ea909a7694d9528ab40f005e9208744d7 category: main optional: false -- name: wcwidth - version: 0.2.12 +- name: readline + version: '8.2' manager: conda platform: osx-64 dependencies: - python: '>=3.8' - url: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.2.12-pyhd8ed1ab_0.conda + ncurses: '>=6.3,<7.0a0' + url: https://conda.anaconda.org/conda-forge/osx-64/readline-8.2-h9e318b2_1.conda hash: - md5: bf4a1d1a97ca27b0b65bacd9e238b484 - sha256: ca757d0fc2dbd422af9d3238a8b4b630a6e11df3707a447bd89540656770d1d7 + md5: f17f77f2acf4d344734bda76829ce14e + sha256: 41e7d30a097d9b060037f0c6a2b1d4c4ae7e942c06c943d23f9d481548478568 category: main optional: false -- name: wheel - version: 0.42.0 +- name: requests + version: 2.31.0 manager: conda - platform: osx-64 + platform: linux-64 dependencies: + certifi: '>=2017.4.17' + charset-normalizer: '>=2,<4' + idna: '>=2.5,<4' python: '>=3.7' - url: https://conda.anaconda.org/conda-forge/noarch/wheel-0.42.0-pyhd8ed1ab_0.conda + urllib3: '>=1.21.1,<3' + url: https://conda.anaconda.org/conda-forge/noarch/requests-2.31.0-pyhd8ed1ab_0.conda hash: - md5: 1cdea58981c5cbc17b51973bcaddcea7 - sha256: 80be0ccc815ce22f80c141013302839b0ed938a2edb50b846cf48d8a8c1cfa01 + md5: a30144e4156cdbb236f99ebb49828f8b + sha256: 9f629d6fd3c8ac5f2a198639fe7af87c4db2ac9235279164bfe0fcb49d8c4bad category: main optional: false -- name: zipp - version: 3.17.0 +- name: requests + version: 2.31.0 manager: conda platform: osx-64 dependencies: - python: '>=3.8' - url: https://conda.anaconda.org/conda-forge/noarch/zipp-3.17.0-pyhd8ed1ab_0.conda + python: '>=3.7' + idna: '>=2.5,<4' + certifi: '>=2017.4.17' + charset-normalizer: '>=2,<4' + urllib3: '>=1.21.1,<3' + url: https://conda.anaconda.org/conda-forge/noarch/requests-2.31.0-pyhd8ed1ab_0.conda hash: - md5: 2e4d6bc0b14e10f895fc6791a7d9b26a - sha256: bced1423fdbf77bca0a735187d05d9b9812d2163f60ab426fc10f11f92ecbe26 + md5: a30144e4156cdbb236f99ebb49828f8b + sha256: 9f629d6fd3c8ac5f2a198639fe7af87c4db2ac9235279164bfe0fcb49d8c4bad category: main optional: false -- name: amqp - version: 5.2.0 +- name: requests-oauthlib + version: 1.4.0 manager: conda - platform: osx-64 + platform: linux-64 dependencies: - python: '>=3.6' - vine: 5.0.0 - url: https://conda.anaconda.org/conda-forge/noarch/amqp-5.2.0-pyhd8ed1ab_0.conda + oauthlib: '>=3.0.0' + python: '>=3.4' + requests: '>=2.0.0' + url: https://conda.anaconda.org/conda-forge/noarch/requests-oauthlib-1.4.0-pyhd8ed1ab_0.conda hash: - md5: 7ab12cc04a0596a8ab937e9d211503cc - sha256: 9993eb1df7e4a9a4e59c82a87495ab03762c94801f7c4bb8db922ca28681c126 + md5: a55b220de8970208f583e38639cfbecc + sha256: 909ec1510bbb6fad9276534352025f428050a4deeea86e68d61c8c580938ac82 category: main optional: false -- name: cffi - version: 1.16.0 +- name: requests-oauthlib + version: 1.4.0 manager: conda platform: osx-64 dependencies: - libffi: '>=3.4,<4.0a0' - pycparser: '' - python: '>=3.11,<3.12.0a0' - python_abi: 3.11.* - url: https://conda.anaconda.org/conda-forge/osx-64/cffi-1.16.0-py311hc0b63fd_0.conda + python: '>=3.4' + requests: '>=2.0.0' + oauthlib: '>=3.0.0' + url: https://conda.anaconda.org/conda-forge/noarch/requests-oauthlib-1.4.0-pyhd8ed1ab_0.conda hash: - md5: 15d07b82223cac96af629e5e747ba27a - sha256: 1f13a5fa7f310fdbd27f5eddceb9e62cfb10012c58a58c923dd6f51fa979748a + md5: a55b220de8970208f583e38639cfbecc + sha256: 909ec1510bbb6fad9276534352025f428050a4deeea86e68d61c8c580938ac82 category: main optional: false -- name: click-didyoumean - version: 0.3.0 +- name: rich + version: 13.7.1 manager: conda - platform: osx-64 + platform: linux-64 dependencies: - click: '>=7' - python: '>=3.6.2,<4.0.0' - url: https://conda.anaconda.org/conda-forge/noarch/click-didyoumean-0.3.0-pyhd8ed1ab_0.tar.bz2 + markdown-it-py: '>=2.2.0' + pygments: '>=2.13.0,<3.0.0' + python: '>=3.7.0' + typing_extensions: '>=4.0.0,<5.0.0' + url: https://conda.anaconda.org/conda-forge/noarch/rich-13.7.1-pyhd8ed1ab_0.conda hash: - md5: a49aa7bc1a82485f6a0a8849dcfefcad - sha256: e1af96905976a83f5545b0914830e272415e2e66428165fdf178e73dc4bc9cff + md5: ba445bf767ae6f0d959ff2b40c20912b + sha256: 2b26d58aa59e46f933c3126367348651b0dab6e0bf88014e857415bb184a4667 category: main optional: false -- name: click-plugins - version: 1.1.1 +- name: rich + version: 13.7.1 manager: conda platform: osx-64 dependencies: - python: '' - click: '>=3.0' - url: https://conda.anaconda.org/conda-forge/noarch/click-plugins-1.1.1-py_0.tar.bz2 + python: '>=3.7.0' + typing_extensions: '>=4.0.0,<5.0.0' + pygments: '>=2.13.0,<3.0.0' + markdown-it-py: '>=2.2.0' + url: https://conda.anaconda.org/conda-forge/noarch/rich-13.7.1-pyhd8ed1ab_0.conda hash: - md5: 4fd2c6b53934bd7d96d1f3fdaf99b79f - sha256: ddef6e559dde6673ee504b0e29dd814d36e22b6b9b1f519fa856ee268905bf92 + md5: ba445bf767ae6f0d959ff2b40c20912b + sha256: 2b26d58aa59e46f933c3126367348651b0dab6e0bf88014e857415bb184a4667 category: main optional: false -- name: gunicorn - version: 21.2.0 +- name: rich-click + version: 1.7.4 manager: conda - platform: osx-64 + platform: linux-64 dependencies: - packaging: '' - python: '>=3.11,<3.12.0a0' - python_abi: 3.11.* - setuptools: '>=3.0' - url: https://conda.anaconda.org/conda-forge/osx-64/gunicorn-21.2.0-py311h6eed73b_1.conda + click: '>=7,<9' + python: '>=3.7' + rich: '>=10' + url: https://conda.anaconda.org/conda-forge/noarch/rich-click-1.7.4-pyhd8ed1ab_0.conda hash: - md5: f818ed3365dbee71c75368587fc976b3 - sha256: dd5e241c4f1ad7982ea6aa5bc2cb5c03ce35af6a73ecc1a86c4dbb621ed12c68 + md5: a3e6556c7cfce33ba7dae23fb17d3303 + sha256: 91e0b041cf663d4a8e27f1cf572d265d90fdf3a58fa6513bc41292572ca5462f category: main optional: false -- name: importlib-metadata - version: 7.0.0 +- name: rich-click + version: 1.7.4 manager: conda platform: osx-64 dependencies: - python: '>=3.8' - zipp: '>=0.5' - url: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-7.0.0-pyha770c72_0.conda + python: '>=3.7' + click: '>=7,<9' + rich: '>=10' + url: https://conda.anaconda.org/conda-forge/noarch/rich-click-1.7.4-pyhd8ed1ab_0.conda hash: - md5: a941237cd06538837b25cd245fcd25d8 - sha256: 9731e82a00d36b182dc515e31723e711ac82890bb1ca86c6a17a4b471135564f + md5: a3e6556c7cfce33ba7dae23fb17d3303 + sha256: 91e0b041cf663d4a8e27f1cf572d265d90fdf3a58fa6513bc41292572ca5462f category: main optional: false -- name: jinja2 - version: 3.1.2 +- name: s3transfer + version: 0.10.0 manager: conda - platform: osx-64 + platform: linux-64 dependencies: - python: '>=3.7' - markupsafe: '>=2.0' - url: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.2-pyhd8ed1ab_1.tar.bz2 + botocore: '>=1.33.2,<2.0a.0' + python: '>=3.8' + url: https://conda.anaconda.org/conda-forge/noarch/s3transfer-0.10.0-pyhd8ed1ab_0.conda hash: - md5: c8490ed5c70966d232fdd389d0dbed37 - sha256: b045faba7130ab263db6a8fdc96b1a3de5fcf85c4a607c5f11a49e76851500b5 + md5: 2d52125a7fe49248ce5e883fed6c935a + sha256: 5f1fccbbc0460971f12dda7ab0465d8f6037486042d156b611881e57d218ce95 category: main optional: false -- name: markdown-it-py - version: 3.0.0 +- name: s3transfer + version: 0.10.0 manager: conda platform: osx-64 dependencies: python: '>=3.8' - mdurl: '>=0.1,<1' - url: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-3.0.0-pyhd8ed1ab_0.conda + botocore: '>=1.33.2,<2.0a.0' + url: https://conda.anaconda.org/conda-forge/noarch/s3transfer-0.10.0-pyhd8ed1ab_0.conda hash: - md5: 93a8e71256479c62074356ef6ebf501b - sha256: c041b0eaf7a6af3344d5dd452815cdc148d6284fec25a4fa3f4263b3a021e962 + md5: 2d52125a7fe49248ce5e883fed6c935a + sha256: 5f1fccbbc0460971f12dda7ab0465d8f6037486042d156b611881e57d218ce95 category: main optional: false -- name: mypy - version: 1.7.1 +- name: setuptools + version: 69.1.1 manager: conda - platform: osx-64 - dependencies: - mypy_extensions: '>=1.0.0' - psutil: '>=4.0' - python: '>=3.11,<3.12.0a0' - python_abi: 3.11.* - typing_extensions: '>=4.1.0' - url: https://conda.anaconda.org/conda-forge/osx-64/mypy-1.7.1-py311he705e18_1.conda + platform: linux-64 + dependencies: + python: '>=3.8' + url: https://conda.anaconda.org/conda-forge/noarch/setuptools-69.1.1-pyhd8ed1ab_0.conda hash: - md5: 153e0baec2f2d6c0bb6c9692f623ecdb - sha256: f9c25d9de38057167a10d4873e8ad7bcb9b9a8b5d1d3195421dac3445fb16b26 + md5: 576de899521b7d43674ba3ef6eae9142 + sha256: 7a6dca60efcaa42d0ebb784950bc16230a968256cb5048a4441cb34653b5ec58 category: main optional: false -- name: nodeenv - version: 1.8.0 +- name: setuptools + version: 69.1.1 manager: conda platform: osx-64 dependencies: - setuptools: '' - python: 2.7|>=3.7 - url: https://conda.anaconda.org/conda-forge/noarch/nodeenv-1.8.0-pyhd8ed1ab_0.conda + python: '>=3.8' + url: https://conda.anaconda.org/conda-forge/noarch/setuptools-69.1.1-pyhd8ed1ab_0.conda hash: - md5: 2a75b296096adabbabadd5e9782e5fcc - sha256: 1320306234552717149f36f825ddc7e27ea295f24829e9db4cc6ceaff0b032bd + md5: 576de899521b7d43674ba3ef6eae9142 + sha256: 7a6dca60efcaa42d0ebb784950bc16230a968256cb5048a4441cb34653b5ec58 category: main optional: false -- name: pip - version: 23.3.2 +- name: shellcheck + version: 0.8.0 manager: conda - platform: osx-64 - dependencies: - setuptools: '' - wheel: '' - python: '>=3.7' - url: https://conda.anaconda.org/conda-forge/noarch/pip-23.3.2-pyhd8ed1ab_0.conda + platform: linux-64 + dependencies: {} + url: https://conda.anaconda.org/conda-forge/linux-64/shellcheck-0.8.0-ha770c72_0.tar.bz2 hash: - md5: 8591c748f98dcc02253003533bc2e4b1 - sha256: 29096d1d53c61aeef518729add2f405df86b3629d1d738a35b15095e6a02eeed + md5: 0276737172793e3ec3dbc1631840ead7 + sha256: dfeecf516522c4b040ef9e4c6ec9c204d2b09f985f114c6a655522ed6407c27f category: main optional: false -- name: prompt-toolkit - version: 3.0.42 +- name: shellcheck + version: 0.8.0 manager: conda platform: osx-64 dependencies: - wcwidth: '' - python: '>=3.7' - url: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.42-pyha770c72_0.conda + gmp: '>=6.2.1,<7.0a0' + url: https://conda.anaconda.org/conda-forge/osx-64/shellcheck-0.8.0-h7dd6a17_0.tar.bz2 hash: - md5: 0bf64bf10eee21f46ac83c161917fa86 - sha256: 58525b2a9305fb154b2b0d43a48b9a6495441b80e4fbea44f2a34a597d2cef16 + md5: b5a30a0ff9f7b85fe4e3195bc12d4b66 + sha256: 9ae08b408c7c352be4dd927b50eb7c2b11d9053409f21c8ed608ac04636edcf6 category: main optional: false -- name: psycopg2 - version: 2.9.9 +- name: six + version: 1.16.0 manager: conda - platform: osx-64 + platform: linux-64 dependencies: - libpq: '>=16.1,<17.0a0' - openssl: '>=3.2.0,<4.0a0' - python: '>=3.11,<3.12.0a0' - python_abi: 3.11.* - url: https://conda.anaconda.org/conda-forge/osx-64/psycopg2-2.9.9-py311h187f0af_0.conda + python: '' + url: https://conda.anaconda.org/conda-forge/noarch/six-1.16.0-pyh6c4a22f_0.tar.bz2 hash: - md5: 2177c8943bbf9bfc45421ecaebd5be11 - sha256: 73c0cf543b0ddd41993956969f665999f5801e027e3d3524604892baedbd2626 + md5: e5f25f8dbc060e9a8d912e432202afc2 + sha256: a85c38227b446f42c5b90d9b642f2c0567880c15d72492d8da074a59c8f91dd6 category: main optional: false -- name: pytest - version: 7.4.3 +- name: six + version: 1.16.0 manager: conda platform: osx-64 dependencies: - packaging: '' - colorama: '' - iniconfig: '' - python: '>=3.7' - exceptiongroup: '>=1.0.0rc8' - tomli: '>=1.0.0' - pluggy: '>=0.12,<2.0' - url: https://conda.anaconda.org/conda-forge/noarch/pytest-7.4.3-pyhd8ed1ab_0.conda + python: '' + url: https://conda.anaconda.org/conda-forge/noarch/six-1.16.0-pyh6c4a22f_0.tar.bz2 hash: - md5: 5bdca0aca30b0ee62bb84854e027eae0 - sha256: 14e948e620ec87d9e62a8d9c21d40084b4805a939cfee322be7d457379dc96a0 + md5: e5f25f8dbc060e9a8d912e432202afc2 + sha256: a85c38227b446f42c5b90d9b642f2c0567880c15d72492d8da074a59c8f91dd6 category: main optional: false -- name: python-dateutil - version: 2.8.2 +- name: sqlalchemy + version: 1.4.49 manager: conda - platform: osx-64 + platform: linux-64 dependencies: - python: '>=3.6' - six: '>=1.5' - url: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.8.2-pyhd8ed1ab_0.tar.bz2 + greenlet: '!=0.4.17' + libgcc-ng: '>=12' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + url: https://conda.anaconda.org/conda-forge/linux-64/sqlalchemy-1.4.49-py311h459d7ec_1.conda hash: - md5: dd999d1cc9f79e67dbb855c8924c7984 - sha256: 54d7785c7678166aa45adeaccfc1d2b8c3c799ca2dc05d4a82bb39b1968bd7da + md5: 17392bcb4ceac1b2c95db9d54b4ac018 + sha256: 542dea4823e2e1283936fbd25c9f3fa960ec6df2dd54589b192b4dac68af7295 category: main optional: false - name: sqlalchemy @@ -2716,198 +2585,187 @@ package: sha256: 39569dfc13e742e1b61fcc92948e02bc7f361d05bb5100e5226251b70fcdf1af category: main optional: false -- name: typing-extensions - version: 4.9.0 +- name: tk + version: 8.6.13 manager: conda - platform: osx-64 + platform: linux-64 dependencies: - typing_extensions: 4.9.0 - url: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.9.0-hd8ed1ab_0.conda + libgcc-ng: '>=12' + libzlib: '>=1.2.13,<1.3.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h4845f30_101.conda hash: - md5: c16524c1b7227dc80b36b4fa6f77cc86 - sha256: d795c1eb1db4ea147f01ece74e5a504d7c2e8d5ee8c11ec987884967dd938f9c + md5: d453b98d9c83e71da0741bb0ff4d76bc + sha256: e0569c9caa68bf476bead1bed3d79650bb080b532c64a4af7d8ca286c08dea4e category: main optional: false -- name: urllib3 - version: 1.26.18 +- name: tk + version: 8.6.13 manager: conda platform: osx-64 dependencies: - python: '>=3.7' - brotli-python: '>=1.0.9' - pysocks: '>=1.5.6,<2.0,!=1.5.7' - url: https://conda.anaconda.org/conda-forge/noarch/urllib3-1.26.18-pyhd8ed1ab_0.conda + libzlib: '>=1.2.13,<1.3.0a0' + url: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-h1abcd95_1.conda hash: - md5: bf61cfd2a7f212efba378167a07d4a6a - sha256: 1cc0bab65a6ad0f5a8bd7657760a4fb4e670d30377f9dab88b792977cb3687e7 + md5: bf830ba5afc507c6232d4ef0fb1a882d + sha256: 30412b2e9de4ff82d8c2a7e5d06a15f4f4fef1809a72138b6ccb53a33b26faf5 category: main optional: false -- name: virtualenv - version: 20.25.0 +- name: tomli + version: 2.0.1 manager: conda - platform: osx-64 + platform: linux-64 dependencies: - python: '>=3.8' - distlib: <1,>=0.3.7 - filelock: <4,>=3.12.2 - platformdirs: <5,>=3.9.1 - url: https://conda.anaconda.org/conda-forge/noarch/virtualenv-20.25.0-pyhd8ed1ab_0.conda + python: '>=3.7' + url: https://conda.anaconda.org/conda-forge/noarch/tomli-2.0.1-pyhd8ed1ab_0.tar.bz2 hash: - md5: c119653cba436d8183c27bf6d190e587 - sha256: 50827c3721a9dbf973b568709d4381add2a6552fa562f26a385c5edc16a534af + md5: 5844808ffab9ebdb694585b50ba02a96 + sha256: 4cd48aba7cd026d17e86886af48d0d2ebc67ed36f87f6534f4b67138f5a5a58f category: main optional: false -- name: werkzeug - version: 2.2.3 +- name: tomli + version: 2.0.1 manager: conda platform: osx-64 dependencies: python: '>=3.7' - markupsafe: '>=2.1.1' - url: https://conda.anaconda.org/conda-forge/noarch/werkzeug-2.2.3-pyhd8ed1ab_0.conda + url: https://conda.anaconda.org/conda-forge/noarch/tomli-2.0.1-pyhd8ed1ab_0.tar.bz2 hash: - md5: 94dea682ba9d8e091e1f46872886fb1f - sha256: 38f68541580b74ed91396a67766fb96088f7b161bfd203675ed7a257d642e2e3 + md5: 5844808ffab9ebdb694585b50ba02a96 + sha256: 4cd48aba7cd026d17e86886af48d0d2ebc67ed36f87f6534f4b67138f5a5a58f category: main optional: false -- name: wtforms - version: 3.0.1 +- name: tomlkit + version: 0.12.4 manager: conda - platform: osx-64 + platform: linux-64 dependencies: - markupsafe: '' python: '>=3.7' - url: https://conda.anaconda.org/conda-forge/noarch/wtforms-3.0.1-pyhd8ed1ab_0.tar.bz2 + url: https://conda.anaconda.org/conda-forge/noarch/tomlkit-0.12.4-pyha770c72_0.conda hash: - md5: 62a19155ee3896566849634586c1813d - sha256: fc6bc71fc1bd5966ad024a1c828652b9a1ceee06ea514a94968b9b53492e3abe + md5: 37c47ea93ef00dd80d880fc4ba21256a + sha256: 8d45c266bf919788abacd9828f4a2101d7216f6d4fc7c8d3417034fe0d795a18 category: main optional: false -- name: annotated-types - version: 0.6.0 +- name: tomlkit + version: 0.12.4 manager: conda platform: osx-64 dependencies: python: '>=3.7' - typing-extensions: '>=4.0.0' - url: https://conda.anaconda.org/conda-forge/noarch/annotated-types-0.6.0-pyhd8ed1ab_0.conda + url: https://conda.anaconda.org/conda-forge/noarch/tomlkit-0.12.4-pyha770c72_0.conda hash: - md5: 997c29372bdbe2afee073dff71f35923 - sha256: 3a2c98154d95cfd54daba6b7d507d31f5ba07ac2ad955c44eb041b66563193cd + md5: 37c47ea93ef00dd80d880fc4ba21256a + sha256: 8d45c266bf919788abacd9828f4a2101d7216f6d4fc7c8d3417034fe0d795a18 category: main optional: false -- name: botocore - version: 1.34.2 +- name: types-markdown + version: 3.5.0.20240311 manager: conda - platform: osx-64 + platform: linux-64 dependencies: - python: '>=3.8' - python-dateutil: '>=2.1,<3.0.0' - jmespath: '>=0.7.1,<2.0.0' - urllib3: '>=1.25.4,<1.27' - url: https://conda.anaconda.org/conda-forge/noarch/botocore-1.34.2-pyhd8ed1ab_0.conda + python: '>=3.6' + url: https://conda.anaconda.org/conda-forge/noarch/types-markdown-3.5.0.20240311-pyhd8ed1ab_0.conda hash: - md5: 90d807074ce872ffb1e20ea0e7c76c10 - sha256: e404de75c98ee31d06b52c631078df97f77e50ebcb644758c22ec86fcca2d6c4 + md5: 01e9f6ef2d980874b9a4e5a6d32443fa + sha256: 65502afa9cd2d7377e5f2fdaff0f415f060a3aefcaa81c4969fae4ad50c31361 category: main optional: false -- name: cryptography - version: 41.0.7 +- name: types-markdown + version: 3.5.0.20240311 manager: conda platform: osx-64 dependencies: - cffi: '>=1.12' - openssl: '>=3.1.4,<4.0a0' - python: '>=3.11,<3.12.0a0' - python_abi: 3.11.* - url: https://conda.anaconda.org/conda-forge/osx-64/cryptography-41.0.7-py311h48c7838_1.conda + python: '>=3.6' + url: https://conda.anaconda.org/conda-forge/noarch/types-markdown-3.5.0.20240311-pyhd8ed1ab_0.conda hash: - md5: 65293feff96135571de02c047ecbd5a2 - sha256: f4c5e683386acf06cfa85805d264c9cd540361ec6e86740cb03312e560aa706a + md5: 01e9f6ef2d980874b9a4e5a6d32443fa + sha256: 65502afa9cd2d7377e5f2fdaff0f415f060a3aefcaa81c4969fae4ad50c31361 category: main optional: false -- name: markdown - version: 3.5.1 +- name: typing-extensions + version: 4.10.0 manager: conda - platform: osx-64 + platform: linux-64 dependencies: - python: '>=3.6' - importlib-metadata: '>=4.4' - url: https://conda.anaconda.org/conda-forge/noarch/markdown-3.5.1-pyhd8ed1ab_0.conda + typing_extensions: 4.10.0 + url: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.10.0-hd8ed1ab_0.conda hash: - md5: 323495027ffa625701129acebf861412 - sha256: 35e8990504cf8dc7e2bb63efd855fb0a0d5c0d77bf79403235e757c24a83ec1d + md5: 091683b9150d2ebaa62fd7e2c86433da + sha256: 0698fe2c4e555fb44c27c60f7a21fa0eea7f5bf8186ad109543c5b056e27f96a category: main optional: false -- name: prompt_toolkit - version: 3.0.42 +- name: typing-extensions + version: 4.10.0 manager: conda platform: osx-64 dependencies: - prompt-toolkit: '>=3.0.42,<3.0.43.0a0' - url: https://conda.anaconda.org/conda-forge/noarch/prompt_toolkit-3.0.42-hd8ed1ab_0.conda + typing_extensions: 4.10.0 + url: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.10.0-hd8ed1ab_0.conda hash: - md5: 85a2189ecd2fcdd86e92b2d4ea8fe461 - sha256: fd2185d501bf34cb4c121f2f5ade9157ac75e1644a9da81355c4c8f9c1b82d4d + md5: 091683b9150d2ebaa62fd7e2c86433da + sha256: 0698fe2c4e555fb44c27c60f7a21fa0eea7f5bf8186ad109543c5b056e27f96a category: main optional: false -- name: pydantic - version: 1.10.13 +- name: typing_extensions + version: 4.10.0 manager: conda - platform: osx-64 + platform: linux-64 dependencies: - python: '>=3.11,<3.12.0a0' - python_abi: 3.11.* - typing-extensions: '>=4.2.0' - url: https://conda.anaconda.org/conda-forge/osx-64/pydantic-1.10.13-py311he705e18_1.conda + python: '>=3.8' + url: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.10.0-pyha770c72_0.conda hash: - md5: ca0cd7b41964ce9a7b80290ea85e22e9 - sha256: c55ab5f7d182421a5c11f70afc32425fa192f1e40de5c301f685b25bdc3391a8 + md5: 16ae769069b380646c47142d719ef466 + sha256: 4be24d557897b2f6609f5d5f7c437833c62f4d4a96581e39530067e96a2d0451 category: main optional: false -- name: pydantic-core - version: 2.14.5 +- name: typing_extensions + version: 4.10.0 manager: conda platform: osx-64 dependencies: - python: '>=3.11,<3.12.0a0' - python_abi: 3.11.* - typing-extensions: '>=4.6.0' - url: https://conda.anaconda.org/conda-forge/osx-64/pydantic-core-2.14.5-py311h5e0f0e4_0.conda + python: '>=3.8' + url: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.10.0-pyha770c72_0.conda hash: - md5: 915ef17e91fbe6c5a2d2ea502e112ae7 - sha256: d79248cd5511c1153981b7fdcad4ef72a17985e604488a3ad1f242e2cd1e2622 + md5: 16ae769069b380646c47142d719ef466 + sha256: 4be24d557897b2f6609f5d5f7c437833c62f4d4a96581e39530067e96a2d0451 category: main optional: false -- name: requests - version: 2.31.0 +- name: tzdata + version: 2024a manager: conda - platform: osx-64 - dependencies: - python: '>=3.7' - idna: '>=2.5,<4' - certifi: '>=2017.4.17' - charset-normalizer: '>=2,<4' - urllib3: '>=1.21.1,<3' - url: https://conda.anaconda.org/conda-forge/noarch/requests-2.31.0-pyhd8ed1ab_0.conda + platform: linux-64 + dependencies: {} + url: https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h0c530f3_0.conda hash: - md5: a30144e4156cdbb236f99ebb49828f8b - sha256: 9f629d6fd3c8ac5f2a198639fe7af87c4db2ac9235279164bfe0fcb49d8c4bad + md5: 161081fc7cec0bfda0d86d7cb595f8d8 + sha256: 7b2b69c54ec62a243eb6fba2391b5e443421608c3ae5dbff938ad33ca8db5122 category: main optional: false -- name: rich - version: 13.7.0 +- name: tzdata + version: 2024a manager: conda platform: osx-64 + dependencies: {} + url: https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h0c530f3_0.conda + hash: + md5: 161081fc7cec0bfda0d86d7cb595f8d8 + sha256: 7b2b69c54ec62a243eb6fba2391b5e443421608c3ae5dbff938ad33ca8db5122 + category: main + optional: false +- name: ukkonen + version: 1.0.1 + manager: conda + platform: linux-64 dependencies: - python: '>=3.7.0' - typing_extensions: '>=4.0.0,<5.0.0' - pygments: '>=2.13.0,<3.0.0' - markdown-it-py: '>=2.2.0' - url: https://conda.anaconda.org/conda-forge/noarch/rich-13.7.0-pyhd8ed1ab_0.conda + cffi: '' + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + url: https://conda.anaconda.org/conda-forge/linux-64/ukkonen-1.0.1-py311h9547e67_4.conda hash: - md5: d7a11d4f3024b2f4a6e0ae7377dd61e9 - sha256: 4bb25bf1f5664772b2c4c2e3878aa6e7dc2695f97e3da4ee8e47c51e179913bb + md5: 586da7df03b68640de14dc3e8bcbf76f + sha256: c2d33e998f637b594632eba3727529171a06eb09896e36aa42f1ebcb03779472 category: main optional: false - name: ukkonen @@ -2925,283 +2783,292 @@ package: sha256: b273782a1277042a54e12411beebd378d2a2a69e503bcf147766e98628e91c91 category: main optional: false -- name: click-repl - version: 0.3.0 +- name: urllib3 + version: 2.0.7 + manager: conda + platform: linux-64 + dependencies: + brotli-python: '>=1.0.9' + pysocks: '>=1.5.6,<2.0,!=1.5.7' + python: '>=3.7' + url: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.0.7-pyhd8ed1ab_0.conda + hash: + md5: 270e71c14d37074b1d066ee21cf0c4a6 + sha256: 9fe14735dde74278c6f1710cbe883d5710fc98501a96031dec6849a8d8a1bb11 + category: main + optional: false +- name: urllib3 + version: 2.0.7 manager: conda platform: osx-64 dependencies: - six: '' - click: '' - prompt_toolkit: '' - python: '>=3.6' - url: https://conda.anaconda.org/conda-forge/noarch/click-repl-0.3.0-pyhd8ed1ab_0.conda + python: '>=3.7' + brotli-python: '>=1.0.9' + pysocks: '>=1.5.6,<2.0,!=1.5.7' + url: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.0.7-pyhd8ed1ab_0.conda + hash: + md5: 270e71c14d37074b1d066ee21cf0c4a6 + sha256: 9fe14735dde74278c6f1710cbe883d5710fc98501a96031dec6849a8d8a1bb11 + category: main + optional: false +- name: urlobject + version: 2.4.3 + manager: conda + platform: linux-64 + dependencies: + python: '' + url: https://conda.anaconda.org/conda-forge/noarch/urlobject-2.4.3-py_1.tar.bz2 + hash: + md5: 91140cccf570095e69d84b58075bc9b2 + sha256: 21354dad71b83bf6d0224f962565018c055f2d7f60205851491b9ff8f1d04936 + category: main + optional: false +- name: urlobject + version: 2.4.3 + manager: conda + platform: osx-64 + dependencies: + python: '' + url: https://conda.anaconda.org/conda-forge/noarch/urlobject-2.4.3-py_1.tar.bz2 hash: - md5: 27eb8f68250666c1a19d1b6ec9d12c4e - sha256: 69c16e0b89e1fb4fc444af4798ef1222b3075d074cbcbe70b9af793668200a14 + md5: 91140cccf570095e69d84b58075bc9b2 + sha256: 21354dad71b83bf6d0224f962565018c055f2d7f60205851491b9ff8f1d04936 category: main optional: false -- name: identify - version: 2.5.33 +- name: vine + version: 5.1.0 manager: conda - platform: osx-64 + platform: linux-64 dependencies: - ukkonen: '' python: '>=3.6' - url: https://conda.anaconda.org/conda-forge/noarch/identify-2.5.33-pyhd8ed1ab_0.conda + url: https://conda.anaconda.org/conda-forge/noarch/vine-5.1.0-pyhd8ed1ab_0.conda hash: - md5: 93c8f8ceb83827d88deeba796f07fba7 - sha256: ce2a64c18221af96226be23278d81f22ff9f64b3c047d8865590f6718915303f + md5: af71debe5f1819b065c0ba2b062ae81c + sha256: 95a819731659de391ee084d68a1bdee56a5a16d0db95783c6cf115638d372215 category: main optional: false -- name: oauthlib - version: 3.2.2 +- name: vine + version: 5.1.0 manager: conda platform: osx-64 dependencies: - cryptography: '' - blinker: '' python: '>=3.6' - pyjwt: '>=1.0.0' - url: https://conda.anaconda.org/conda-forge/noarch/oauthlib-3.2.2-pyhd8ed1ab_0.tar.bz2 + url: https://conda.anaconda.org/conda-forge/noarch/vine-5.1.0-pyhd8ed1ab_0.conda hash: - md5: 8f882b197fd9c4941a787926baea4868 - sha256: 0cfd5146a91d3974f4abfc2a45de890371d510a77238fe553e036ec8c031dc5b + md5: af71debe5f1819b065c0ba2b062ae81c + sha256: 95a819731659de391ee084d68a1bdee56a5a16d0db95783c6cf115638d372215 category: main optional: false -- name: pydantic-settings - version: 2.1.0 +- name: virtualenv + version: 20.25.1 manager: conda - platform: osx-64 + platform: linux-64 dependencies: + distlib: <1,>=0.3.7 + filelock: <4,>=3.12.2 + platformdirs: <5,>=3.9.1 python: '>=3.8' - python-dotenv: '>=0.21.0' - pydantic: '>=2.3.0' - url: https://conda.anaconda.org/conda-forge/noarch/pydantic-settings-2.1.0-pyhd8ed1ab_1.conda + url: https://conda.anaconda.org/conda-forge/noarch/virtualenv-20.25.1-pyhd8ed1ab_0.conda hash: - md5: 2a63052c1180846d4a6aaa9df7e113a3 - sha256: 2c80df34463dabec383b37dc19da48f84a1ea97f3d828d6d0dd220110da5f4e1 + md5: 8797a4e26be36880a603aba29c785352 + sha256: 1ced4445cf72cd9dc344ad04bdaf703a08cc428c8c46e4bda928ad79786ee153 category: main optional: false -- name: pyopenssl - version: 23.3.0 +- name: virtualenv + version: 20.25.1 manager: conda platform: osx-64 dependencies: - python: '>=3.7' - cryptography: '>=41.0.5,<42' - url: https://conda.anaconda.org/conda-forge/noarch/pyopenssl-23.3.0-pyhd8ed1ab_0.conda + python: '>=3.8' + distlib: <1,>=0.3.7 + filelock: <4,>=3.12.2 + platformdirs: <5,>=3.9.1 + url: https://conda.anaconda.org/conda-forge/noarch/virtualenv-20.25.1-pyhd8ed1ab_0.conda hash: - md5: 7819533e674dbbc51468f3228b9b1bb6 - sha256: f7e04c4a49b1593140231d70801e2204e314e26d7141bfbdc8089d04114c0010 + md5: 8797a4e26be36880a603aba29c785352 + sha256: 1ced4445cf72cd9dc344ad04bdaf703a08cc428c8c46e4bda928ad79786ee153 category: main optional: false -- name: rich-click - version: 1.7.2 +- name: wcwidth + version: 0.2.13 manager: conda - platform: osx-64 + platform: linux-64 dependencies: - python: '>=3.7' - click: '>=7,<9' - rich: '>=10' - url: https://conda.anaconda.org/conda-forge/noarch/rich-click-1.7.2-pyhd8ed1ab_0.conda + python: '>=3.8' + url: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.2.13-pyhd8ed1ab_0.conda hash: - md5: fe1eae76a71246959cefd09b5b00869b - sha256: 4bfddbc5716a09bfea1c3345b1eed0ddb36d0e55f357afb60ca3e73c9231cc68 + md5: 68f0738df502a14213624b288c60c9ad + sha256: b6cd2fee7e728e620ec736d8dfee29c6c9e2adbd4e695a31f1d8f834a83e57e3 category: main optional: false -- name: s3transfer - version: 0.9.0 +- name: wcwidth + version: 0.2.13 manager: conda platform: osx-64 dependencies: python: '>=3.8' - botocore: '>=1.33.2,<2.0a.0' - url: https://conda.anaconda.org/conda-forge/noarch/s3transfer-0.9.0-pyhd8ed1ab_0.conda + url: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.2.13-pyhd8ed1ab_0.conda hash: - md5: 27ad14e5fc6a13f05b90140debc72cd2 - sha256: c9fc315d830238113160471467259740593966bcf59f17287a0baeaf1f6a76d8 + md5: 68f0738df502a14213624b288c60c9ad + sha256: b6cd2fee7e728e620ec736d8dfee29c6c9e2adbd4e695a31f1d8f834a83e57e3 category: main optional: false -- name: boto3 - version: 1.34.2 +- name: werkzeug + version: 2.2.3 manager: conda - platform: osx-64 + platform: linux-64 dependencies: - python: '>=3.8' - jmespath: '>=0.7.1,<2.0.0' - s3transfer: '>=0.9.0,<0.10.0' - botocore: '>=1.34.2,<1.35.0' - url: https://conda.anaconda.org/conda-forge/noarch/boto3-1.34.2-pyhd8ed1ab_0.conda + markupsafe: '>=2.1.1' + python: '>=3.7' + url: https://conda.anaconda.org/conda-forge/noarch/werkzeug-2.2.3-pyhd8ed1ab_0.conda hash: - md5: 6f47ce30284317cb85960c4668cc5bd9 - sha256: c0444d9d386a1991fadca8b31b9dfc302e5d781b8a2d29faff2c47286eff477c + md5: 94dea682ba9d8e091e1f46872886fb1f + sha256: 38f68541580b74ed91396a67766fb96088f7b161bfd203675ed7a257d642e2e3 category: main optional: false -- name: bump-my-version - version: 0.9.3 +- name: werkzeug + version: 2.2.3 manager: conda platform: osx-64 dependencies: - click: '' - rich: '' - tomlkit: '' - rich-click: '' - python: '>=3.8' - pydantic: <2.0.0 - url: https://conda.anaconda.org/conda-forge/noarch/bump-my-version-0.9.3-pyhd8ed1ab_0.conda + python: '>=3.7' + markupsafe: '>=2.1.1' + url: https://conda.anaconda.org/conda-forge/noarch/werkzeug-2.2.3-pyhd8ed1ab_0.conda hash: - md5: 24f0c263af3783651f77b021a6ad97a8 - sha256: b5530371b54677cb72d93ae091052327b2c809a6066d85366af4f3f0728888eb + md5: 94dea682ba9d8e091e1f46872886fb1f + sha256: 38f68541580b74ed91396a67766fb96088f7b161bfd203675ed7a257d642e2e3 category: main optional: false -- name: pre-commit - version: 3.6.0 +- name: wheel + version: 0.42.0 manager: conda - platform: osx-64 + platform: linux-64 dependencies: - python: '>=3.9' - pyyaml: '>=5.1' - identify: '>=1.0.0' - nodeenv: '>=0.11.1' - cfgv: '>=2.0.0' - virtualenv: '>=20.10.0' - url: https://conda.anaconda.org/conda-forge/noarch/pre-commit-3.6.0-pyha770c72_0.conda + python: '>=3.7' + url: https://conda.anaconda.org/conda-forge/noarch/wheel-0.42.0-pyhd8ed1ab_0.conda hash: - md5: 473a7cfca197da0a10cff3f6dded7d4b - sha256: 7d1f4b4a2eb4946b5808769642c5f643788c3a9e090f1c02a6c63f8794fb3d54 + md5: 1cdea58981c5cbc17b51973bcaddcea7 + sha256: 80be0ccc815ce22f80c141013302839b0ed938a2edb50b846cf48d8a8c1cfa01 category: main optional: false -- name: requests-oauthlib - version: 1.3.1 +- name: wheel + version: 0.42.0 manager: conda platform: osx-64 dependencies: - python: '>=3.4' - requests: '>=2.0.0' - oauthlib: '>=3.0.0' - url: https://conda.anaconda.org/conda-forge/noarch/requests-oauthlib-1.3.1-pyhd8ed1ab_0.tar.bz2 + python: '>=3.7' + url: https://conda.anaconda.org/conda-forge/noarch/wheel-0.42.0-pyhd8ed1ab_0.conda hash: - md5: 61b279f051eef9c89d58f4d813e75e04 - sha256: 889e3c1b84467b64046776db95dc4c5ea4dad5afaa5ec18ad811bd95c63286b0 + md5: 1cdea58981c5cbc17b51973bcaddcea7 + sha256: 80be0ccc815ce22f80c141013302839b0ed938a2edb50b846cf48d8a8c1cfa01 category: main optional: false -- name: kombu - version: 5.3.4 +- name: wtforms + version: 3.1.2 manager: conda - platform: osx-64 + platform: linux-64 dependencies: - amqp: '>=5.1.1,<6.0.0' - boto3: '>=1.26.143' - python: '>=3.11,<3.12.0a0' - python_abi: 3.11.* - vine: '' - url: https://conda.anaconda.org/conda-forge/osx-64/kombu-5.3.4-py311h6eed73b_0.conda + markupsafe: '' + python: '>=3.8' + url: https://conda.anaconda.org/conda-forge/noarch/wtforms-3.1.2-pyhd8ed1ab_0.conda hash: - md5: d3dbbb66857aea7968bf38186bbf970c - sha256: 249eded4ed2933f29e90e98632a2e50b74d20c0829cf9d302b19fb478c07cdd2 + md5: 1e60bfa73e13b1ca9c439c6938664ab4 + sha256: 6d3ae74038e34945daf8e091b7fa85741162c0db6e989da4d0f8164d84a4cbce category: main optional: false -- name: celery - version: 5.3.4 +- name: wtforms + version: 3.1.2 manager: conda platform: osx-64 dependencies: - python: '>=3.7' - backports.zoneinfo: '>=0.2.1' - pytz: '>=2021.3' - python-dateutil: '>=2.8.2' - importlib-metadata: '>=3.6' - vine: '>=5.0.0,<6.0' - click-plugins: '>=1.1.1' - click-repl: '>=0.2.0' - billiard: '>=4.1.0,<5.0' - click: '>=8.1.2,<9.0' - click-didyoumean: '>=0.3.0' - python-tzdata: '>=2022.7' - kombu: '>=5.3.2,<6.0' - url: https://conda.anaconda.org/conda-forge/noarch/celery-5.3.4-pyhd8ed1ab_1.conda + markupsafe: '' + python: '>=3.8' + url: https://conda.anaconda.org/conda-forge/noarch/wtforms-3.1.2-pyhd8ed1ab_0.conda hash: - md5: 64a66acef34f7cca60b929b8228951f1 - sha256: b93109413da956e6e2a37b0a852ccb15a45f706c7ce718f9ea094c660b18b5e3 + md5: 1e60bfa73e13b1ca9c439c6938664ab4 + sha256: 6d3ae74038e34945daf8e091b7fa85741162c0db6e989da4d0f8164d84a4cbce category: main optional: false -- name: flask - version: 2.2.5 +- name: xz + version: 5.2.6 manager: conda - platform: osx-64 + platform: linux-64 dependencies: - python: '>=3.7' - jinja2: '>=3.0' - click: '>=8.0' - importlib-metadata: '>=3.6.0' - itsdangerous: '>=2.0' - werkzeug: '>=2.2.2' - celery: '>=5.2.7' - url: https://conda.anaconda.org/conda-forge/noarch/flask-2.2.5-pyhd8ed1ab_0.conda + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/xz-5.2.6-h166bdaf_0.tar.bz2 hash: - md5: 66c558a28b9b47c1e2aa8e8cb80b9037 - sha256: 9356ae47278788439770eb9d05bfeb72c39a53a8b10a0e534b612f99b9c18e9b + md5: 2161070d867d1b1204ea749c8eec4ef0 + sha256: 03a6d28ded42af8a347345f82f3eebdd6807a08526d47899a42d62d319609162 category: main optional: false -- name: flask-dance - version: 7.0.0 +- name: xz + version: 5.2.6 manager: conda platform: osx-64 - dependencies: - werkzeug: '' - urlobject: '' - python: '>=3.7' - requests-oauthlib: '>=1.0.0' - requests: '>=2.0' - flask: '>=2.0.3' - oauthlib: '>=3.2' - url: https://conda.anaconda.org/conda-forge/noarch/flask-dance-7.0.0-pyhd8ed1ab_0.conda + dependencies: {} + url: https://conda.anaconda.org/conda-forge/osx-64/xz-5.2.6-h775f41a_0.tar.bz2 hash: - md5: a0007a17aa03b39c941f921f082afe9e - sha256: 7f3b75ba7f321c9058e0adbd4113fe3bfad3fc28ea266fb42f7636091cd4ca34 + md5: a72f9d4ea13d55d745ff1ed594747f10 + sha256: eb09823f34cc2dd663c0ec4ab13f246f45dcd52e5b8c47b9864361de5204a1c8 category: main optional: false -- name: flask-login - version: 0.6.3 +- name: yaml + version: 0.2.5 manager: conda - platform: osx-64 + platform: linux-64 dependencies: - python: '>=3.7' - flask: '>=1.0.4' - werkzeug: '>=1.0.1' - url: https://conda.anaconda.org/conda-forge/noarch/flask-login-0.6.3-pyhd8ed1ab_1.conda + libgcc-ng: '>=9.4.0' + url: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h7f98852_2.tar.bz2 hash: - md5: 1d3523e7b18b38e4be992bf462d99e40 - sha256: 23ffed52801132e3e434c82a544ef1b77efbc025878a1a1cd0fbcc4c361cd9de + md5: 4cb3ad778ec2d5a7acbdf254eb1c42ae + sha256: a4e34c710eeb26945bdbdaba82d3d74f60a78f54a874ec10d373811a5d217535 category: main optional: false -- name: flask-sqlalchemy - version: 3.0.3 +- name: yaml + version: 0.2.5 manager: conda platform: osx-64 + dependencies: {} + url: https://conda.anaconda.org/conda-forge/osx-64/yaml-0.2.5-h0d85af4_2.tar.bz2 + hash: + md5: d7e08fcf8259d742156188e8762b4d20 + sha256: 5301417e2c8dea45b401ffee8df3957d2447d4ce80c83c5ff151fc6bfe1c4148 + category: main + optional: false +- name: zipp + version: 3.17.0 + manager: conda + platform: linux-64 dependencies: - python: '>=3.7' - flask: '>=2.2' - sqlalchemy: '>=1.4.18' - url: https://conda.anaconda.org/conda-forge/noarch/flask-sqlalchemy-3.0.3-pyhd8ed1ab_0.conda + python: '>=3.8' + url: https://conda.anaconda.org/conda-forge/noarch/zipp-3.17.0-pyhd8ed1ab_0.conda hash: - md5: 6a301e2aa76aef91787e15f59ab03390 - sha256: 460693607ab0e2feb600bb755851c0c19db8d6dbd1c7636904c8de6f2c63040d + md5: 2e4d6bc0b14e10f895fc6791a7d9b26a + sha256: bced1423fdbf77bca0a735187d05d9b9812d2163f60ab426fc10f11f92ecbe26 category: main optional: false -- name: flask-wtf - version: 1.2.1 +- name: zipp + version: 3.17.0 manager: conda platform: osx-64 dependencies: - flask: '' - itsdangerous: '' - wtforms: '' python: '>=3.8' - url: https://conda.anaconda.org/conda-forge/noarch/flask-wtf-1.2.1-pyhd8ed1ab_0.conda + url: https://conda.anaconda.org/conda-forge/noarch/zipp-3.17.0-pyhd8ed1ab_0.conda hash: - md5: e7b75c25f98d7b55943466683e400529 - sha256: cee583240eec93d27f262f3bb8cdecf8334e502d81923295d98f378b15fe26d5 + md5: 2e4d6bc0b14e10f895fc6791a7d9b26a + sha256: bced1423fdbf77bca0a735187d05d9b9812d2163f60ab426fc10f11f92ecbe26 + category: main + optional: false +- name: bootstrap-flask + version: 2.3.3 + manager: pip + platform: linux-64 + dependencies: + flask: '*' + wtforms: '*' + url: https://files.pythonhosted.org/packages/95/e4/7f3dd2dba1ef70ddbf1bf6a58ea8245c20b2d44bd81e8b37215e812f5d0e/Bootstrap_Flask-2.3.3-py2.py3-none-any.whl + hash: + sha256: 4c3d1f1b8ee19081dfc80e03aba5cb19cd92a8d54ba0ba98cee69acffded91d0 category: main optional: false - name: bootstrap-flask @@ -3216,6 +3083,42 @@ package: sha256: 4c3d1f1b8ee19081dfc80e03aba5cb19cd92a8d54ba0ba98cee69acffded91d0 category: main optional: false +- name: flask-pydantic + version: 0.10.0 + manager: pip + platform: linux-64 + dependencies: + flask: '*' + pydantic: '>=1.7' + url: https://files.pythonhosted.org/packages/9e/a8/89b0d85e2b76a10bb1987050c8d3fd5160b8749574807f4502525cba5988/Flask_Pydantic-0.10.0-py3-none-any.whl + hash: + sha256: a52e12d5c3d9f53c06edb4a138973d2bf6dea968269bcedc67751403ca70e856 + category: main + optional: false +- name: flask-pydantic + version: 0.10.0 + manager: pip + platform: osx-64 + dependencies: + flask: '*' + pydantic: '>=1.7' + url: https://files.pythonhosted.org/packages/9e/a8/89b0d85e2b76a10bb1987050c8d3fd5160b8749574807f4502525cba5988/Flask_Pydantic-0.10.0-py3-none-any.whl + hash: + sha256: a52e12d5c3d9f53c06edb4a138973d2bf6dea968269bcedc67751403ca70e856 + category: main + optional: false +- name: wtforms-sqlalchemy + version: '0.3' + manager: pip + platform: linux-64 + dependencies: + wtforms: '>=1.0.5' + sqlalchemy: '>=0.7.10' + url: https://files.pythonhosted.org/packages/62/71/a5624ff63f4024d27489263b56e3ab6a5122087eb432d14617cfed90ec3d/WTForms_SQLAlchemy-0.3-py3-none-any.whl + hash: + sha256: 90195d7592bf256d82498c42c79d416832e4a4e6fbca4f1e745a018f66d26c47 + category: main + optional: false - name: wtforms-sqlalchemy version: '0.3' manager: pip diff --git a/doc/how-to/development.md b/doc/how-to/development.md index dc8554b3..451ec6d4 100644 --- a/doc/how-to/development.md +++ b/doc/how-to/development.md @@ -88,6 +88,8 @@ docker compose logs -f In dev this will initialize a PostgreSQL DB that is preserved in the `_db/` directory. In NSIDC deployment environments we deploy the db on a separate host using the [usaon-benefit-tool-db project](https://github.com/nsidc/usaon-benefit-tool-db). + +**The dev credentials are specified in `compose.dev.yml`.** ::: Run `./scripts/invoke_in_container.sh db.init` @@ -145,3 +147,74 @@ See [our documentation on third-party services](/reference/third-party-service-dependencies.md) for more, but the way they are currently set up should allow for development on `localhost`. + + +### Coding concerns + +#### Adding a new route + +* Create the new route function in a module under `/routes/` +* Should the route be restricted to only logged-in users? If so, decorate with + `@login_required`. +* If you need to create a new module and blueprint, don't forget to register the + blueprint in `/__init__.py` + + +#### REST API design + +* Use `.../form` endpoints for HTMX to get user interface elements. + * This is valuable for using HTMX to help with separation of user interface + concerns; e.g. instead of designing a page with multiple forms, design form + endpoints and HTMX elements on the page which use those endpoints to display the + returned forms in a modal. + * Use `GET resources/form` route to serve a form to add a new resource to collection + "resources". + * That form will `POST resources` to request the resource to be created. + * Use `GET resource//form` route to serve a form to edit a "resource". + * That form will `PUT resource/` to request the resource to be updated. +* Use consistent HTTP response codes: + * `200`: Successfully returned page or successfully updated resource + * `201`: Create resource + * `202`: Successfully deleted resource + * ... +* Avoid redirections after operations, e.g. after a delete, don't `302`. Instead, + `202` and pass an `HX-Redirect` header to inform HTMX, if used on the client side, + what to do. +* Use separate route functions for separate verbs/methods! Otherwise there can be + excessive conditional logic inside route functions. + + +#### Flask route design and naming + +Most importantly, **keep it simple**! Constructing route strings can be confusing +otherwise. + +* Use HTTP verbs to name basic endpoint functions (`get`, `post`, `delete`, etc.). + Resist the honorable temptation to write descriptive route function names. Instead of + `view_project_data_products`, stick with `get`. Then the resultant route identifier + might be `project.data_products.get` instead of + `project.data_products.view_project_data_products`. + * Form-serving routes are an exception; see the "REST API design" section above. +* Use nested routes. This pushes the responsibility for route registry down the route + hierarchy instead of requiring every blueprint to be registered in `__init__.py`. + + +#### Flask template design and naming + +##### Macros + +* Macros live in `templates/macros/` +* Macros that output HTML should be prefixed with `render_`. The codebase is currently + not in compliance with this. + + +### Debugging + +#### HTMX routes + +When you put a `breakpoint()` in a route that returns a partial to HTMX and try to test +in the browser, HTMX will swallow the debugger response, treating it like a failure. + +You can directly visit the route in question with your browser to bypass this and access +the Flask debugger! However, the HTMX Javascript won't load, so you can't really use +this to debug forms. diff --git a/doc/notes/TODO.md b/doc/notes/TODO.md index 10806c7f..aa736ce0 100644 --- a/doc/notes/TODO.md +++ b/doc/notes/TODO.md @@ -2,6 +2,18 @@ title: "TODO" --- + +## Type checking + +* The data models don't seem to be being type checked as expected. E.g. Passing the + wrong field name doesn't trigger the type checker. + + +## Dependencies + +* SQLAlchemy -> 2.0. NOTE: Remove the mypy plugin during migration! + + ## Templates * Should Jinja templates have a `.html` or `.j2` extension? diff --git a/doc/reference/data_model.md b/doc/reference/data_model.md index cadb5423..d1495522 100644 --- a/doc/reference/data_model.md +++ b/doc/reference/data_model.md @@ -1,16 +1,6 @@ ```mermaid erDiagram -%% Dynamic operational data: -survey { - uuid id PK - int response_id FK - str title - str created_by FK - datetime created_timestamp - str notes "nullable" -} - user { string id PK "user identifier" string role FK @@ -19,12 +9,19 @@ user { string affiliation "?" } -response { +role { + string id PK "name" +} + +%% Dynamic operational data: +assessment { uuid id PK - seq version PK + %% seq version PK - string expert_user_id FK - string application_object_id FK + str title + str description + + string assignee FK array tags @@ -34,102 +31,62 @@ response { } -response_observing_system { - int response_id FK - string id PK - string object_id FK +assessment_node { + int id PK "SK" + int assessment_id FK + int node_id FK - enum type - string url - string author_name - string author_email - string funding_country - string funding_agency - string references_citations - string notes "nullable" } -%% This name is confusing, but "observational" _is_ a type of observing system -response_observing_system_observational { - int response_id PK - string response_observing_system_id FK +link { + int source_assessment_node_id PK "FK" + int target_assessment_node_id PK "FK" - string platform - string sensor + %% TODO identify which rating + int rating } -response_observing_system_research { - int response_id PK - string response_observing_system_id FK - - - string intermediate_product -} - -response_observing_system_data_product { - int response_id PK - string observing_system_id PK - string data_product_id PK - - int observing_system_contribution_to_data_product_rating "0-100" - int satisfaction_rating "0-100" - string rationale "nullable" - string needed_improvements "nullable" -} - -response_data_product { +%% AKA node library +node { int id PK - int response_id FK - - str name - int satisfaction_rating "0-100" -} - -response_data_product_application { - string response_data_product_id FK - string response_application_id PK - - int data_product_contribution_to_application_rating "0-100" - int satisfaction_rating "0-100" - string rationale "nullable" - string needed_improvements "nullable" -} -response_application { - int id PK + enum type - int response_id FK - string name -} + str description + %% not implemented in the app + %% str tags + str version -response_application_societal_benefit_area { - int response_application_id FK - string response_societal_benefit_area_id FK + string created_by FK + datetime created + datetime updated - int application_contribution_to_sociateal_benefit_area_rating "0-100" } +node_other{ + int node_id PK "FK" + %% str societal_benefit_area_id FK "UK" -response_object { - string id PK "name" - string type FK - TODO TODO "more fields?" + enum type + str short_name + str full_name + str organization + str funder + str funding_country + str website + str contact_information + str persistent_identifier + boolean hypothetical } - -analysis { - string id PK "name" - string analyst_user_id FK - string description -} -analysis_response { - string analysis_id PK - string response_id PK +node_societal_benefit_area { + int node_id PK "FK" + str societal_benefit_area_id FK "UK" } %% Static reference data: -response_object_type { +node_type { string id PK "name" } societal_benefit_area { @@ -143,52 +100,29 @@ societal_benefit_key_objective { string id PK "name" string societal_benefit_subarea_id FK } -role { - string id PK "name" -} - %% Relationships -survey }|--|| user: "" + +assessment }|--|| user: "" user }|--|| role: "" +node }|--|| user: "" + -response }|--o{ survey: "" -response }o--|| user: "" -response }o--|| response_object: "" +assessment_node ||--|{ link: "points from" +assessment_node ||--|{ link: "points to" -response ||--o{ response_observing_system: "" -response ||--o{ response_data_product: "" -response ||--o{ response_application: "" -response ||--o{ response_application_societal_benefit_area: "" +node ||--|{ assessment_node: "" +assessment ||--|{ assessment_node: "" -response_observing_system }|--|| response_object: "" -response_observing_system ||--o| response_observing_system_observational: "" -response_observing_system ||--o| response_observing_system_research: "" +node ||--o| node_societal_benefit_area: "" +node_societal_benefit_area |o--|| societal_benefit_area: "" -response_data_product }|--|| response_object: "" -response_application }|--|| response_object: "" +node }o--|| node_type: "" +node ||--|| node_other: "" societal_benefit_area ||--|{ societal_benefit_subarea: "" societal_benefit_subarea ||--|{ societal_benefit_key_objective: "" - -response_application_societal_benefit_area ||--|| societal_benefit_area: "" - -response_object }|--|| response_object_type: "" - -analysis }|--|| user: "" - -%% Associative relationships (i.e. relationships with data) -response_observing_system ||--|{ response_observing_system_data_product: "" -response_data_product ||--|{ response_observing_system_data_product: "" - -response_data_product ||--|{ response_data_product_application: "" -response_application ||--|{ response_data_product_application: "" - -response_application ||--|{ response_application_societal_benefit_area: "" - -response }|--o{ analysis_response: "" -analysis }|--o{ analysis_response: "" ``` diff --git a/environment.yml b/environment.yml index b2e7cdc1..ffaeab94 100644 --- a/environment.yml +++ b/environment.yml @@ -45,3 +45,4 @@ dependencies: # TODO: Get these on conda-forge - wtforms-sqlalchemy - bootstrap-flask ~=2.3.0 + - flask-pydantic ~=0.10.0 diff --git a/pyproject.toml b/pyproject.toml index a0e8dc03..3b492f9e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ warn_unused_configs = true warn_unreachable = true # disallow_untyped_defs = true # disallow_incomplete_defs = true -# struct = true +# strict = true [[tool.mypy.overrides]] module = [ @@ -31,6 +31,7 @@ module = [ "flask_wtf.*", "flask_dance.*", "flask_login.*", + "flask_pydantic.*", "wtforms.*", "wtforms_sqlalchemy.*", ] diff --git a/tasks/db.py b/tasks/db.py index 6310e385..c94d952f 100644 --- a/tasks/db.py +++ b/tasks/db.py @@ -4,8 +4,16 @@ @task(aliases=('recreate',)) -def init(ctx, *, load=True): - """Drop and recreate all database tables, loading them by default.""" +def init(ctx, *, reference_data=True): + """Drop and recreate all database tables, loading them by default. + + TODO: Enable loading test data. Currently doesn't work with error `Instance + is not bound to a Session`. Works fine when directly running load_test_data. + + ```python + def init(ctx, *, reference_data=True, test_data=False): + ``` + """ if not in_container(): print( 'Please run from the container context using' @@ -23,10 +31,12 @@ def init(ctx, *, load=True): print('Recreating tables...') recreate_tables_() - if load: - print('Loading reference tables...') + if reference_data: load_reference_data(ctx) + # if test_data: + # load_test_data(ctx) + @task() def load_reference_data(ctx): @@ -44,4 +54,25 @@ def load_reference_data(ctx): app = create_app() with app.app_context(): + print('Loading reference tables...') populate_reference_data() + + +@task() +def load_test_data(ctx): + """Populate operational tables with test data.""" + if not in_container(): + print( + 'Please run from the container context using' + ' `scripts/invoke_in_container.sh`', + ) + return + + from usaon_benefit_tool import create_app + from usaon_benefit_tool.util.db.setup import populate_test_data + + app = create_app() + + with app.app_context(): + print('Loading test data...') + populate_test_data() diff --git a/usaon_benefit_tool/__init__.py b/usaon_benefit_tool/__init__.py index 676c4a18..0f69178a 100644 --- a/usaon_benefit_tool/__init__.py +++ b/usaon_benefit_tool/__init__.py @@ -13,9 +13,11 @@ from werkzeug.middleware.proxy_fix import ProxyFix from usaon_benefit_tool.constants import repo +from usaon_benefit_tool.constants.sankey import DUMMY_NODE_ID from usaon_benefit_tool.constants.version import VERSION from usaon_benefit_tool.util.db.connect import db_connstr from usaon_benefit_tool.util.envvar import envvar_is_true +from usaon_benefit_tool.util.flask_jsglue import JSGlue __version__: Final[str] = VERSION @@ -38,8 +40,25 @@ def create_app(): """Create and configure the app.""" # TODO: enable override config to test_config # https://flask.palletsprojects.com/en/2.3.x/tutorial/factory/ + _monkeypatch() app = Flask(__name__) + _setup_config(app) + _setup_proxy_support(app) + + db.init_app(app) + + Bootstrap5(app) + JSGlue(app) + + _setup_login(app) + _register_blueprints(app) + _register_template_helpers(app) + + return app + + +def _setup_config(app) -> None: app.config['SECRET_KEY'] = os.environ.get('FLASK_SECRET_KEY', 'youcanneverguess') app.config['SQLALCHEMY_DATABASE_URI'] = db_connstr(app) app.config['BOOTSTRAP_BOOTSWATCH_THEME'] = 'cosmo' @@ -52,13 +71,13 @@ def create_app(): # DEV ONLY: Disable login app.config['LOGIN_DISABLED'] = envvar_is_true("USAON_BENEFIT_TOOL_LOGIN_DISABLED") + +def _setup_proxy_support(app) -> None: if envvar_is_true("USAON_BENEFIT_TOOL_PROXY"): app.wsgi_app = ProxyFix(app.wsgi_app, x_prefix=1) # type: ignore - db.init_app(app) - - Bootstrap5(app) +def _setup_login(app) -> None: login_manager = LoginManager() login_manager.login_view = "login.login" login_manager.init_app(app) @@ -86,44 +105,10 @@ def before_request(): if time.time() >= token['expires_at']: del s['google_oauth_token'] - from usaon_benefit_tool.routes.login import google_bp, login_bp - from usaon_benefit_tool.routes.logout import logout_bp - from usaon_benefit_tool.routes.root import root_bp - from usaon_benefit_tool.routes.survey import response_bp, survey_bp - from usaon_benefit_tool.routes.survey.applications import application_bp - from usaon_benefit_tool.routes.survey.data_products import data_product_bp - from usaon_benefit_tool.routes.survey.observing_systems import observing_system_bp - from usaon_benefit_tool.routes.survey.relationships.application_societal_benefit_area import ( - application_societal_benefit_area_bp, - ) - from usaon_benefit_tool.routes.survey.relationships.data_product_application import ( - data_product_application_bp, - ) - from usaon_benefit_tool.routes.survey.relationships.observing_system_data_product import ( - observing_system_data_product_bp, - ) - from usaon_benefit_tool.routes.survey.sbas import societal_benefit_area_bp - from usaon_benefit_tool.routes.surveys import surveys_bp - from usaon_benefit_tool.routes.user import user_bp - from usaon_benefit_tool.routes.users import users_bp - - app.register_blueprint(root_bp) - app.register_blueprint(surveys_bp) - app.register_blueprint(survey_bp) - app.register_blueprint(user_bp) - app.register_blueprint(users_bp) - app.register_blueprint(login_bp) - app.register_blueprint(logout_bp) - app.register_blueprint(google_bp, url_prefix="/google_oauth") - app.register_blueprint(response_bp) - app.register_blueprint(observing_system_bp) - app.register_blueprint(societal_benefit_area_bp) - app.register_blueprint(application_bp) - app.register_blueprint(data_product_bp) - app.register_blueprint(observing_system_data_product_bp) - app.register_blueprint(data_product_application_bp) - app.register_blueprint(application_societal_benefit_area_bp) +def _register_template_helpers(app) -> None: + # TODO: Consider context processors instead? + # https://flask.palletsprojects.com/en/2.3.x/templating/#context-processors app.jinja_env.globals.update( __version__=__version__, sqla_inspect=sqla_inspect, @@ -131,6 +116,9 @@ def before_request(): doc_url=repo.DOC_URL, discuss_url=repo.DISCUSS_URL, current_year=repo.CURRENT_YEAR, + constants={ + "DUMMY_NODE_ID": DUMMY_NODE_ID, + }, ) md = Markdown(extensions=['fenced_code']) @@ -139,4 +127,37 @@ def before_request(): dateformat=lambda date: date.strftime("%Y-%m-%d %H:%M%Z"), ) - return app + +def _register_blueprints(app) -> None: + # TODO: Extract function register_blueprints + from usaon_benefit_tool.routes.assessment import assessment_bp + from usaon_benefit_tool.routes.assessments import assessments_bp + from usaon_benefit_tool.routes.login import google_bp, login_bp + from usaon_benefit_tool.routes.logout import logout_bp + from usaon_benefit_tool.routes.node import node_bp + from usaon_benefit_tool.routes.nodes import nodes_bp + from usaon_benefit_tool.routes.root import root_bp + from usaon_benefit_tool.routes.user import user_bp + from usaon_benefit_tool.routes.users import users_bp + + app.register_blueprint(root_bp) + + app.register_blueprint(user_bp) + app.register_blueprint(users_bp) + app.register_blueprint(login_bp) + app.register_blueprint(logout_bp) + app.register_blueprint(google_bp, url_prefix="/google_oauth") + + app.register_blueprint(assessments_bp) + app.register_blueprint(assessment_bp) + + app.register_blueprint(nodes_bp) + app.register_blueprint(node_bp) + + +def _monkeypatch(): + import wtforms_sqlalchemy + + from usaon_benefit_tool.util.monkeypatch.wtforms_sqlalchemy import model_fields + + wtforms_sqlalchemy.orm.model_fields = model_fields diff --git a/usaon_benefit_tool/_types.py b/usaon_benefit_tool/_types.py index fe9c693c..d8dfcb10 100644 --- a/usaon_benefit_tool/_types.py +++ b/usaon_benefit_tool/_types.py @@ -1,7 +1,16 @@ from enum import Enum -class ObservingSystemType(Enum): - observational = 'observational' - research = 'research' - other = 'other' +# FIXME: We're inheriting from `str` to make this JSON serializable for flask-pydantic. +# Undo this once the lib fixes it. +# https://github.com/bauerji/flask-pydantic/issues/54 +class NodeType(str, Enum): + OBSERVING_SYSTEM = 'observing_system' + DATA_PRODUCT = 'data_product' + APPLICATION = 'application' + SOCIETAL_BENEFIT_AREA = 'societal_benefit_area' + + +class NodeTypeDiscriminator(Enum): + OTHER = "other" + SOCIETAL_BENEFIT_AREA = "sba" diff --git a/usaon_benefit_tool/constants/sankey.py b/usaon_benefit_tool/constants/sankey.py new file mode 100644 index 00000000..d3a428a4 --- /dev/null +++ b/usaon_benefit_tool/constants/sankey.py @@ -0,0 +1,3 @@ +from typing import Final + +DUMMY_NODE_ID: Final = "__DUMMY__" diff --git a/usaon_benefit_tool/constants/status.py b/usaon_benefit_tool/constants/status.py index 4af73990..26459d58 100644 --- a/usaon_benefit_tool/constants/status.py +++ b/usaon_benefit_tool/constants/status.py @@ -1,10 +1,9 @@ from typing import Final -# Note STATUSES[0] is the default value -# Keep that in mind when altering the list -STATUSES: Final[list[str]] = [ - 'work in progress', - 'published', - 'closed', - 'archived', -] +# NOTE: element 0 is the default value. Keep that in mind when altering the list. +ASSESSMENT_STATUSES: Final[dict[str, str]] = { + 'work in progress': 'The assessment is not done yet', + 'published': 'The assessment is complete and visible to the public', + 'closed': 'TODO', + 'archived': 'TODO', +} diff --git a/usaon_benefit_tool/forms.py b/usaon_benefit_tool/forms.py index e6f465d0..840f1826 100644 --- a/usaon_benefit_tool/forms.py +++ b/usaon_benefit_tool/forms.py @@ -1,10 +1,4 @@ -"""Forms corresponding to database models. - -TODO: What can we do to improve syncing between models and forms? Consider -wtforms-sqlalchemy? Consider designing a custom class or type that can represent -everything needed to construct column and field instances, and functions for converting -objects of that class to appropriate field/column? -""" +"""Forms corresponding to database models.""" from functools import partial from flask_wtf import FlaskForm @@ -18,36 +12,22 @@ from usaon_benefit_tool import db from usaon_benefit_tool.models.tables import ( - ResponseApplication, - ResponseApplicationSocietalBenefitArea, - ResponseDataProduct, - ResponseDataProductApplication, - ResponseObservingSystem, - ResponseObservingSystemDataProduct, - ResponseSocietalBenefitArea, - Survey, + Assessment, + AssessmentNode, + Link, + Node, + NodeSubtypeOther, + NodeSubtypeSocietalBenefitArea, User, ) -common_response_object_fields = [ - 'short_name', - 'full_name', - 'organization', - 'funder', - 'funding_country', - 'website', - 'description', - 'contact_name', - 'contact_title', - 'contact_email', - 'tags', - 'version', -] - -app_response_object_fields = [ - *common_response_object_fields, - 'performance_criteria', - 'performance_rating', +node_exclude = [ + 'assessment_nodes', + 'created_by_id', + 'created_by', + 'created_timestamp', + 'updated_timestamp', + 'type', ] @@ -66,6 +46,10 @@ def conv_String(self, field_args, **extra): return super().conv_String(field_args, **extra) +def get_node_label(node: Node) -> str: + return f"Object #{node.id} ({node.type.value}): {node.title}" + + model_form = partial( model_form, converter=CustomModelConverter(), @@ -78,56 +62,42 @@ def conv_String(self, field_args, **extra): BaseModel: DeclarativeMeta = db.Model FORMS_BY_MODEL: dict[BaseModel, FlaskForm] = { - User: model_form( - User, - only=['orcid', 'biography', 'affiliation', 'role'], - field_args={'role': {'get_label': 'id'}}, + Assessment: model_form(Assessment, only=['title', 'description']), + AssessmentNode: model_form( + AssessmentNode, + only=['node'], + field_args={ + 'node': {'get_label': get_node_label}, + }, ), - Survey: model_form(Survey, only=['title', 'description']), - # Response entities ("nodes" from Sankey diagram perspective) - # TODO: Restrict "rating" values to correct range - ResponseObservingSystem: model_form( - ResponseObservingSystem, - only=common_response_object_fields, + Link: model_form( + Link, + field_args={ + 'source_assessment_node': {'get_label': lambda an: get_node_label(an.node)}, + 'target_assessment_node': {'get_label': lambda an: get_node_label(an.node)}, + }, ), - ResponseDataProduct: model_form( - ResponseDataProduct, - only=common_response_object_fields, + NodeSubtypeOther: model_form( + NodeSubtypeOther, + exclude=node_exclude, ), - ResponseApplication: model_form( - ResponseApplication, - only=app_response_object_fields, + NodeSubtypeSocietalBenefitArea: model_form( + NodeSubtypeSocietalBenefitArea, + exclude=[*node_exclude, "societal_benefit_area_id"], + exclude_fk=False, + field_args={ + 'societal_benefit_area': {'get_label': 'id'}, + }, ), - ResponseSocietalBenefitArea: model_form( - ResponseSocietalBenefitArea, - only=['societal_benefit_area'], - field_args={'societal_benefit_area': {'get_label': 'id'}}, - ), - # Response relationships ("edges" from Sankey diagram perspective) - ResponseObservingSystemDataProduct: model_form( - ResponseObservingSystemDataProduct, - only=[ - 'criticality_rating', - 'performance_rating', - 'rationale', - 'needed_improvements', - ], - ), - ResponseDataProductApplication: model_form( - ResponseDataProductApplication, - only=[ - 'criticality_rating', - 'performance_rating', - 'rationale', - 'needed_improvements', - ], - ), - ResponseApplicationSocietalBenefitArea: model_form( - ResponseApplicationSocietalBenefitArea, - only=['performance_rating'], + User: model_form( + User, + only=['orcid', 'biography', 'affiliation', 'role'], + # Helps drop-down display the correct user-facing string + field_args={'role': {'get_label': 'id'}}, ), } + # HACK: Add a submit button so bootstrap-flask's render_form macro can work # TODO: Make this less hacky for form in FORMS_BY_MODEL.values(): - form.submit_button = SubmitField('submit') + form.submit_button = SubmitField('Submit') diff --git a/usaon_benefit_tool/models/tables.py b/usaon_benefit_tool/models/tables.py index 2e84dac2..8478d7a8 100644 --- a/usaon_benefit_tool/models/tables.py +++ b/usaon_benefit_tool/models/tables.py @@ -1,4 +1,4 @@ -"""The VTA survey data model. +"""The Value Tree Analysis assessment data model. WARNING: The type-checker can't save you from yourself in this file; there are many magic strings that need to match class names at runtime. @@ -9,75 +9,28 @@ """ from datetime import datetime -from functools import cache -from typing import Final, NotRequired +from typing import ClassVar from flask_login import UserMixin, current_user -from sqlalchemy import CheckConstraint +from sqlalchemy import CheckConstraint, case from sqlalchemy.ext.declarative import DeclarativeMeta from sqlalchemy.orm import relationship from sqlalchemy.schema import Column, ForeignKey, Index, UniqueConstraint -from sqlalchemy.types import Boolean, DateTime, Enum, Integer, SmallInteger, String -from typing_extensions import TypedDict +from sqlalchemy.types import Boolean, DateTime, Enum, Integer, String from usaon_benefit_tool import db -from usaon_benefit_tool._types import ObservingSystemType -from usaon_benefit_tool.constants.status import STATUSES +from usaon_benefit_tool._types import NodeType, NodeTypeDiscriminator +from usaon_benefit_tool.constants.status import ASSESSMENT_STATUSES # Workaround for missing type stubs for flask-sqlalchemy: # https://github.com/dropbox/sqlalchemy-stubs/issues/76#issuecomment-595839159 +# TODO: Consider adding updated_timestamp there BaseModel: DeclarativeMeta = db.Model -class IORelationship(TypedDict): - input: NotRequired[BaseModel] - output: NotRequired[BaseModel] - - -class IORelationshipMixin: - """Provide a dictionary of input and/or output models related to this entity. - - TODO: Could use a clearer name. We may want to also have a mixin for models - representing relationships instead of entities. - """ - - @classmethod - @cache - def __io__(cls) -> IORelationship: - """Return dictionary of any IO relationships this model has. - - TODO: Fix and remove type-ignore comments. - """ - io: IORelationship = {} - if hasattr(cls, 'input_relationships'): - io['input'] = cls.input_relationships.mapper.class_ # type: ignore - if hasattr(cls, 'output_relationships'): - io['output'] = cls.output_relationships.mapper.class_ # type: ignore - - if io == {}: - raise RuntimeError( - 'Please only use IORelationshipMixin on a model with' - ' input_relationships or output_relationships', - ) - - return io - - -class ResponseObjectFieldMixin: - """Provide shared fields between all relationship objects to reduce repetition.""" - - short_name = Column(String(256), nullable=False) - full_name = Column(String(256), nullable=True) - organization = Column(String(256), nullable=False) - funder = Column(String(256), nullable=False) - funding_country = Column(String(256), nullable=False) - website = Column(String(256), nullable=True) - description = Column(String(512), nullable=True) - contact_name = Column(String(256), nullable=False) - contact_title = Column(String(256), nullable=True) - contact_email = Column(String(256), nullable=False) - tags = Column(String, nullable=False) - version = Column(String(64), nullable=True) +#################### +# Association tables +#################### class User(BaseModel, UserMixin): @@ -101,7 +54,7 @@ class User(BaseModel, UserMixin): nullable=False, ) orcid = Column( - String(64), # how long are orcids? + String(64), # TODO: Exactly how long are orcids? Validator? nullable=True, ) role_id = Column( @@ -118,40 +71,47 @@ class User(BaseModel, UserMixin): String, nullable=True, ) + role = relationship('Role') - responses = relationship( - 'Response', + # TODO: Rename "created_assessments"? Add "filled_assessments"? + assessments = relationship( + 'Assessment', back_populates='created_by', ) - surveys = relationship( - 'Survey', + nodes = relationship( + 'Node', back_populates='created_by', ) -class Survey(BaseModel): - __tablename__ = 'survey' +class Assessment(BaseModel): + __tablename__ = 'assessment' id = Column( Integer, primary_key=True, autoincrement=True, ) - response_id = Column( - Integer, - ForeignKey('response.id'), + title = Column( + String(128), + nullable=False, + ) + + description = Column( + String(512), nullable=True, ) - title = Column( - String(128), + private = Column( + Boolean, nullable=False, + default=False, ) status_id = Column( String, ForeignKey('status.id'), - default=STATUSES[0], + default=next(iter(ASSESSMENT_STATUSES.keys())), nullable=False, ) @@ -167,36 +127,71 @@ class Survey(BaseModel): nullable=False, default=datetime.now, ) - - description = Column( - String(512), - nullable=True, - ) - - private = Column( - Boolean, + updated_timestamp = Column( + DateTime, nullable=False, - default=False, + default=datetime.now, + onupdate=datetime.now, ) - response = relationship( - 'Response', - back_populates='survey', - ) created_by = relationship( 'User', - back_populates='surveys', + back_populates='assessments', + ) + status = relationship( + 'AssessmentStatus', + back_populates="assessments", + ) + assessment_nodes = relationship( + 'AssessmentNode', + back_populates="assessment", ) - status = relationship('Status') + # TODO: Re-enable this convenience relationship which passes through the association + # table. Disabled to enable defining AssessmentNode table in a consistent way; to + # make this work it seems you need to use the Table() constructor, but that was + # causing problems with constructing a relationship to Links table. + # nodes = relationship( + # 'Node', + # secondary=assessment_node, + # back_populates='assessments', + # ) -class Response(BaseModel): - __tablename__ = 'response' +class Node(BaseModel): + """The "Node Library".""" + + __tablename__ = 'node' id = Column( Integer, primary_key=True, autoincrement=True, ) + type = Column( + Enum(NodeType), + nullable=False, + ) + __mapper_args__: ClassVar = { + # 'polymorphic_identity': 'none', # TODO: Do we need this? + 'polymorphic_on': case( + [ + ( + type == NodeType.SOCIETAL_BENEFIT_AREA, + NodeTypeDiscriminator.SOCIETAL_BENEFIT_AREA.value, + ), + ], + else_=NodeTypeDiscriminator.OTHER.value, + ), + } + + title = Column(String(128), nullable=False) + short_name = Column(String(256), nullable=False) + description = Column(String(512), nullable=True) + + # TODO: Implement tags! + # tags = Column(String, nullable=False) + # TODO: Implement versioning! + # version = Column(String(64), nullable=True) + created_by_id = Column( Integer, ForeignKey('user.id'), @@ -212,232 +207,138 @@ class Response(BaseModel): DateTime, nullable=False, default=datetime.now, + onupdate=datetime.now, ) - survey = relationship( - 'Survey', - back_populates='response', - ) created_by = relationship( 'User', - back_populates='responses', - ) - observing_systems = relationship( - 'ResponseObservingSystem', - back_populates='response', + back_populates='nodes', ) - data_products = relationship( - 'ResponseDataProduct', - back_populates='response', - ) - applications = relationship( - 'ResponseApplication', - back_populates='response', - ) - societal_benefit_areas = relationship( - 'ResponseSocietalBenefitArea', - back_populates='response', + assessment_nodes = relationship( + 'AssessmentNode', + back_populates="node", ) + # TODO: Re-enable this convenience relationship which passes through the association + # table. Disabled to enable defining AssessmentNode table in a consistent way; to + # make this work it seems you need to use the Table() constructor, but that was + # causing problems with constructing a relationship to Links table. + # assessments = relationship( + # 'Assessment', + # secondary=assessment_node, + # back_populates='nodes', + # ) -class ResponseObservingSystem(BaseModel, IORelationshipMixin, ResponseObjectFieldMixin): - __tablename__ = 'response_observing_system' - __table_args__ = (UniqueConstraint('short_name', 'response_id'),) - id = Column( - Integer, - primary_key=True, - autoincrement=True, - ) - response_id = Column( - Integer, - ForeignKey('response.id'), - nullable=False, - ) +class NodeSubtypeOther(Node): + """Fields that are used for all node types except societal benefit area.""" - type = Column( - Enum(ObservingSystemType), - nullable=False, - ) - - __mapper_args__: Final[dict] = { - 'polymorphic_identity': ObservingSystemType.other, - 'polymorphic_on': type, + __tablename__ = "node_subtype_other" + __mapper_args__: ClassVar = { + 'polymorphic_identity': NodeTypeDiscriminator.OTHER.value, } - response = relationship( - 'Response', - back_populates='observing_systems', - ) - output_relationships = relationship( - 'ResponseObservingSystemDataProduct', - back_populates='observing_system', - cascade="all, delete", - ) - - -class ResponseObservingSystemObservational(BaseModel): - __tablename__ = 'response_observing_system_observational' - __mapper_args__: Final[dict] = { - 'polymorphic_identity': ObservingSystemType.observational, - } - - response_observing_system_id = Column( + node_id = Column( Integer, - ForeignKey('response_observing_system.id'), + ForeignKey('node.id'), primary_key=True, + nullable=False, ) - platform = Column(String(256), nullable=False) - sensor = Column(String(256), nullable=False) - - -class ResponseObservingSystemResearch(BaseModel): - __tablename__ = 'response_observing_system_research' - __mapper_args__: Final[dict] = { - 'polymorphic_identity': ObservingSystemType.research, - } + organization = Column(String(256), nullable=False) + funder = Column(String(256), nullable=False) + funding_country = Column(String(256), nullable=False) + # TODO: do we need multiple website fields? + website = Column(String(256), nullable=True) + contact_information = Column(String(256), nullable=False) + persistent_identifier = Column(String(256), nullable=True) + hypothetical = Column(Boolean) - response_observing_system_id = Column( - Integer, - ForeignKey('response_observing_system.id'), - primary_key=True, - ) - intermediate_product = Column(String(256), nullable=False) +class NodeSubtypeSocietalBenefitArea(Node): + """Fields that are specific to societal benefit area type nodes.""" + __tablename__ = "node_subtype_societal_benefit_area" + __table_args__ = (UniqueConstraint('societal_benefit_area_id'),) + __mapper_args__: ClassVar = { + 'polymorphic_identity': NodeTypeDiscriminator.SOCIETAL_BENEFIT_AREA.value, + } -class ResponseDataProduct(BaseModel, IORelationshipMixin, ResponseObjectFieldMixin): - __tablename__ = 'response_data_product' - __table_args__ = (UniqueConstraint('short_name', 'response_id'),) - id = Column( + node_id = Column( Integer, + ForeignKey('node.id'), primary_key=True, - autoincrement=True, + nullable=False, ) - response_id = Column( - Integer, - ForeignKey('response.id'), + societal_benefit_area_id = Column( + String, + ForeignKey('societal_benefit_area.id'), nullable=False, ) - response = relationship( - 'Response', - back_populates='data_products', - ) - input_relationships = relationship( - 'ResponseObservingSystemDataProduct', - back_populates='data_product', - cascade="all, delete", - ) - output_relationships = relationship( - 'ResponseDataProductApplication', - back_populates='data_product', - cascade="all, delete", - ) + # TODO: Relationship to societal benefit area table? How would we make a similar + # relationship for the other node types? + societal_benefit_area = relationship("SocietalBenefitArea") -class ResponseApplication(BaseModel, IORelationshipMixin, ResponseObjectFieldMixin): - __tablename__ = 'response_application' - __table_args__ = (UniqueConstraint('short_name', 'response_id'),) - id = Column( - Integer, - primary_key=True, - autoincrement=True, - ) - response_id = Column( - Integer, - ForeignKey('response.id'), - nullable=False, - ) +class AssessmentNode(BaseModel): + """An instance of a Node in the Node Library for use in an Assessment.""" - performance_criteria = Column( - String(256), - ) - performance_rating = Column( - Integer, - CheckConstraint( - 'performance_rating>0 and performance_rating<101', - name='0-100', + __tablename__ = "assessment_node" + __table_args__ = ( + UniqueConstraint('assessment_id', 'node_id'), + Index( + f'idx_{__tablename__}', + 'assessment_id', + 'node_id', + unique=True, # TODO: Do we need this? ), - nullable=False, ) - response = relationship( - 'Response', - back_populates='applications', - ) - input_relationships = relationship( - 'ResponseDataProductApplication', - back_populates='application', - cascade="all, delete", - ) - output_relationships = relationship( - 'ResponseApplicationSocietalBenefitArea', - back_populates='application', - cascade="all, delete", - ) - - -class ResponseSocietalBenefitArea(BaseModel, IORelationshipMixin): - __tablename__ = 'response_societal_benefit_area' - __table_args__ = (UniqueConstraint('societal_benefit_area_id', 'response_id'),) + # TODO: If/when we make this a pure associative entity, do we need a surrogate ID? id = Column( Integer, primary_key=True, - autoincrement=True, ) - response_id = Column( + assessment_id = Column( Integer, - ForeignKey('response.id'), + ForeignKey('assessment.id'), nullable=False, ) - - societal_benefit_area_id = Column( - String(256), - ForeignKey('societal_benefit_area.id'), + node_id = Column( + Integer, + ForeignKey('node.id'), nullable=False, ) - response = relationship( - 'Response', - back_populates='societal_benefit_areas', - ) - societal_benefit_area = relationship('SocietalBenefitArea') - input_relationships = relationship( - 'ResponseApplicationSocietalBenefitArea', - back_populates='societal_benefit_area', - cascade="all, delete", - ) + # TODO: Special case for applications, they have performance_criteria and + # performance_rating. + assessment = relationship(Assessment) + node = relationship(Node) -# Association tables -class Status(BaseModel): - __tablename__ = 'status' - id = Column( - String, - primary_key=True, - nullable=False, + input_links = relationship( + "Link", + foreign_keys="Link.target_assessment_node_id", + back_populates="target_assessment_node", ) - - -class Role(BaseModel): - __tablename__ = 'role' - id = Column( - String, - primary_key=True, - nullable=False, + output_links = relationship( + "Link", + foreign_keys="Link.source_assessment_node_id", + back_populates="source_assessment_node", ) -class ResponseObservingSystemDataProduct(BaseModel): - __tablename__ = 'response_observing_system_data_product' +class Link(BaseModel): + """A link between two nodes _in an assessment_.""" + + __tablename__ = 'link' __table_args__ = ( - UniqueConstraint('response_observing_system_id', 'response_data_product_id'), + UniqueConstraint('source_assessment_node_id', 'target_assessment_node_id'), Index( f'idx_{__tablename__}', - 'response_observing_system_id', - 'response_data_product_id', - unique=True, + 'source_assessment_node_id', + 'target_assessment_node_id', + unique=True, # TODO: Do we need this? ), ) id = Column( @@ -445,153 +346,70 @@ class ResponseObservingSystemDataProduct(BaseModel): autoincrement=True, primary_key=True, ) - response_observing_system_id = Column( + source_assessment_node_id = Column( Integer, - ForeignKey('response_observing_system.id'), - index=True, + ForeignKey('assessment_node.id'), + nullable=False, ) - response_data_product_id = Column( + target_assessment_node_id = Column( Integer, - ForeignKey('response_data_product.id'), - index=True, - ) - - criticality_rating = Column( - SmallInteger, - CheckConstraint( - 'criticality_rating>=0 and criticality_rating<=100', - name='c0-100', - ), + ForeignKey('assessment_node.id'), nullable=False, ) + performance_rating = Column( - SmallInteger, + Integer, CheckConstraint( - 'performance_rating>=0 and performance_rating<=100', + 'performance_rating>0 and performance_rating<101', name='0-100', ), nullable=False, ) - rationale = Column(String(512), nullable=True) - needed_improvements = Column(String(512), nullable=True) - observing_system = relationship( - 'ResponseObservingSystem', - back_populates='output_relationships', + source_assessment_node = relationship( + AssessmentNode, + foreign_keys=[source_assessment_node_id], + back_populates='output_links', ) - data_product = relationship( - 'ResponseDataProduct', - back_populates='input_relationships', + target_assessment_node = relationship( + AssessmentNode, + foreign_keys=[target_assessment_node_id], + back_populates='input_links', ) -class ResponseDataProductApplication(BaseModel): - __tablename__ = 'response_data_product_application' - __table_args__ = ( - UniqueConstraint('response_data_product_id', 'response_application_id'), - Index( - f'idx_{__tablename__}', - 'response_data_product_id', - 'response_application_id', - unique=True, - ), - ) - id = Column( - Integer, - autoincrement=True, - primary_key=True, - ) +################## +# Reference tables +################## - response_data_product_id = Column( - Integer, - ForeignKey('response_data_product.id'), - index=True, - ) - response_application_id = Column( - Integer, - ForeignKey('response_application.id'), - index=True, - ) - criticality_rating = Column( - SmallInteger, - CheckConstraint( - 'criticality_rating>=0 and criticality_rating<=100', - name='c0-100', - ), - nullable=False, - ) - performance_rating = Column( - SmallInteger, - CheckConstraint( - 'performance_rating>=0 and performance_rating<=100', - name='0-100', - ), +# TODO: Is this "association" or "reference" +class Role(BaseModel): + __tablename__ = 'role' + id = Column( + String, + primary_key=True, nullable=False, ) - rationale = Column(String(512), nullable=True) - needed_improvements = Column(String(512), nullable=True) - - data_product = relationship( - 'ResponseDataProduct', - back_populates='output_relationships', - ) - application = relationship( - 'ResponseApplication', - back_populates='input_relationships', - ) -class ResponseApplicationSocietalBenefitArea(BaseModel): - __tablename__ = 'response_application_societal_benefit_area' - __table_args__ = ( - UniqueConstraint( - 'response_application_id', - 'response_societal_benefit_area_id', - ), - Index( - 'idx_{__tablename__}', - 'response_application_id', - 'response_societal_benefit_area_id', - unique=True, - ), - ) +class AssessmentStatus(BaseModel): + __tablename__ = 'status' id = Column( - Integer, - autoincrement=True, + String, primary_key=True, ) - response_application_id = Column( - Integer, - ForeignKey('response_application.id'), - index=True, - ) - response_societal_benefit_area_id = Column( - Integer, - ForeignKey('response_societal_benefit_area.id'), - index=True, - ) - - performance_rating = Column( - SmallInteger, - CheckConstraint( - 'performance_rating>=0 and performance_rating<=100', - name='0-100', - ), + description = Column( + String, nullable=False, ) - application = relationship( - 'ResponseApplication', - back_populates='output_relationships', - ) - societal_benefit_area = relationship( - 'ResponseSocietalBenefitArea', - back_populates='input_relationships', + assessments = relationship( + Assessment, + back_populates='status', ) -# Reference tables class SocietalBenefitArea(BaseModel): __tablename__ = 'societal_benefit_area' id = Column( diff --git a/usaon_benefit_tool/routes/assessment/__init__.py b/usaon_benefit_tool/routes/assessment/__init__.py new file mode 100644 index 00000000..b20706f5 --- /dev/null +++ b/usaon_benefit_tool/routes/assessment/__init__.py @@ -0,0 +1,47 @@ +from flask import Blueprint, render_template +from flask_login import login_required + +from usaon_benefit_tool import db +from usaon_benefit_tool.models.tables import Assessment + +# from usaon_benefit_tool.routes.assessment.link import assessment_link_bp +from usaon_benefit_tool.routes.assessment.links import assessment_links_bp + +# from usaon_benefit_tool.routes.assessment.node import assessment_node_bp +from usaon_benefit_tool.routes.assessment.nodes import ( + assessment_nodes_bp, +) +from usaon_benefit_tool.util.sankey import sankey + +assessment_bp = Blueprint( + 'assessment', + __name__, + url_prefix='/assessment/', +) +assessment_bp.register_blueprint(assessment_links_bp) +# assessment_bp.register_blueprint(assessment_link_bp) +assessment_bp.register_blueprint(assessment_nodes_bp) +# assessment_bp.register_blueprint(assessment_node_bp) + + +@assessment_bp.route('') +@login_required +def get(assessment_id: str): + """Display the assessment overview.""" + assessment = db.get_or_404(Assessment, assessment_id) + return render_template( + 'assessment/overview.html', + assessment=assessment, + sankey_series=sankey(assessment), + ) + + +@assessment_bp.route('/user_guide', methods=['GET']) +@login_required +def user_guide(assessment_id: str): + """Display the assessment user guide.""" + assessment = db.get_or_404(Assessment, assessment_id) + return render_template( + 'assessment/user_guide.html', + assessment=assessment, + ) diff --git a/usaon_benefit_tool/routes/assessment/links.py b/usaon_benefit_tool/routes/assessment/links.py new file mode 100644 index 00000000..e4d43662 --- /dev/null +++ b/usaon_benefit_tool/routes/assessment/links.py @@ -0,0 +1,50 @@ +from flask import Blueprint, Response, render_template, request, url_for +from flask_login import login_required + +from usaon_benefit_tool import db +from usaon_benefit_tool.forms import FORMS_BY_MODEL +from usaon_benefit_tool.models.tables import Link + +assessment_links_bp = Blueprint('links', __name__, url_prefix='/links') +Form = FORMS_BY_MODEL[Link] + + +@assessment_links_bp.route('', methods=['POST']) +@login_required +def post(assessment_id: str): + """Add an entry to the assessment's link collection.""" + assessment_link = Link() + form = Form(request.form, obj=assessment_link) + + if form.validate(): + form.populate_obj(assessment_link) + db.session.add(assessment_link) + db.session.commit() + + return Response( + status=201, + headers={ + 'HX-Redirect': url_for( + 'assessment.get', + assessment_id=assessment_id, + ), + }, + ) + + +@assessment_links_bp.route('/form', methods=['GET']) +@login_required +def form(assessment_id: str): + """Return a form to add an entry to the assessment's link collection.""" + assessment_link = Link() + form = Form(obj=assessment_link) + form_attrs = ( + f"hx-post={url_for('assessment.links.post', assessment_id=assessment_id)}" + ) + + return render_template( + 'partials/modal_form.html', + title="Add a link", + form_attrs=form_attrs, + form=form, + ) diff --git a/usaon_benefit_tool/routes/assessment/nodes.py b/usaon_benefit_tool/routes/assessment/nodes.py new file mode 100644 index 00000000..b22c2d8f --- /dev/null +++ b/usaon_benefit_tool/routes/assessment/nodes.py @@ -0,0 +1,67 @@ +from flask import Blueprint, Response, render_template, request, url_for +from flask_login import login_required + +from usaon_benefit_tool import db +from usaon_benefit_tool.forms import FORMS_BY_MODEL +from usaon_benefit_tool.models.tables import AssessmentNode + +assessment_nodes_bp = Blueprint('nodes', __name__, url_prefix='/nodes') +Form = FORMS_BY_MODEL[AssessmentNode] + + +# @assessment_nodes_bp.route('', methods=['GET']) +# @login_required +# def get(assessment_id: str): +# """Return a page for managing data products associated with a assessment.""" +# assessment = db.get_or_404(Assessment, assessment_id) +# assessment_data_product = AssessmentDataProduct(assessment_id=assessment.id) +# +# form = Form(obj=assessment_data_product) +# return render_template( +# 'assessment/nodes.html', +# form=form, +# assessment=assessment, +# nodes=assessment.nodes, +# sankey_series=sankey_subset(assessment, AssessmentDataProduct), +# ) + + +@assessment_nodes_bp.route('', methods=['POST']) +@login_required +def post(assessment_id: str): + """Add an entry to the assessment's node collection.""" + assessment_node = AssessmentNode(assessment_id=assessment_id) + form = Form(request.form, obj=assessment_node) + + if form.validate(): + form.populate_obj(assessment_node) + db.session.add(assessment_node) + db.session.commit() + + return Response( + status=201, + headers={ + 'HX-Redirect': url_for( + 'assessment.get', + assessment_id=assessment_id, + ), + }, + ) + + +@assessment_nodes_bp.route('/form', methods=['GET']) +@login_required +def form(assessment_id: str): + """Return a form to add an entry to the assessment's nodes collection.""" + assessment_node = AssessmentNode(assessment_id=assessment_id) + form = Form(obj=assessment_node) + form_attrs = ( + f"hx-post={url_for('assessment.nodes.post', assessment_id=assessment_id)}" + ) + + return render_template( + 'partials/modal_form.html', + title="Add a node", + form_attrs=form_attrs, + form=form, + ) diff --git a/usaon_benefit_tool/routes/assessments.py b/usaon_benefit_tool/routes/assessments.py new file mode 100644 index 00000000..7a0ba498 --- /dev/null +++ b/usaon_benefit_tool/routes/assessments.py @@ -0,0 +1,42 @@ +from flask import ( + Blueprint, + redirect, + render_template, + request, + url_for, +) +from flask_login import login_required + +from usaon_benefit_tool import db +from usaon_benefit_tool.forms import FORMS_BY_MODEL +from usaon_benefit_tool.models.tables import Assessment + +assessments_bp = Blueprint('assessments', __name__, url_prefix='/assessments') + + +@assessments_bp.route('', methods=["GET"]) +@login_required +def get(): + assessments = Assessment.query.order_by(Assessment.created_timestamp).all() + form = FORMS_BY_MODEL[Assessment](obj=Assessment()) + return render_template('assessments.html', assessments=assessments, form=form) + + +@assessments_bp.route('', methods=["POST"]) +@login_required +def post(): + assessment = Assessment() + form = FORMS_BY_MODEL[Assessment](request.form, obj=assessment) + + if not form.validate(): + # FIXME: Handle this case! Return an error code and let HTMX handle it? + # breakpoint() + raise RuntimeError("FIXME") + + # Insert to DB + form.populate_obj(assessment) + + db.session.add(assessment) + db.session.commit() + + return redirect(url_for('assessment.get', assessment_id=assessment.id)) diff --git a/usaon_benefit_tool/routes/node/__init__.py b/usaon_benefit_tool/routes/node/__init__.py new file mode 100644 index 00000000..5e2142a5 --- /dev/null +++ b/usaon_benefit_tool/routes/node/__init__.py @@ -0,0 +1,80 @@ +from flask import Blueprint, Response, render_template, request, url_for +from flask_login import login_required + +from usaon_benefit_tool import db +from usaon_benefit_tool.forms import FORMS_BY_MODEL +from usaon_benefit_tool.models.tables import Node + +# TODO: Separate endpoints for each type of node? /node/data_product/... +# Or /node/?type=data_product or /node/', methods=['GET']) -@login_required -def view_response(survey_id: str): - """View or create response to a survey.""" - # Anyone should be able to view a survey - # Only admins or respondents should be able to create a response. - survey = db.get_or_404(Survey, survey_id) - - return render_template( - 'survey/user_guide.html', - survey=survey, - response=survey.response, - sankey_series=sankey(survey.response), - ) - - -@survey_bp.route('/new', methods=['GET', 'POST']) -@login_required -def new_survey(): - Form = FORMS_BY_MODEL[Survey] - survey = Survey() - - if request.method == 'POST': - form = Form(request.form, obj=survey) - - if form.validate(): - # Insert to DB - # TODO: We don't need a response concept! - form.populate_obj(survey) - - response = Response() - survey.response = response - - db.session.add(survey) - db.session.add(response) - db.session.commit() - - return redirect(url_for('survey.view_survey', survey_id=survey.id)) - - form = Form(obj=survey) - return render_template('new_survey.html', form=form) - - -@survey_bp.route('/') -@login_required -def view_survey(survey_id: str): - # Fetch survey by id - survey = db.get_or_404(Survey, survey_id) - - return render_template( - 'survey/overview.html', - survey=survey, - sankey_series=sankey(survey.response) if survey.response else [], - ) diff --git a/usaon_benefit_tool/routes/survey/applications.py b/usaon_benefit_tool/routes/survey/applications.py deleted file mode 100644 index b6b5093a..00000000 --- a/usaon_benefit_tool/routes/survey/applications.py +++ /dev/null @@ -1,61 +0,0 @@ -from flask import Blueprint, redirect, render_template, request, url_for -from flask_login import login_required - -from usaon_benefit_tool import db -from usaon_benefit_tool.forms import FORMS_BY_MODEL -from usaon_benefit_tool.models.tables import ResponseApplication, Survey -from usaon_benefit_tool.util.authorization import limit_response_editors -from usaon_benefit_tool.util.sankey import applications_sankey - -application_bp = Blueprint( - 'application', - __name__, - url_prefix='/response//applications', -) - - -@application_bp.route('', methods=['GET', 'POST']) -@login_required -def view_response_applications(survey_id: int): - """View and add to applications associated with a response.""" - Form = FORMS_BY_MODEL[ResponseApplication] - survey = db.get_or_404(Survey, survey_id) - response_application = ResponseApplication(response_id=survey.response_id) - - if request.method == 'POST': - limit_response_editors() - form = Form(request.form, obj=response_application) - - if form.validate(): - form.populate_obj(response_application) - db.session.add(response_application) - db.session.commit() - - return redirect( - url_for('application.view_response_applications', survey_id=survey.id), - ) - - form = Form(obj=response_application) - return render_template( - 'survey/applications.html', - form=form, - survey=survey, - response=survey.response, - applications=survey.response.applications, - sankey_series=applications_sankey(survey.response), - ) - - -@application_bp.route('/', methods=['DELETE']) -@login_required -def delete_response_application(survey_id: int, response_application_id: int): - """Delete application response object from survey.""" - survey = db.get_or_404(Survey, survey_id) - response_application = db.get_or_404(ResponseApplication, response_application_id) - db.session.delete(response_application) - db.session.commit() - - return redirect( - url_for('application.view_response_applications', survey_id=survey.id), - code=303, - ) diff --git a/usaon_benefit_tool/routes/survey/data_products.py b/usaon_benefit_tool/routes/survey/data_products.py deleted file mode 100644 index d321a212..00000000 --- a/usaon_benefit_tool/routes/survey/data_products.py +++ /dev/null @@ -1,61 +0,0 @@ -from flask import Blueprint, redirect, render_template, request, url_for -from flask_login import login_required - -from usaon_benefit_tool import db -from usaon_benefit_tool.forms import FORMS_BY_MODEL -from usaon_benefit_tool.models.tables import ResponseDataProduct, Survey -from usaon_benefit_tool.util.authorization import limit_response_editors -from usaon_benefit_tool.util.sankey import data_products_sankey - -data_product_bp = Blueprint( - 'data_product', - __name__, - url_prefix='/response//data_products', -) - - -@data_product_bp.route('', methods=['GET', 'POST']) -@login_required -def view_response_data_products(survey_id: str): - """View and add to data products associated with a response.""" - Form = FORMS_BY_MODEL[ResponseDataProduct] - survey = db.get_or_404(Survey, survey_id) - response_data_product = ResponseDataProduct(response_id=survey.response_id) - - if request.method == 'POST': - limit_response_editors() - form = Form(request.form, obj=response_data_product) - - if form.validate(): - form.populate_obj(response_data_product) - db.session.add(response_data_product) - db.session.commit() - - return redirect( - url_for('data_product.view_response_data_products', survey_id=survey.id), - ) - - form = Form(obj=response_data_product) - return render_template( - 'survey/data_products.html', - form=form, - survey=survey, - response=survey.response, - data_products=survey.response.data_products, - sankey_series=data_products_sankey(survey.response), - ) - - -@data_product_bp.route('/', methods=['DELETE']) -@login_required -def delete_response_data_product(survey_id: int, response_data_product_id: int): - """Delete data product response object from survey.""" - survey = db.get_or_404(Survey, survey_id) - response_data_product = db.get_or_404(ResponseDataProduct, response_data_product_id) - db.session.delete(response_data_product) - db.session.commit() - - return redirect( - url_for('data_product.view_response_data_products', survey_id=survey.id), - code=303, - ) diff --git a/usaon_benefit_tool/routes/survey/observing_systems.py b/usaon_benefit_tool/routes/survey/observing_systems.py deleted file mode 100644 index 23d942d7..00000000 --- a/usaon_benefit_tool/routes/survey/observing_systems.py +++ /dev/null @@ -1,68 +0,0 @@ -from flask import Blueprint, redirect, render_template, request, url_for -from flask_login import login_required - -from usaon_benefit_tool import db -from usaon_benefit_tool._types import ObservingSystemType -from usaon_benefit_tool.forms import FORMS_BY_MODEL -from usaon_benefit_tool.models.tables import ResponseObservingSystem, Survey -from usaon_benefit_tool.util.authorization import limit_response_editors - -observing_system_bp = Blueprint( - 'obs', - __name__, - url_prefix='/response//observing_systems', -) - - -@observing_system_bp.route('', methods=['GET', 'POST']) -@login_required -def view_response_observing_systems(survey_id: str): - """View and add to observing systems associated with a response.""" - Form = FORMS_BY_MODEL[ResponseObservingSystem] - survey = db.get_or_404(Survey, survey_id) - response_observing_system = ResponseObservingSystem( - response_id=survey.response_id, - type=ObservingSystemType.other, - ) - - if request.method == 'POST': - limit_response_editors() - form = Form(request.form, obj=response_observing_system) - - if form.validate(): - form.populate_obj(response_observing_system) - db.session.add(response_observing_system) - db.session.commit() - - redirect_url = url_for( - 'obs.view_response_observing_systems', - survey_id=survey.id, - ) - return redirect(redirect_url) - - form = Form(obj=response_observing_system) - return render_template( - 'survey/observing_systems.html', - form=form, - survey=survey, - response=survey.response, - observing_systems=survey.response.observing_systems, - ) - - -@observing_system_bp.route('/', methods=['DELETE']) -@login_required -def delete_response_observing_system(survey_id: int, response_observing_system_id: int): - """Delete observing system response object from survey.""" - survey = db.get_or_404(Survey, survey_id) - response_observing_system = db.get_or_404( - ResponseObservingSystem, - response_observing_system_id, - ) - db.session.delete(response_observing_system) - db.session.commit() - - return redirect( - url_for('obs.view_response_observing_systems', survey_id=survey.id), - code=303, - ) diff --git a/usaon_benefit_tool/routes/survey/relationships/application_societal_benefit_area.py b/usaon_benefit_tool/routes/survey/relationships/application_societal_benefit_area.py deleted file mode 100644 index ab9195c9..00000000 --- a/usaon_benefit_tool/routes/survey/relationships/application_societal_benefit_area.py +++ /dev/null @@ -1,263 +0,0 @@ -from flask import Blueprint, Request, redirect, render_template, request, url_for -from wtforms import FormField - -from usaon_benefit_tool import db -from usaon_benefit_tool.forms import FORMS_BY_MODEL -from usaon_benefit_tool.models.tables import ( - ResponseApplication, - ResponseApplicationSocietalBenefitArea, - ResponseSocietalBenefitArea, - Survey, -) -from usaon_benefit_tool.util.authorization import limit_response_editors -from usaon_benefit_tool.util.superform import SuperForm - - -def _update_super_form( - super_form: type[SuperForm], - /, - *, - societal_benefit_area_id: int | None, - application_id: int | None, -) -> None: - """Populate the form of forms with sub-forms depending on provided IDs. - - When an ID for an object is not provided, we need to gather information from the - user to create that object. - - TODO: Better function name. - """ - if societal_benefit_area_id is None: - super_form.societal_benefit_area = FormField( - FORMS_BY_MODEL[ResponseSocietalBenefitArea], - ) - - if application_id is None: - super_form.application = FormField(FORMS_BY_MODEL[ResponseApplication]) - - -def _update_relationship( - relationship: ResponseSocietalBenefitArea, - *, - societal_benefit_area_id: int | None, - application_id: int | None, -) -> None: - """Populate the relationship with any known identifiers. - - TODO: Better function name. - """ - if societal_benefit_area_id: - relationship.response_societal_benefit_area_id = societal_benefit_area_id - - if application_id: - relationship.response_application_id = application_id - - -def _response_societal_benefit_area( - *, - societal_benefit_area_id: int | None, - response_id: int, -) -> ResponseSocietalBenefitArea: - """Return a SBA db object (or 404).""" - if societal_benefit_area_id is not None: - response_societal_benefit_area = db.get_or_404( - ResponseSocietalBenefitArea, - societal_benefit_area_id, - ) - else: - response_societal_benefit_area = ResponseSocietalBenefitArea( - response_id=response_id, - ) - - return response_societal_benefit_area - - -def _response_application( - *, - application_id: int | None, - response_id: int, -) -> ResponseApplication: - """Return an application db object (or 404).""" - if application_id is not None: - response_application = db.get_or_404(ResponseApplication, application_id) - else: - response_application = ResponseApplication(response_id=response_id) - - return response_application - - -def _response_application_societal_benefit_area( - *, - societal_benefit_area_id: int | None, - application_id: int | None, -) -> ResponseApplicationSocietalBenefitArea: - """Return a relationship db object. - - Returned object may be transient or persistent depending on whether a match exists - in the db. - """ - if societal_benefit_area_id and application_id: - # If not found, will be `None` - response_application_societal_benefit_area = ( - db.session.query(ResponseApplicationSocietalBenefitArea) - .filter( - ResponseApplicationSocietalBenefitArea.response_societal_benefit_area_id - == societal_benefit_area_id - and ResponseApplicationSocietalBenefitArea.response_application_id - == application_id, - ) - .one_or_none() - ) - else: - response_application_societal_benefit_area = None - - if response_application_societal_benefit_area is not None: - return response_application_societal_benefit_area - else: - return ResponseApplicationSocietalBenefitArea() - - -def _request_args(request: Request) -> tuple[int | None, int | None]: - societal_benefit_area_id: int | str | None = request.args.get( - 'societal_benefit_area_id', - ) - if societal_benefit_area_id is not None: - societal_benefit_area_id = int(societal_benefit_area_id) - - application_id: int | str | None = request.args.get('application_id') - if application_id is not None: - application_id = int(application_id) - - return societal_benefit_area_id, application_id - - -application_societal_benefit_area_bp = Blueprint( - 'application_societal_benefit_area', - __name__, - url_prefix='/response//application_societal_benefit_area_relationships', -) - - -@application_societal_benefit_area_bp.route( - '', - methods=['GET', 'POST'], -) -def view_response_application_societal_benefit_area_relationships(survey_id: str): - """View and add application/SBA relationships to a response. - - TODO: Refactor this whole pile of stuff. Less string magic. Less cyclomatic - complexity. - """ - societal_benefit_area_id, application_id = _request_args(request) - survey = db.get_or_404(Survey, survey_id) - - class ApplicationSocietalBenefitAreaForm(SuperForm): - """Combine all necessary forms into one super-form. - - NOTE: Additional class attributes are added dynamically below. - """ - - relationship = FormField(FORMS_BY_MODEL[ResponseApplicationSocietalBenefitArea]) - - response_application_societal_benefit_area = ( - _response_application_societal_benefit_area( - societal_benefit_area_id=societal_benefit_area_id, - application_id=application_id, - ) - ) - - response_societal_benefit_area = _response_societal_benefit_area( - societal_benefit_area_id=societal_benefit_area_id, - response_id=survey.response_id, - ) - - response_application = _response_application( - application_id=application_id, - response_id=survey.response_id, - ) - - _update_super_form( - ApplicationSocietalBenefitAreaForm, - societal_benefit_area_id=societal_benefit_area_id, - application_id=application_id, - ) - _update_relationship( - response_application_societal_benefit_area, - societal_benefit_area_id=societal_benefit_area_id, - application_id=application_id, - ) - - form_obj: dict[ - str, - ResponseSocietalBenefitArea - | ResponseApplication - | ResponseApplicationSocietalBenefitArea, - ] = { - 'societal_benefit_area': response_societal_benefit_area, - 'application': response_application, - # NOTE: Logic below depends on relationship being last in this dict - 'relationship': response_application_societal_benefit_area, - } - - if request.method == 'POST': - limit_response_editors() - form = ApplicationSocietalBenefitAreaForm(request.form, obj=form_obj) - - if form.validate(): - # Add only submitted sub-forms into the db session - for key, obj in form_obj.items(): - if hasattr(form, key): - form[key].form.populate_obj(obj) - db.session.add(obj) - - # Update the relationship object with the ids of any new entities - if type(obj) is not ResponseApplicationSocietalBenefitArea: - # Get the db object's new ID - db.session.flush() - db.session.refresh(obj) - - # Update the relationship db object - setattr( - response_application_societal_benefit_area, - f'response_{key}_id', - obj.id, - ) - - db.session.commit() - - return redirect(url_for('sba.view_response_sbas', survey_id=survey.id)) - - form = ApplicationSocietalBenefitAreaForm(obj=form_obj) - return render_template( - 'survey/relationships/application_societal_benefit_area.html', - form=form, - survey=survey, - societal_benefit_area=response_societal_benefit_area, - societal_benefit_areas=survey.response.societal_benefit_areas, - application=response_application, - applications=survey.response.applications, - relationship=response_application_societal_benefit_area, - ) - - -@application_societal_benefit_area_bp.route( - '/', - methods=['DELETE'], -) -def delete_response_application_societal_benefit_area_relationship( - survey_id: int, - response_application_societal_benefit_area_id: int, -): - """Delete application/data product relationship.""" - survey = db.get_or_404(Survey, survey_id) - response_application_societal_benefit_area = db.get_or_404( - ResponseApplicationSocietalBenefitArea, - response_application_societal_benefit_area_id, - ) - db.session.delete(response_application_societal_benefit_area) - db.session.commit() - - return redirect( - url_for('sba.view_response_sbas', survey_id=survey.id), - code=303, - ) diff --git a/usaon_benefit_tool/routes/survey/relationships/data_product_application.py b/usaon_benefit_tool/routes/survey/relationships/data_product_application.py deleted file mode 100644 index 7264f1ad..00000000 --- a/usaon_benefit_tool/routes/survey/relationships/data_product_application.py +++ /dev/null @@ -1,252 +0,0 @@ -from flask import Blueprint, Request, redirect, render_template, request, url_for -from wtforms import FormField - -from usaon_benefit_tool import db -from usaon_benefit_tool.forms import FORMS_BY_MODEL -from usaon_benefit_tool.models.tables import ( - ResponseApplication, - ResponseDataProduct, - ResponseDataProductApplication, - Survey, -) -from usaon_benefit_tool.util.authorization import limit_response_editors -from usaon_benefit_tool.util.superform import SuperForm - - -def _update_super_form( - super_form: type[SuperForm], - /, - *, - data_product_id: int | None, - application_id: int | None, -) -> None: - """Populate the form of forms with sub-forms depending on provided IDs. - - When an ID for an object is not provided, we need to gather information from the - user to create that object. - - TODO: Better function name. - """ - if data_product_id is None: - super_form.data_product = FormField(FORMS_BY_MODEL[ResponseDataProduct]) - - if application_id is None: - super_form.application = FormField(FORMS_BY_MODEL[ResponseApplication]) - - -def _update_relationship( - relationship: ResponseDataProductApplication, - *, - data_product_id: int | None, - application_id: int | None, -) -> None: - """Populate the relationship with any known identifiers. - - TODO: Better function name. - """ - if data_product_id: - relationship.response_data_product_id = data_product_id - - if application_id: - relationship.response_application_id = application_id - - -def _response_data_product( - *, - data_product_id: int | None, - response_id: int, -) -> ResponseDataProduct: - """Return a data product db object (or 404).""" - if data_product_id is not None: - response_data_product = db.get_or_404(ResponseDataProduct, data_product_id) - else: - response_data_product = ResponseDataProduct(response_id=response_id) - - return response_data_product - - -def _response_application( - *, - application_id: int | None, - response_id: int, -) -> ResponseApplication: - """Return an application db object (or 404).""" - if application_id is not None: - response_application = db.get_or_404(ResponseApplication, application_id) - else: - response_application = ResponseApplication(response_id=response_id) - - return response_application - - -def _response_data_product_application( - *, - data_product_id: int | None, - application_id: int | None, -) -> ResponseDataProductApplication: - """Return a relationship db object. - - Returned object may be transient or persistent depending on whether a match exists - in the db. - """ - if data_product_id and application_id: - response_data_product_application = ( - db.session.query(ResponseDataProductApplication) - .filter( - ResponseDataProductApplication.response_data_product_id - == data_product_id - and ResponseDataProductApplication.response_application_id - == application_id, - ) - .one_or_none() - ) - else: - response_data_product_application = None - - if response_data_product_application is not None: - return response_data_product_application - else: - return ResponseDataProductApplication() - - -def _request_args(request: Request) -> tuple[int | None, int | None]: - data_product_id: int | str | None = request.args.get('data_product_id') - if data_product_id is not None: - data_product_id = int(data_product_id) - - application_id: int | str | None = request.args.get('application_id') - if application_id is not None: - application_id = int(application_id) - - return data_product_id, application_id - - -# NOTE: consider making this a child of response_bp -data_product_application_bp = Blueprint( - 'data_product_application', - __name__, - url_prefix='/response//data_product_application_relationships', -) - - -@data_product_application_bp.route( - '', - methods=['GET', 'POST'], -) -def view_response_data_product_application_relationships(survey_id: str): - """View and add application/dataproduct relationships to a response. - - TODO: Refactor this whole pile of stuff. Less string magic. Less cyclomatic - complexity. - """ - data_product_id, application_id = _request_args(request) - survey = db.get_or_404(Survey, survey_id) - - class ResponseDataProductApplicationForm(SuperForm): - """Combine all necessary forms into one super-form. - - NOTE: Additional class attributes are added dynamically below. - """ - - relationship = FormField(FORMS_BY_MODEL[ResponseDataProductApplication]) - - response_data_product_application = _response_data_product_application( - data_product_id=data_product_id, - application_id=application_id, - ) - - response_data_product = _response_data_product( - data_product_id=data_product_id, - response_id=survey.response_id, - ) - - response_application = _response_application( - application_id=application_id, - response_id=survey.response_id, - ) - - _update_super_form( - ResponseDataProductApplicationForm, - data_product_id=data_product_id, - application_id=application_id, - ) - _update_relationship( - response_data_product_application, - data_product_id=data_product_id, - application_id=application_id, - ) - - form_obj: dict[ - str, - ResponseDataProduct | ResponseApplication | ResponseDataProductApplication, - ] = { - 'data_product': response_data_product, - 'application': response_application, - # NOTE: Logic below depends on relationship being last in this dict - 'relationship': response_data_product_application, - } - - if request.method == 'POST': - limit_response_editors() - form = ResponseDataProductApplicationForm(request.form, obj=form_obj) - - if form.validate(): - # Add only submitted sub-forms into the db session - for key, obj in form_obj.items(): - if hasattr(form, key): - form[key].form.populate_obj(obj) - db.session.add(obj) - - # Update the relationship object with the ids of any new entities - if type(obj) is not ResponseDataProductApplication: - # Get the db object's new ID - db.session.flush() - db.session.refresh(obj) - - # Update the relationship db object - setattr( - response_data_product_application, - f'response_{key}_id', - obj.id, - ) - - db.session.commit() - - return redirect( - url_for('application.view_response_applications', survey_id=survey.id), - ) - - form = ResponseDataProductApplicationForm(obj=form_obj) - return render_template( - 'survey/relationships/data_product_application.html', - form=form, - survey=survey, - data_product=response_data_product, - data_products=survey.response.data_products, - application=response_application, - applications=survey.response.applications, - relationship=response_data_product_application, - ) - - -@data_product_application_bp.route( - '/', - methods=['DELETE'], -) -def delete_response_data_product_application_relationships( - survey_id: int, - response_data_product_application_id: int, -): - """Delete application/data product relationship.""" - survey = db.get_or_404(Survey, survey_id) - response_data_product_application = db.get_or_404( - ResponseDataProductApplication, - response_data_product_application_id, - ) - db.session.delete(response_data_product_application) - db.session.commit() - - return redirect( - url_for('application.view_response_applications', survey_id=survey.id), - code=303, - ) diff --git a/usaon_benefit_tool/routes/survey/relationships/observing_system_data_product.py b/usaon_benefit_tool/routes/survey/relationships/observing_system_data_product.py deleted file mode 100644 index 0c07a47f..00000000 --- a/usaon_benefit_tool/routes/survey/relationships/observing_system_data_product.py +++ /dev/null @@ -1,260 +0,0 @@ -from flask import Blueprint, Request, redirect, render_template, request, url_for -from wtforms import FormField - -from usaon_benefit_tool import db -from usaon_benefit_tool.forms import FORMS_BY_MODEL -from usaon_benefit_tool.models.tables import ( - ResponseDataProduct, - ResponseObservingSystem, - ResponseObservingSystemDataProduct, - Survey, -) -from usaon_benefit_tool.util.authorization import limit_response_editors -from usaon_benefit_tool.util.superform import SuperForm - - -def _update_super_form( - super_form: type[SuperForm], - /, - *, - data_product_id: int | None, - observing_system_id: int | None, -) -> None: - """Populate the form of forms with sub-forms depending on provided IDs. - - When an ID for an object is not provided, we need to gather information from the - user to create that object. - - TODO: Better function name. - """ - if observing_system_id is None: - super_form.observing_system = FormField(FORMS_BY_MODEL[ResponseObservingSystem]) - - if data_product_id is None: - super_form.data_product = FormField(FORMS_BY_MODEL[ResponseDataProduct]) - - -def _update_relationship( - relationship: ResponseObservingSystemDataProduct, - *, - observing_system_id: int | None, - data_product_id: int | None, -) -> None: - """Populate the relationship with any known identifiers. - - TODO: Better function name. - """ - if observing_system_id: - relationship.response_observing_system_id = observing_system_id - - if data_product_id: - relationship.response_data_product_id = data_product_id - - -# may not need to be internal -def _response_data_product( - *, - data_product_id: int | None, - response_id: int, -) -> ResponseDataProduct: - """Return a data product db object (or 404).""" - if data_product_id is not None: - response_data_product = db.get_or_404(ResponseDataProduct, data_product_id) - else: - response_data_product = ResponseDataProduct(response_id=response_id) - - return response_data_product - - -def _response_observing_system( - *, - observing_system_id: int | None, - response_id: int, -) -> ResponseObservingSystem: - """Return an observing system db object (or 404).""" - if observing_system_id is not None: - response_observing_system = db.get_or_404( - ResponseObservingSystem, - observing_system_id, - ) - else: - response_observing_system = ResponseObservingSystem(response_id=response_id) - - return response_observing_system - - -def _response_observing_system_data_product( - *, - observing_system_id: int | None, - data_product_id: int | None, -) -> ResponseObservingSystemDataProduct: - """Return a relationship db object. - - Returned object may be transient or persistent depending on whether a match exists - in the db. - """ - if data_product_id and observing_system_id: - # If not found, will be `None` - response_observing_system_data_product = ( - db.session.query(ResponseObservingSystemDataProduct) - .filter( - ResponseObservingSystemDataProduct.response_data_product_id - == data_product_id - and ResponseObservingSystemDataProduct.response_observing_system_id - == observing_system_id, - ) - .one_or_none() - ) - else: - response_observing_system_data_product = None - - if response_observing_system_data_product is not None: - return response_observing_system_data_product - else: - return ResponseObservingSystemDataProduct() - - -def _request_args(request: Request) -> tuple[int | None, int | None]: - data_product_id: int | str | None = request.args.get('data_product_id') - if data_product_id is not None: - data_product_id = int(data_product_id) - - observing_system_id: int | str | None = request.args.get('observing_system_id') - if observing_system_id is not None: - observing_system_id = int(observing_system_id) - - return data_product_id, observing_system_id - - -observing_system_data_product_bp = Blueprint( - 'observing_system_data_product', - __name__, - url_prefix='/response//observing_system_data_product_relationships', -) - - -@observing_system_data_product_bp.route( - '', - methods=['GET', 'POST'], -) -def view_response_observing_system_data_product_relationships(survey_id: str): - """View and add observing system/dataproduct relationships to a response. - - TODO: Refactor this whole pile of stuff. Less string magic. Less cyclomatic - complexity. - """ - data_product_id, observing_system_id = _request_args(request) - survey = db.get_or_404(Survey, survey_id) - - class ObservingSystemDataProductForm(SuperForm): - """Combine all necessary forms into one super-form. - - NOTE: Additional class attributes are added dynamically below. - """ - - relationship = FormField(FORMS_BY_MODEL[ResponseObservingSystemDataProduct]) - - response_observing_system_data_product = _response_observing_system_data_product( - data_product_id=data_product_id, - observing_system_id=observing_system_id, - ) - - response_data_product = _response_data_product( - data_product_id=data_product_id, - response_id=survey.response_id, - ) - - response_observing_system = _response_observing_system( - observing_system_id=observing_system_id, - response_id=survey.response_id, - ) - - _update_super_form( - ObservingSystemDataProductForm, - observing_system_id=observing_system_id, - data_product_id=data_product_id, - ) - _update_relationship( - response_observing_system_data_product, - observing_system_id=observing_system_id, - data_product_id=data_product_id, - ) - - form_obj: dict[ - str, - ResponseObservingSystem - | ResponseDataProduct - | ResponseObservingSystemDataProduct, - ] = { - 'observing_system': response_observing_system, - 'data_product': response_data_product, - # NOTE: Logic below depends on relationship being last in this dict - 'relationship': response_observing_system_data_product, - } - - if request.method == 'POST': - limit_response_editors() - form = ObservingSystemDataProductForm(request.form, obj=form_obj) - - if form.validate(): - # Add only submitted sub-forms into the db session - for key, obj in form_obj.items(): - if hasattr(form, key): - form[key].form.populate_obj(obj) - db.session.add(obj) - - # Update the relationship object with the ids of any new entities - if type(obj) is not ResponseObservingSystemDataProduct: - # Get the db object's new ID - db.session.flush() - db.session.refresh(obj) - - # Update the relationship db object - setattr( - response_observing_system_data_product, - f'response_{key}_id', - obj.id, - ) - - db.session.commit() - - return redirect( - url_for('data_product.view_response_data_products', survey_id=survey.id), - ) - - form = ObservingSystemDataProductForm(obj=form_obj) - return render_template( - 'survey/relationships/observing_system_data_product.html', - form=form, - survey=survey, - observing_system=response_observing_system, - observing_systems=survey.response.observing_systems, - data_product=response_data_product, - data_products=survey.response.data_products, - relationship=response_observing_system_data_product, - ) - - -@observing_system_data_product_bp.route( - '/', - methods=['DELETE'], -) -def delete_response_observing_system_data_product_relationship( - survey_id: int, - response_observing_system_data_product_id: int, -): - """Delete data product/observing system relationship.""" - survey = db.get_or_404(Survey, survey_id) - response_observing_system_data_product = db.get_or_404( - ResponseObservingSystemDataProduct, - response_observing_system_data_product_id, - ) - db.session.delete(response_observing_system_data_product) - db.session.commit() - # TODO: figure out why this isn't working - # flash('You have deleted this relationship') - - return redirect( - url_for('data_product.view_response_data_products', survey_id=survey.id), - code=303, - ) diff --git a/usaon_benefit_tool/routes/survey/sbas.py b/usaon_benefit_tool/routes/survey/sbas.py deleted file mode 100644 index 6acf07d9..00000000 --- a/usaon_benefit_tool/routes/survey/sbas.py +++ /dev/null @@ -1,74 +0,0 @@ -from flask import Blueprint, redirect, render_template, request, url_for -from flask_login import login_required - -from usaon_benefit_tool import db -from usaon_benefit_tool.forms import FORMS_BY_MODEL -from usaon_benefit_tool.models.tables import ( - ResponseSocietalBenefitArea, - SocietalBenefitArea, - Survey, -) -from usaon_benefit_tool.util.authorization import limit_response_editors -from usaon_benefit_tool.util.sankey import societal_benefit_areas_sankey - -societal_benefit_area_bp = Blueprint( - 'sba', - __name__, - url_prefix='/response//societal_benefit_areas', -) - - -@societal_benefit_area_bp.route('', methods=['GET', 'POST']) -@login_required -def view_response_sbas(survey_id: str): - """View and add to observing systems associated with a response.""" - sbas = SocietalBenefitArea.query.all() - Form = FORMS_BY_MODEL[ResponseSocietalBenefitArea] - survey = db.get_or_404(Survey, survey_id) - # show the list of available SBAs - response_societal_benefit_area = ResponseSocietalBenefitArea( - response_id=survey.response_id, - ) - - if request.method == 'POST': - limit_response_editors() - form = Form(request.form, obj=response_societal_benefit_area) - - if form.validate(): - form.populate_obj(response_societal_benefit_area) - db.session.add(response_societal_benefit_area) - db.session.commit() - - return redirect(url_for('sba.view_response_sbas', survey_id=survey.id)) - - form = Form(obj=response_societal_benefit_area) - return render_template( - 'survey/sbas.html', - form=form, - survey=survey, - sbas=sbas, - response=survey.response, - societal_benefit_areas=survey.response.societal_benefit_areas, - sankey_series=societal_benefit_areas_sankey(survey.response), - ) - - -@societal_benefit_area_bp.route( - '/', - methods=['DELETE'], -) -@login_required -def delete_response_sba(survey_id: int, response_societal_benefit_area_id: int): - """Delete societal benefit area response object from survey.""" - survey = db.get_or_404(Survey, survey_id) - response_sba = db.get_or_404( - ResponseSocietalBenefitArea, - response_societal_benefit_area_id, - ) - db.session.delete(response_sba) - db.session.commit() - - return redirect( - url_for('sba.view_response_sbas', survey_id=survey.id), - code=303, - ) diff --git a/usaon_benefit_tool/routes/surveys.py b/usaon_benefit_tool/routes/surveys.py deleted file mode 100644 index 9e8f46fe..00000000 --- a/usaon_benefit_tool/routes/surveys.py +++ /dev/null @@ -1,13 +0,0 @@ -from flask import Blueprint, render_template -from flask_login import login_required - -from usaon_benefit_tool.models.tables import Survey - -surveys_bp = Blueprint('surveys', __name__, url_prefix='/surveys') - - -@surveys_bp.route('') -@login_required -def view_surveys(): - surveys = Survey.query.order_by(Survey.created_timestamp).all() - return render_template('surveys.html', surveys=surveys) diff --git a/usaon_benefit_tool/static/style.css b/usaon_benefit_tool/static/style.css index 8925922b..9ae56050 100644 --- a/usaon_benefit_tool/static/style.css +++ b/usaon_benefit_tool/static/style.css @@ -4,6 +4,13 @@ margin-bottom: 0; } +a { + text-decoration: none; +} +a:hover { + text-decoration: underline; +} + /* Footer stuff */ body { diff --git a/usaon_benefit_tool/templates/assessment/_data_product.html b/usaon_benefit_tool/templates/assessment/_data_product.html new file mode 100644 index 00000000..7f3c726e --- /dev/null +++ b/usaon_benefit_tool/templates/assessment/_data_product.html @@ -0,0 +1,37 @@ +{% from 'bootstrap5/form.html' import render_form %} +{% from 'macros/buttons.j2' import delete_button_htmx %} + +{# Construct POST or PUT request depending on whether a data product id was passed #} +{% set edit = assessment_data_product_id is not undefined %} +{% set + htmxQueryAttr = 'hx-put=' ~ url_for("assessment.data_product.put", assessment_id=assessment_id, assessment_data_product_id=assessment_data_product_id) + if edit else 'hx-post=' ~ url_for("assessment.data_products.post", assessment_id=assessment_id) +%} +{% set endpoint = "assessment.data_product.edit" if edit else "assessment.data_products.post" %} + + diff --git a/usaon_benefit_tool/templates/survey/base.html b/usaon_benefit_tool/templates/assessment/base.html similarity index 54% rename from usaon_benefit_tool/templates/survey/base.html rename to usaon_benefit_tool/templates/assessment/base.html index 660e5091..761461fc 100644 --- a/usaon_benefit_tool/templates/survey/base.html +++ b/usaon_benefit_tool/templates/assessment/base.html @@ -6,9 +6,9 @@ {% block content %}

- {% block title %}Project #{{survey.id}}: {{survey.title}}{% endblock %} + {% block title %}Assessment #{{assessment.id}}: {{assessment.title}}{% endblock %} {{ help_icon( - "Created: "~survey.response.created_timestamp | dateformat~"
Last updated: "~survey.response.updated_timestamp | dateformat, + "Created: " ~ assessment.created_timestamp | dateformat ~ "
Last updated: " ~ assessment.updated_timestamp | dateformat, html="true", )}}

@@ -17,45 +17,47 @@

diff --git a/usaon_benefit_tool/templates/survey/overview.html b/usaon_benefit_tool/templates/assessment/edit.html similarity index 59% rename from usaon_benefit_tool/templates/survey/overview.html rename to usaon_benefit_tool/templates/assessment/edit.html index 7f51dc0d..7507c2b3 100644 --- a/usaon_benefit_tool/templates/survey/overview.html +++ b/usaon_benefit_tool/templates/assessment/edit.html @@ -1,4 +1,4 @@ -{% extends 'survey/base.html' %} +{% extends 'assessment/base.html' %} {% from 'bootstrap5/utils.html' import render_icon %} {% from 'macros/javascript_highcharts.j2' import display_sankey %} @@ -6,9 +6,8 @@ {{super()}}

Description

-

{{survey.description}}

+

{{assessment.description}}

- - {{ display_sankey(sankey_series, survey_title=survey.title)}} + {{ display_sankey(sankey_series, assessment=assessment)}} {% endblock %} diff --git a/usaon_benefit_tool/templates/assessment/overview.html b/usaon_benefit_tool/templates/assessment/overview.html new file mode 100644 index 00000000..7730babe --- /dev/null +++ b/usaon_benefit_tool/templates/assessment/overview.html @@ -0,0 +1,65 @@ +{% extends 'assessment/base.html' %} +{% from 'bootstrap5/utils.html' import render_icon %} +{% from 'macros/javascript_highcharts.j2' import display_sankey %} +{% from 'macros/buttons.j2' import create_button %} + +{% block content %} + {{super()}} + +

Description

+

{{assessment.description}}

+ +
+ + +
+ + {{ display_sankey(sankey_series, assessment=assessment)}} + +
+ {# NOTE: This div has HTMX configuration because it can be used by + Highcharts callbacks to open a modal for viewing or editing a Highcharts + node/link. Highcharts will call `htmx.trigger("#this-div-id", + "modalOpened")` after setting the `hx-get` attribute on this div with + the correct URL to fetch the needed form. This is a bit complex, but it + helps simplify the UI by promoting separation of concerns (a new endpoint + is created to take responsibility to display the form, and HTMX is + responsible for fetching and submitting right form). This helps prevent + one big Jinja template for a unified diagram editing interface that + must display at least 4 different kinds of forms. + #} + +
+ +{% endblock %} diff --git a/usaon_benefit_tool/templates/survey/user_guide.html b/usaon_benefit_tool/templates/assessment/user_guide.html similarity index 76% rename from usaon_benefit_tool/templates/survey/user_guide.html rename to usaon_benefit_tool/templates/assessment/user_guide.html index e4e835c3..f4c084fd 100644 --- a/usaon_benefit_tool/templates/survey/user_guide.html +++ b/usaon_benefit_tool/templates/assessment/user_guide.html @@ -1,4 +1,4 @@ -{% extends 'survey/base.html' %} +{% extends 'assessment/base.html' %} {% block content %} diff --git a/usaon_benefit_tool/templates/assessments.html b/usaon_benefit_tool/templates/assessments.html new file mode 100644 index 00000000..dd209bb8 --- /dev/null +++ b/usaon_benefit_tool/templates/assessments.html @@ -0,0 +1,83 @@ +{% extends 'base.html' %} +{% from 'bootstrap5/utils.html' import render_icon %} +{% from 'macros/badges/private.j2' import private_badge %} +{% from 'macros/forms/new_assessment.j2' import new_assessment_form %} + + +{% block content %} +

{% block title %}Assessments{% endblock %}

+ + {% if current_user.role_id=='admin' %} + + + + {% endif %} + +
+
+ + + + + + {% if current_user.role_id=='admin' %} + + {% endif %} + + + + {% if current_user.role_id=='admin' %} + + + + {% endif %} + + + + + + + + + + {% for assessment in assessments %} + + {% if current_user.role_id=='admin' %} + + {% endif %} + + + + {% if current_user.role_id=='admin' %} + + + + {% endif %} + + + + + {% endfor %} + + +
#TitleCreated byCreated timeUpdated timeStatus
{{assessment.id}} + {{assessment.title}} + {{assessment.created_by.email}}{{assessment.created_timestamp | dateformat}}{{assessment.updated_timestamp | dateformat}}{{assessment.status.id}}{{private_badge(assessment.private)}}
+ +{% endblock %} diff --git a/usaon_benefit_tool/templates/base.html b/usaon_benefit_tool/templates/base.html index e830f877..89d7db2e 100644 --- a/usaon_benefit_tool/templates/base.html +++ b/usaon_benefit_tool/templates/base.html @@ -8,6 +8,18 @@ {% block title %}{% endblock %} - US AON Benefit Tool {{ bootstrap.load_css() }} + {# TODO: + Can the version of HTMX we include be managed as a dependency like + bootstrap? + #} + + {{ JSGlue.include() }} + {# TODO: + The best practice is to load bootstrap right before , but then the + features we need (e.g. `element.modal()`) aren't available at the time we try + to use them. How to work around this? + #} + {{ bootstrap.load_js() }} @@ -73,7 +85,6 @@ {% block scripts %} - {{ bootstrap.load_js() }} {{ add_javascript__remove() }} diff --git a/usaon_benefit_tool/templates/home.md b/usaon_benefit_tool/templates/home.md index e9c40afa..e60098a3 100644 --- a/usaon_benefit_tool/templates/home.md +++ b/usaon_benefit_tool/templates/home.md @@ -4,8 +4,8 @@ ### Actions -* [View our library of projects]({{ url_for("surveys.view_surveys") }}). -* [Edit or update your own project]({{ url_for("surveys.view_surveys") }}). +* [View our library of assessments]({{ url_for("assessments.get") }}). +* [Edit or update your own assessment]({{ url_for("assessments.get") }}). * If you'd like to create a new evaluation please email: [hazel@iarpccollaborations.org](mailto:hazel@iarpccollaborations.org) * Our [data management plan](https://docs.google.com/document/d/13DLX3A__M60xMgFbQqk2NMAjZzahn5FZcvB3fWSVRck/edit?usp=sharing) includes information about how any information you add to the tool will be stored and may be shared. diff --git a/usaon_benefit_tool/templates/jsglue/js_bridge.js b/usaon_benefit_tool/templates/jsglue/js_bridge.js new file mode 100644 index 00000000..b89a6608 --- /dev/null +++ b/usaon_benefit_tool/templates/jsglue/js_bridge.js @@ -0,0 +1,113 @@ +{# Provide `{{namespace}}.url_for` JS function (default `Flask.url_for()`). + +Vendored from https://github.com/stewartpark/Flask-JSGlue; appears abandoned. +Applied changes to work with modern versions of these dependencies. + +Copyright 2022 Stewart Park +License: BSD 3-clause + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT +OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +#} +var {{ namespace }} = new(function () { + 'use strict'; + return { + '_endpoints': {{ rules|safe }}, + 'url_for': function (endpoint, rule) { + if (typeof rule === "undefined") rule = {}; + else if (typeof(rule) !== "object") { + // rule *must* be an Object, anything else is wrong + throw {name: "ValueError", message: "type for 'rule' must be Object, got: " + typeof(rule)}; + } + + var has_everything = false, + url = ""; + + var is_absolute = false, + has_anchor = false, + has_scheme; + var anchor = "", + scheme = ""; + + if (rule['_external'] === true) { + is_absolute = true; + scheme = location.protocol.split(':')[0]; + delete rule['_external']; + } + + if ('_scheme' in rule) { + if (is_absolute) { + scheme = rule['_scheme']; + delete rule['_scheme']; + } else { + throw { + name: "ValueError", + message: "_scheme is set without _external." + }; + } + } + + if ('_anchor' in rule) { + has_anchor = true; + anchor = rule['_anchor']; + delete rule['_anchor']; + } + + for (var i in this._endpoints) { + if (endpoint == this._endpoints[i][0]) { + var url = ''; + var j = 0; + var has_everything = true; + var used = {}; + for (var j = 0; j < this._endpoints[i][2].length; j++) { + var t = rule[this._endpoints[i][2][j]]; + if (typeof t === "undefined") { + has_everything = false; + break; + } + url += this._endpoints[i][1][j] + t; + used[this._endpoints[i][2][j]] = true; + } + if (has_everything) { + if (this._endpoints[i][2].length != this._endpoints[i][1].length) + url += this._endpoints[i][1][j]; + + var first = true; + for (var r in rule) { + if (r[0] != '_' && !(r in used)) { + if (first) { + url += '?'; + first = false; + } else { + url += '&'; + } + url += r + '=' + rule[r]; + } + } + if (has_anchor) { + url += "#" + anchor; + } + + if (is_absolute) { + return scheme + "://" + location.host + url; + } else { + return url; + } + } + } + } + + throw { + name: 'BuildError', + message: "Endpoint '" + endpoint + "' does not exist or you have passed incorrect parameters " + JSON.stringify(rule) + }; + } + }; +}); diff --git a/usaon_benefit_tool/templates/macros/buttons.j2 b/usaon_benefit_tool/templates/macros/buttons.j2 index 345b5da5..7271b914 100644 --- a/usaon_benefit_tool/templates/macros/buttons.j2 +++ b/usaon_benefit_tool/templates/macros/buttons.j2 @@ -1,5 +1,18 @@ {% from 'bootstrap5/utils.html' import render_icon %} +{% macro delete_button_htmx(endpoint, text) -%} + {% set text = text | default('Delete') %} + + +{%- endmacro -%} + {% macro delete_button(endpoint, text) -%} {# CRITICAL: The "remove()" JS function must be available. #} {% set text = text | default('Delete') %} @@ -21,12 +34,12 @@ class="btn btn-success text-nowrap" role="button" /> + {# TODO: Sparkles? :) #} {{ render_icon("plus-circle-fill") }} {{text}} {%- endmacro -%} - {% macro edit_button(endpoint, text) -%} {% set text = text | default('Edit') %} diff --git a/usaon_benefit_tool/templates/macros/forms/form_fields.j2 b/usaon_benefit_tool/templates/macros/forms/form_fields.j2 new file mode 100644 index 00000000..099ae03e --- /dev/null +++ b/usaon_benefit_tool/templates/macros/forms/form_fields.j2 @@ -0,0 +1,35 @@ +{% from 'bootstrap5/form.html' import render_hidden_errors, render_field %} + +{% macro render_form_fields( + form, + form_type="basic", + horizontal_columns=('lg', 2, 10), + button_map={}, + button_style="", + button_size="", + form_group_classes='' +) %} + {#- + Render a form without the
tag; we want full control of it so we can use + HTMX attributes. + + Based on: + + https://github.com/helloflask/bootstrap-flask/blob/ce852e4c8e6c04952cbc27aa8089418ce04ebd13/flask_bootstrap/templates/bootstrap5/form.html#L299-L311 + -#} + + {{ form.hidden_tag() }} + {{ render_hidden_errors(form) }} + {%- for field in form %} + {% if not bootstrap_is_hidden_field(field) -%} + {{ render_field(field, + form_type=form_type, + horizontal_columns=horizontal_columns, + button_map=button_map, + button_style=button_style, + button_size=button_size, + form_group_classes=form_group_classes) }} + {%- endif %} + {%- endfor %} + +{% endmacro %} diff --git a/usaon_benefit_tool/templates/macros/forms/new_assessment.j2 b/usaon_benefit_tool/templates/macros/forms/new_assessment.j2 new file mode 100644 index 00000000..a177d3a3 --- /dev/null +++ b/usaon_benefit_tool/templates/macros/forms/new_assessment.j2 @@ -0,0 +1,30 @@ +{% macro new_assessment_form(form) -%} +

New assessment

+ + + {{form.csrf_token}} + + {{form.title.label}} {{form.title(rows=4, cols=100)}} + {% if form.title.errors %} +
    + {% for error in form.title.errors %} +
  • {{ error }}
  • + {% endfor %} +
+ {% endif %} + +
+ + {{form.description.label}} {{form.description(rows=4, cols=100)}} + {% if form.description.errors %} +
    + {% for error in form.name.errors %} +
  • {{ error }}
  • + {% endfor %} +
+ {% endif %} + + + +
+{%- endmacro -%} diff --git a/usaon_benefit_tool/templates/macros/javascript_highcharts.j2 b/usaon_benefit_tool/templates/macros/javascript_highcharts.j2 index e7e5b326..2ced724e 100644 --- a/usaon_benefit_tool/templates/macros/javascript_highcharts.j2 +++ b/usaon_benefit_tool/templates/macros/javascript_highcharts.j2 @@ -1,6 +1,52 @@ -{% macro add_javascript__highcharts(sankey_series, survey_title) -%} - +{% macro add_javascript__highcharts(sankey_series, assessment) -%} {% endmacro %} -{% macro display_sankey(sankey_series, survey_title) -%} +{% macro display_sankey(sankey_series, assessment) -%} {% include 'includes/highcharts.html' %}

Diagram

@@ -42,6 +124,6 @@
- {{ add_javascript__highcharts(sankey_series, survey_title) }} + {{ add_javascript__highcharts(sankey_series=sankey_series, assessment=assessment) }} {% endif %} {% endmacro %} diff --git a/usaon_benefit_tool/templates/macros/nav_buttons.j2 b/usaon_benefit_tool/templates/macros/nav_buttons.j2 index 81f8aabb..e2fc85bc 100644 --- a/usaon_benefit_tool/templates/macros/nav_buttons.j2 +++ b/usaon_benefit_tool/templates/macros/nav_buttons.j2 @@ -1,8 +1,9 @@ {% macro nav_buttons(current_user) -%} {% from "bootstrap5/nav.html" import render_nav_item %} - {{ render_nav_item("root.root", "Home", _use_li=True)}} - {{ render_nav_item("surveys.view_surveys", "Projects", _use_li=True)}} + {{ render_nav_item("assessments.get", "Assessments", _use_li=True)}} + {# NB: The user-facing term for "nodes" is "objects" #} + {{ render_nav_item("nodes.get", "Object library", _use_li=True)}} {%- endmacro -%} diff --git a/usaon_benefit_tool/templates/new_survey.html b/usaon_benefit_tool/templates/new_survey.html deleted file mode 100644 index aac2d899..00000000 --- a/usaon_benefit_tool/templates/new_survey.html +++ /dev/null @@ -1,33 +0,0 @@ -{% extends 'base.html' %} - - -{% block content %} - -

{% block title %}New Project{% endblock %}

- -
- {{form.csrf_token}} - - {{form.title.label}} {{form.title(rows=4, cols=100)}} - {% if form.title.errors %} -
    - {% for error in form.title.errors %} -
  • {{ error }}
  • - {% endfor %} -
- {% endif %} -
- {{form.description.label}} {{form.description(rows=4, cols=100)}} - {% if form.description.errors %} -
    - {% for error in form.name.errors %} -
  • {{ error }}
  • - {% endfor %} -
- {% endif %} - - - -
- -{% endblock %} diff --git a/usaon_benefit_tool/templates/node.html b/usaon_benefit_tool/templates/node.html new file mode 100644 index 00000000..7975ffa2 --- /dev/null +++ b/usaon_benefit_tool/templates/node.html @@ -0,0 +1,20 @@ +{% extends 'base.html' %} +{% from 'macros/forms/form_fields.j2' import render_form_fields %} +{% from 'macros/buttons.j2' import delete_button_htmx %} +{% from 'macros/help_icon.j2' import help_icon %} + +{% block content %} +

+ {% block title %}Object #{{node.id}}: {{node.title}}{% endblock %} + {{ help_icon( + "Created: " ~ node.created_timestamp | dateformat ~ "
Last updated: " ~ node.updated_timestamp | dateformat, + html="true", + )}} +

+ +
+ {{ render_form_fields(form) }} +
+{% endblock %} diff --git a/usaon_benefit_tool/templates/nodes.html b/usaon_benefit_tool/templates/nodes.html new file mode 100644 index 00000000..8667b10b --- /dev/null +++ b/usaon_benefit_tool/templates/nodes.html @@ -0,0 +1,129 @@ +{% extends 'base.html' %} +{% from 'bootstrap5/utils.html' import render_icon %} +{% from 'macros/badges/private.j2' import private_badge %} +{% from 'macros/buttons.j2' import edit_button, delete_button_htmx %} + + +{% block content %} +

{% block title %}Object library{% endblock %}

+ + {% if current_user.role_id=='admin' %} + {# TODO: _DRY!_ #} + + + + + + {% endif %} + +
+
+ + + + + + {% if current_user.role_id=='admin' %} + + {% endif %} + + + + + {% if current_user.role_id=='admin' %} + + + + {% endif %} + + + + + + {% for node in nodes %} + + + {% if current_user.role_id=='admin' %} + + {% endif %} + + + + + + {% if current_user.role_id=='admin' %} + + + + + {% endif %} + + + {% endfor %} + + +
#TitleObject typeCreated byCreated timeUpdated time
{{node.id}} + + {{node.title}} + + + {{node.type.value}} + {{node.created_by.email}}{{node.created_timestamp | dateformat}}{{node.updated_timestamp | dateformat}} + {{delete_button_htmx( + 'node.delete', + 'Delete object', + node_id=node.id, + )}} +
+ +{% endblock %} diff --git a/usaon_benefit_tool/templates/partials/modal_form.html b/usaon_benefit_tool/templates/partials/modal_form.html new file mode 100644 index 00000000..6df3592b --- /dev/null +++ b/usaon_benefit_tool/templates/partials/modal_form.html @@ -0,0 +1,14 @@ +{% from 'macros/forms/form_fields.j2' import render_form_fields %} + + diff --git a/usaon_benefit_tool/templates/survey/applications.html b/usaon_benefit_tool/templates/survey/applications.html deleted file mode 100644 index ee035aa2..00000000 --- a/usaon_benefit_tool/templates/survey/applications.html +++ /dev/null @@ -1,97 +0,0 @@ -{% extends 'survey/base.html' %} -{% from 'bootstrap5/form.html' import render_form %} -{% from 'bootstrap5/utils.html' import render_icon %} -{% from 'macros/buttons.j2' import delete_button, create_button %} -{% from 'macros/javascript_highcharts.j2' import display_sankey %} - -{% block content %} - {{super()}} - -

Applications

- -
-
- - - - - - - - - - - - {% for application in response.applications %} - - - - - - - - {% endfor %} -
NameData products related
NamePerformance RatingCriticality RatingEdit Relationships
{{application.short_name}} - {{delete_button( - 'application.delete_response_application', - 'Delete object', - survey_id=survey.id, - response_application_id=application.id, - )}} - - {% if not application.input_relationships %} - None -
- {% endif %} -
    - {% for input_relationship in application.input_relationships %} -
  • {{input_relationship.data_product.short_name}}
  • - {% endfor %} -
-
-
-
    - {% for input_relationship in application.input_relationships %} -
  • {{input_relationship.performance_rating}}
  • - {% endfor %} -
-
-
    - {% for input_relationship in application.input_relationships %} -
  • {{input_relationship.criticality_rating}}
  • - {% endfor %} -
-
- {{create_button( - 'data_product_application.view_response_data_product_application_relationships', - 'New relationship', - survey_id=survey.id, - application_id=application.id, - )}} - {% for input_relationship in application.input_relationships %} -
  • - {{delete_button( - 'data_product_application.delete_response_data_product_application_relationships', - 'Delete relationship', - survey_id=survey.id, - response_data_product_application_id=input_relationship.id, - )}} - -
  • - {% endfor %} -
    -
    - - {{ display_sankey(sankey_series) }} -
    - - {% if not current_user.role_id not in ['admin', 'respondent'] %} -

    Add application

    - -
    - {{render_form(form, button_map={'submit_button': 'primary'}) }} - -
    - {% endif %} - -{% endblock %} diff --git a/usaon_benefit_tool/templates/survey/data_products.html b/usaon_benefit_tool/templates/survey/data_products.html deleted file mode 100644 index eb5600b0..00000000 --- a/usaon_benefit_tool/templates/survey/data_products.html +++ /dev/null @@ -1,94 +0,0 @@ -{% extends 'survey/base.html' %} -{% from 'bootstrap5/form.html' import render_form %} -{% from 'bootstrap5/utils.html' import render_icon %} -{% from 'macros/buttons.j2' import delete_button, create_button %} -{% from 'macros/javascript_highcharts.j2' import display_sankey %} - -{% block content %} - {{super()}} - -

    Data products

    - -
    -
    - - - - - - - - - - - - {% for data_product in response.data_products %} - - - - - - - - {% endfor %} -
    NameRelated Observing Systems
    NamePerformance RatingCriticality RatingEdit Relationships
    {{data_product.short_name}} - {{delete_button( - 'data_product.delete_response_data_product', - 'Delete object', - survey_id=survey.id, - response_data_product_id=data_product.id, - )}} - - {% if not data_product.input_relationships %} - None -
    - {% endif %} -
      - {% for input_relationship in data_product.input_relationships %} -
    • {{input_relationship.observing_system.short_name}}
    • - {% endfor %} -
    -
    -
      - {% for input_relationship in data_product.input_relationships %} -
    • {{input_relationship.performance_rating}}
    • - {% endfor %} -
    -
    -
      - {% for input_relationship in data_product.input_relationships %} -
    • {{input_relationship.criticality_rating}}
    • - {% endfor %} -
    -
    - {{create_button( - 'observing_system_data_product.view_response_observing_system_data_product_relationships', - 'New relationship', - survey_id=survey.id, - data_product_id=data_product.id, - )}} - {% for input_relationship in data_product.input_relationships %} -
  • - {{delete_button( - 'observing_system_data_product.delete_response_observing_system_data_product_relationship', - 'Delete relationship', - survey_id=survey.id, - response_observing_system_data_product_id=input_relationship.id, - )}} -
  • - {% endfor %} -
    -
    - - {{ display_sankey(sankey_series) }} -
    - - - {% if not current_user.role_id not in ['admin', 'respondent'] %} -

    Add data product

    -
    - {{render_form(form, button_map={'submit_button': 'primary'}) }} -
    - {% endif %} - -{% endblock %} diff --git a/usaon_benefit_tool/templates/survey/observing_systems.html b/usaon_benefit_tool/templates/survey/observing_systems.html deleted file mode 100644 index 6ffcffa2..00000000 --- a/usaon_benefit_tool/templates/survey/observing_systems.html +++ /dev/null @@ -1,39 +0,0 @@ -{% extends 'survey/base.html' %} -{% from 'bootstrap5/form.html' import render_form %} -{% from 'bootstrap5/utils.html' import render_icon %} -{% from 'macros/buttons.j2' import delete_button %} - - -{% block content %} - - {{super()}} - -

    Observing systems

    - - - - - - {% for observing_system in response.observing_systems %} - - - - {% endfor %} -
    Name
    {{observing_system.short_name}} - {{delete_button( - 'obs.delete_response_observing_system', - 'Delete object', - survey_id=survey.id, - response_observing_system_id=observing_system.id, - )}} -
    - - - {% if not current_user.role_id not in ['admin', 'respondent'] %} -

    Add observing system

    -
    - {{render_form(form, button_map={'submit_button': 'primary'}) }} - -
    - {% endif %} -{% endblock %} diff --git a/usaon_benefit_tool/templates/survey/relationships/application_societal_benefit_area.html b/usaon_benefit_tool/templates/survey/relationships/application_societal_benefit_area.html deleted file mode 100644 index 4025e3cb..00000000 --- a/usaon_benefit_tool/templates/survey/relationships/application_societal_benefit_area.html +++ /dev/null @@ -1,60 +0,0 @@ -{% extends 'survey/base.html' %} -{% from 'bootstrap5/form.html' import render_form %} - - -{% block content %} - - {{super()}} - - {% if not current_user.role_id=='analyst' %} -

    Application <-> Societal Benefit Area relationship

    - - -
    - -
    -

    Application

    - - -
    - - -
    -

    Societal Benefit Areas

    - - -
    -
    - -
    - {{form.csrf_token}} - {{render_form(form)}} - -
    - {% else %} -

    You do not have permissions to add relationships.

    - {% endif %} - - -{% endblock %} diff --git a/usaon_benefit_tool/templates/survey/relationships/data_product_application.html b/usaon_benefit_tool/templates/survey/relationships/data_product_application.html deleted file mode 100644 index 16845189..00000000 --- a/usaon_benefit_tool/templates/survey/relationships/data_product_application.html +++ /dev/null @@ -1,65 +0,0 @@ -{% extends 'survey/base.html' %} -{% from 'bootstrap5/form.html' import render_form, render_field %} - - -{% block content %} - - {{super()}} - - {% if not current_user.role_id=='analyst' %} -

    Data Product <-> Application relationship

    - - -
    - -
    -

    Data product

    - - -
    - - -
    -

    Application

    - - -
    -
    - - -
    - {{form.csrf_token}} - - {{render_form(form)}} -
    - - {% else %} -

    You do not have permissions to add relationships.

    - {% endif %} - - -{% endblock %} diff --git a/usaon_benefit_tool/templates/survey/relationships/observing_system_data_product.html b/usaon_benefit_tool/templates/survey/relationships/observing_system_data_product.html deleted file mode 100644 index 72077a67..00000000 --- a/usaon_benefit_tool/templates/survey/relationships/observing_system_data_product.html +++ /dev/null @@ -1,60 +0,0 @@ -{% extends 'survey/base.html' %} -{% from 'bootstrap5/form.html' import render_form %} - - -{% block content %} - - {{super()}} - - {% if not current_user.role_id=='analyst' %} -

    Observing System <-> Data Product relationship

    - - -
    - -
    -

    Observing Systems

    - - -
    - - -
    -

    Data Product

    - - -
    -
    - -
    - {{form.csrf_token}} - {{render_form(form)}} - {% else %} -

    You do not have permissions to add relationships.

    - {% endif %} - -{% endblock %} diff --git a/usaon_benefit_tool/templates/survey/sbas.html b/usaon_benefit_tool/templates/survey/sbas.html deleted file mode 100644 index 5343e728..00000000 --- a/usaon_benefit_tool/templates/survey/sbas.html +++ /dev/null @@ -1,87 +0,0 @@ -{% extends 'survey/base.html' %} -{% from 'bootstrap5/form.html' import render_form %} -{% from 'bootstrap5/utils.html' import render_icon %} -{% from 'macros/buttons.j2' import delete_button, create_button %} -{% from 'macros/javascript_highcharts.j2' import display_sankey %} - - -{% block content %} - {{super()}} - -
    -
    -

    Societal benefit areas

    - - - - - - - - - - - {% for societal_benefit_area in response.societal_benefit_areas %} - - - - - - - {% endfor %} -
    SBA IDRelated Applications
    NamePerformance RatingEdit Relationships
    {{societal_benefit_area.societal_benefit_area_id}} - {{delete_button( - 'sba.delete_response_sba', - 'Delete object', - survey_id=survey.id, - response_societal_benefit_area_id=societal_benefit_area.id, - )}} - - {% if not societal_benefit_area.input_relationships %} - None -
    - {% endif %} - -
      - {% for input_relationship in societal_benefit_area.input_relationships %} -
    • {{input_relationship.application.short_name}}
    • - {% endfor %} -
    -
    -
      - {% for input_relationship in societal_benefit_area.input_relationships %} -
    • {{input_relationship.performance_rating}}
    • - {% endfor %} -
    -
    - {{create_button( - 'application_societal_benefit_area.view_response_application_societal_benefit_area_relationships', - 'New relationship', - survey_id=survey.id, - societal_benefit_area_id=societal_benefit_area.id, - )}} - {% for input_relationship in societal_benefit_area.input_relationships %} -
  • - {{delete_button( - 'application_societal_benefit_area.delete_response_application_societal_benefit_area_relationship', - 'Delete relationship', - survey_id=survey.id, - response_application_societal_benefit_area_id=input_relationship.id, - )}} -
  • - {% endfor %} -
    -

    - {% if not current_user.role_id not in ['admin', 'respondent'] %} -

    Assign societal benefit area

    - - {{render_form(form, button_map={'submit_button': 'primary'}) }} - - - {% endif %} -
    - - {{ display_sankey(sankey_series) }} -
    - -{% endblock %} diff --git a/usaon_benefit_tool/templates/surveys.html b/usaon_benefit_tool/templates/surveys.html deleted file mode 100644 index de839b54..00000000 --- a/usaon_benefit_tool/templates/surveys.html +++ /dev/null @@ -1,67 +0,0 @@ -{% extends 'base.html' %} -{% from 'bootstrap5/utils.html' import render_icon %} -{% from 'macros/badges/private.j2' import private_badge %} -{% from 'macros/buttons.j2' import create_button, edit_button %} - - -{% block content %} -

    {% block title %}Projects{% endblock %}

    - - {% if current_user.role_id=='admin' %} - {{ create_button('survey.new_survey') }} - {% endif %} - -
    -
    - - - - - - {% if current_user.role_id=='admin' %} - - {% endif %} - - - - {% if current_user.role_id=='admin' %} - - - - {% endif %} - - - - - - - - - - - {% for survey in surveys %} - - {% if current_user.role_id=='admin' %} - - {% endif %} - - - - {% if current_user.role_id=='admin' %} - - - - {% endif %} - - - - - - {% endfor %} - - -
    #TitleCreated byCreated timeUpdated timeStatus
    {{survey.id}}{{survey.title}}{{survey.created_by.email}}{{survey.created_timestamp | dateformat}}{{survey.response.updated_timestamp | dateformat}}{{survey.status.id}}{{private_badge(survey.private)}} - {{ edit_button('survey.view_survey', survey_id=survey.id) }} -
    - -{% endblock %} diff --git a/usaon_benefit_tool/util/authorization.py b/usaon_benefit_tool/util/authorization.py index 866f75fb..3cae4c1f 100644 --- a/usaon_benefit_tool/util/authorization.py +++ b/usaon_benefit_tool/util/authorization.py @@ -6,5 +6,5 @@ def limit_response_editors() -> None: raise RuntimeError("Please login") if current_user.role_id not in ['admin', 'respondent']: raise RuntimeError( - "You must be a respondent or admin to respond to this survey.", + "You must be a respondent or admin to respond to this assessment.", ) diff --git a/usaon_benefit_tool/util/db/setup.py b/usaon_benefit_tool/util/db/setup.py index 7fd91d32..f65acc58 100644 --- a/usaon_benefit_tool/util/db/setup.py +++ b/usaon_benefit_tool/util/db/setup.py @@ -1,18 +1,28 @@ +"""Set up an existing database with the tables and/or data this application needs. + +TODO: Break this up in to multiple modules! +""" from flask import current_app from loguru import logger from sqlalchemy import MetaData from sqlalchemy.orm import Session from usaon_benefit_tool import db +from usaon_benefit_tool._types import NodeType from usaon_benefit_tool.constants.rbac import ROLES from usaon_benefit_tool.constants.sba import IAOA_SBA_FRAMEWORK -from usaon_benefit_tool.constants.status import STATUSES +from usaon_benefit_tool.constants.status import ASSESSMENT_STATUSES from usaon_benefit_tool.models.tables import ( + Assessment, + AssessmentNode, + AssessmentStatus, + Link, + NodeSubtypeOther, + NodeSubtypeSocietalBenefitArea, Role, SocietalBenefitArea, SocietalBenefitKeyObjective, SocietalBenefitSubArea, - Status, ) from usaon_benefit_tool.util.dev import DEV_USER @@ -33,36 +43,51 @@ def recreate_tables() -> None: def create_tables(session: Session) -> None: - # Create all tables + """Create all tables.""" db.Model.metadata.create_all(bind=session.get_bind()) logger.info('Tables created.') def populate_reference_data() -> None: - init_societal_benefit_areas(db.session) - init_roles(db.session) - init_statuses(db.session) + """Load the reference tables with data. + + Reference tables are tables containing (mostly) static data, e.g. the statuses + table. Usually these are not editable within the GUI. + """ + _init_societal_benefit_areas(db.session) + _init_roles(db.session) + _init_statuses(db.session) + # TODO: Should this be part of test data? If so, we need to find a way to allow this + # to only run the first time / tolerate duplicate errors. if current_app.config["LOGIN_DISABLED"]: - init_dev_user(db.session) + _init_dev_user(db.session) logger.info('Reference data loaded.') -def init_statuses(session: Session) -> None: +def populate_test_data() -> None: + """Load the operational tables with example data.""" + _init_test_assessment(db.session) + + logger.info('Test data loaded.') + + +def _init_statuses(session: Session) -> None: session.add_all( [ - Status( + AssessmentStatus( id=status, + description=description, ) - for status in STATUSES + for status, description in ASSESSMENT_STATUSES.items() ], ) session.commit() -def init_roles(session: Session) -> None: +def _init_roles(session: Session) -> None: session.add_all( [ Role( @@ -75,13 +100,7 @@ def init_roles(session: Session) -> None: session.commit() -def init_dev_user(session: Session) -> None: - logger.warning("Inserting dev user. This should not happen in production!") - session.add(DEV_USER) - session.commit() - - -def init_societal_benefit_areas(session: Session) -> None: +def _init_societal_benefit_areas(session: Session) -> None: """Insert Societal Benefit Areas from GEOSS framework. https://en.wikipedia.org/wiki/Global_Earth_Observation_System_of_Systems @@ -126,3 +145,110 @@ def init_societal_benefit_areas(session: Session) -> None: ) session.commit() + + +def _init_test_assessment(session: Session) -> None: + assessment = Assessment( + title="[TEST] This is testing data!", + description=( + "Created by running the relevant invoke task from the project source code." + ), + ) + + common_obj_fields = { + "organization": "-", + "funder": "-", + "funding_country": "-", + "website": "-", + "description": "This was inserted by an automated script.", + "contact_information": "-", + "hypothetical": False, + # "contact_name": "-", + # "contact_title": "-", + # "contact_email": "-", + # "tags": "-", + # "version": "-", + } + + observing_system = NodeSubtypeOther( + **common_obj_fields, + title="This is a test observing system", + short_name="Test observing system", + type=NodeType.OBSERVING_SYSTEM, + ) + data_product = NodeSubtypeOther( + **common_obj_fields, + title="This is a test data product", + short_name="Test data product", + type=NodeType.DATA_PRODUCT, + ) + application = NodeSubtypeOther( + **common_obj_fields, + title="This is a test application", + short_name="Test application", + type=NodeType.APPLICATION, + ) + sba = NodeSubtypeSocietalBenefitArea( + title="This is a test SBA", + short_name="Test SBA", + societal_benefit_area_id=next(iter(IAOA_SBA_FRAMEWORK.keys())), + type=NodeType.SOCIETAL_BENEFIT_AREA, + ) + + assessment_observing_system = AssessmentNode( + assessment=assessment, + node=observing_system, + ) + assessment_data_product = AssessmentNode( + assessment=assessment, + node=data_product, + ) + os_dp_link = Link( + source_assessment_node=assessment_observing_system, + target_assessment_node=assessment_data_product, + performance_rating=25, + ) + + assessment_application = AssessmentNode( + assessment=assessment, + node=application, + ) + dp_app_link = Link( + source_assessment_node=assessment_data_product, + target_assessment_node=assessment_application, + performance_rating=50, + ) + + assessment_sba = AssessmentNode( + assessment=assessment, + node=sba, + ) + app_sba_link = Link( + source_assessment_node=assessment_application, + target_assessment_node=assessment_sba, + performance_rating=75, + ) + + session.add_all( + [ + assessment, + observing_system, + data_product, + application, + sba, + assessment_observing_system, + assessment_data_product, + assessment_application, + assessment_sba, + os_dp_link, + dp_app_link, + app_sba_link, + ], + ) + session.commit() + + +def _init_dev_user(session: Session) -> None: + logger.warning("Inserting dev user. This should not happen in production!") + session.add(DEV_USER) + session.commit() diff --git a/usaon_benefit_tool/util/db/user.py b/usaon_benefit_tool/util/db/user.py index d7e67aab..6d39f570 100644 --- a/usaon_benefit_tool/util/db/user.py +++ b/usaon_benefit_tool/util/db/user.py @@ -8,6 +8,10 @@ def ensure_user_exists(google_json: dict) -> User: Note: Does this include user updates for something like a name change? """ user = User(email=google_json['email'], name=google_json['name']) + + # NOTE: Alternative form is pseudocode: + # try: query.filter_by(email == ...).one() + # except NoResultFound: insert user_db = db.session.query(User).filter(User.email == user.email).one_or_none() if user_db: return user_db diff --git a/usaon_benefit_tool/util/flask_jsglue.py b/usaon_benefit_tool/util/flask_jsglue.py new file mode 100644 index 00000000..2c9f5647 --- /dev/null +++ b/usaon_benefit_tool/util/flask_jsglue.py @@ -0,0 +1,79 @@ +"""Enable access to url_for in JavaScript code. + +Vendored from https://github.com/stewartpark/Flask-JSGlue; appears abandoned. +Applied changes to work with modern versions of these dependencies. + +Copyright 2022 Stewart Park +License: BSD 3-clause + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT +OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +""" +import json +import re + +from flask import make_response, render_template, url_for +from markupsafe import Markup + +JSGLUE_JS_PATH = '/jsglue.js' +JSGLUE_NAMESPACE = 'Flask' +rule_parser = re.compile(r'<(.+?)>') +splitter = re.compile(r'<.+?>') + + +def get_routes(app): + output = [] + for r in app.url_map.iter_rules(): + endpoint = r.endpoint + if app.config['APPLICATION_ROOT'] == '/' or not app.config['APPLICATION_ROOT']: + rule = r.rule + else: + rule = '{root}{rule}'.format( + root=app.config['APPLICATION_ROOT'], + rule=r.rule, + ) + rule_args = [x.split(':')[-1] for x in rule_parser.findall(rule)] + rule_tr = splitter.split(rule) + output.append((endpoint, rule_tr, rule_args)) + return sorted(output, key=lambda x: len(x[1]), reverse=True) + + +class JSGlue: + def __init__(self, app=None): + self.app = app + if app is not None: + self.init_app(app) + + def init_app(self, app): + self.app = app + + @app.route(JSGLUE_JS_PATH) + def serve_js(): + return make_response( + (self.generate_js(), 200, {'Content-Type': 'text/javascript'}), + ) + + @app.context_processor + def context_processor(): + return {'JSGlue': JSGlue} + + def generate_js(self): + rules = get_routes(self.app) + # NOTE: File has .js extension to avoid autoescaping + return render_template( + 'jsglue/js_bridge.js', + namespace=JSGLUE_NAMESPACE, + rules=json.dumps(rules), + ) + + @staticmethod + def include(): + js_path = url_for('serve_js') + return Markup('') % (js_path,) diff --git a/usaon_benefit_tool/util/full_sankey.py b/usaon_benefit_tool/util/full_sankey.py deleted file mode 100644 index c62c357b..00000000 --- a/usaon_benefit_tool/util/full_sankey.py +++ /dev/null @@ -1,23 +0,0 @@ -from usaon_benefit_tool.models.tables import Response -from usaon_benefit_tool.util.sankey import ( - _applications_sankey, - _data_products_sankey, - _societal_benefit_areas_sankey, -) - - -# TODO: Can we do better than `object` here? Mypy doesn't correctly infer `str | int` -def sankey(response: Response) -> list[list[object]]: - """Provide Sankey data structure, formatted for Highcharts.""" - # Convert tuples to lists for passing to Javascript-land: - data = [list(e) for e in _sankey(response)] - return data - - -def _sankey(response: Response) -> list[tuple[str, str, int]]: - """Provide a sankey data structure of response objects.""" - data1 = _applications_sankey(response) - data2 = _data_products_sankey(response) - data3 = _societal_benefit_areas_sankey(response) - data = data1 + data2 + data3 - return data diff --git a/usaon_benefit_tool/routes/survey/relationships/__init__.py b/usaon_benefit_tool/util/monkeypatch/__init__.py similarity index 100% rename from usaon_benefit_tool/routes/survey/relationships/__init__.py rename to usaon_benefit_tool/util/monkeypatch/__init__.py diff --git a/usaon_benefit_tool/util/monkeypatch/wtforms_sqlalchemy.py b/usaon_benefit_tool/util/monkeypatch/wtforms_sqlalchemy.py new file mode 100644 index 00000000..b280fc37 --- /dev/null +++ b/usaon_benefit_tool/util/monkeypatch/wtforms_sqlalchemy.py @@ -0,0 +1,54 @@ +from sqlalchemy import inspect as sainspect +from wtforms_sqlalchemy.orm import ModelConverter + + +def model_fields( # noqa: C901, PLR0913 + model, + db_session=None, + only=None, + exclude=None, + field_args=None, + converter=None, + exclude_pk=False, # noqa: FBT002 + exclude_fk=False, # noqa: FBT002 +): + """Generate a dictionary of fields for a given SQLAlchemy model. + + See `model_form` docstring for description of parameters. + + FROM: + https://github.com/wtforms/wtforms-sqlalchemy/blob/09b3d4745ec98d6d8f769f6794bc217c63d81946/wtforms_sqlalchemy/orm.py + + PATCHES: + * Skip props that are polymorphic discriminators. + """ + mapper = sainspect(model) + converter = converter or ModelConverter() + field_args = field_args or {} + properties = [] + + for prop in mapper.attrs.values(): + if getattr(prop, "_is_polymorphic_discriminator", False): + continue + if getattr(prop, "columns", None): + if exclude_fk and prop.columns[0].foreign_keys: + continue + elif exclude_pk and prop.columns[0].primary_key: + continue + + properties.append((prop.key, prop)) + + # ((p.key, p) for p in mapper.iterate_properties) + if only: + properties = (x for x in properties if x[0] in only) + elif exclude: + properties = (x for x in properties if x[0] not in exclude) + + field_dict = {} + for name, prop in properties: + field = converter.convert(model, mapper, prop, field_args.get(name), db_session) + + if field is not None: + field_dict[name] = field + + return field_dict diff --git a/usaon_benefit_tool/util/node_type.py b/usaon_benefit_tool/util/node_type.py new file mode 100644 index 00000000..e8a50bbd --- /dev/null +++ b/usaon_benefit_tool/util/node_type.py @@ -0,0 +1,16 @@ +from usaon_benefit_tool._types import NodeType +from usaon_benefit_tool.models.tables import ( + NodeSubtypeOther, + NodeSubtypeSocietalBenefitArea, +) + + +def get_node_class_by_type(node_type: NodeType): + """Get the appropriate Node subclass based on `node_type`. + + This duplicates logic found in the data model, powered by a `case()` statement. + """ + if node_type == NodeType.SOCIETAL_BENEFIT_AREA: + return NodeSubtypeSocietalBenefitArea + + return NodeSubtypeOther diff --git a/usaon_benefit_tool/util/sankey.py b/usaon_benefit_tool/util/sankey.py index c69c1a3e..7f904fa4 100644 --- a/usaon_benefit_tool/util/sankey.py +++ b/usaon_benefit_tool/util/sankey.py @@ -1,102 +1,172 @@ from itertools import chain - -from usaon_benefit_tool.models.tables import ( - Response, - ResponseApplication, - ResponseDataProduct, - ResponseObservingSystem, +from typing import NotRequired, TypedDict + +from usaon_benefit_tool.constants.sankey import DUMMY_NODE_ID +from usaon_benefit_tool.models.tables import Assessment, AssessmentNode, Node + +# NOTE: Can't use class syntax because of hard keyword conflict "from" +HighchartsSankeySeriesLink = TypedDict( + 'HighchartsSankeySeriesLink', + { + "from": str, + "to": str, + "weight": int, + "color": NotRequired[str], + }, ) +# TODO: Use dataclasses instead +class HighchartsSankeySeriesNode(TypedDict): + id: str + name: str + type: str + color: NotRequired[str] + + +class HighchartsSankeySeries(TypedDict): + """Highcharts Sankey series data. + + Based on https://api.highcharts.com/highcharts/series.sankey.data + """ + + data: list[HighchartsSankeySeriesLink] + nodes: list[HighchartsSankeySeriesNode] + + # TODO: Can we do better than `object` here? Mypy doesn't correctly infer `str | int` -def applications_sankey(response: Response) -> list[list[object]]: - """Provide Sankey data structure of applications, formatted for Highcharts.""" - # Convert tuples to lists for passing to Javascript-land: - data = [list(e) for e in _applications_sankey(response)] - return data - - -def data_products_sankey(response: Response) -> list[list[object]]: - """Provide Sankey data structure of applications, formatted for Highcharts.""" - # Convert tuples to lists for passing to Javascript-land: - data = [list(e) for e in _data_products_sankey(response)] - return data - - -def societal_benefit_areas_sankey(response: Response) -> list[list[object]]: - """Provide Sankey data structure of applications, formatted for Highcharts.""" - # Convert tuples to lists for passing to Javascript-land: - data = [list(e) for e in _societal_benefit_areas_sankey(response)] - return data - - -def _societal_benefit_areas_sankey(response: Response): - data = list( - chain.from_iterable( - [ - _application_societal_benefit_area_sankey_links(application) - for application in response.applications - ], - ), - ) - return data +def sankey(assessment: Assessment) -> HighchartsSankeySeries: + """Provide Sankey data structure, formatted for Highcharts.""" + series = _sankey(assessment) + return series + + +def sankey_subset( + assessment: Assessment, + include_related_to_type: type[AssessmentNode], +) -> HighchartsSankeySeries: + """Provide subset of Sankey data structure. + + Include only nodes related to `include_related_to_type`. + """ + series = _sankey(assessment) + # FIXME: Using `cls.__name__` could be much better. Replace with an Enum for node + # type. + node_ids_matching_object_type = [ + n["id"] + for n in series["nodes"] + if n["type"] == include_related_to_type.__name__ + ] + + # For now, select only the outputs. That was the previous behavior, but do we like + # it? + filtered_links = [ + link for link in series["data"] if link["from"] in node_ids_matching_object_type + ] + filtered_node_ids = _node_ids_in_links(filtered_links) + filtered_nodes = [ + node for node in series["nodes"] if node["id"] in filtered_node_ids + ] + return { + "data": filtered_links, + "nodes": filtered_nodes, + } + + +def _sankey(assessment: Assessment) -> HighchartsSankeySeries: + """Extract Sankey-relevant data from Response and format for Highcharts.""" + assessment_nodes: list[AssessmentNode] = assessment.assessment_nodes + nodes_simplified: list[HighchartsSankeySeriesNode] = [ + { + "id": _node_id(an.node), + "name": an.node.short_name, + "type": an.node.type.value, + } + for an in assessment_nodes + ] -def _applications_sankey(response: Response) -> list[tuple[str, str, int]]: - """Provide a sankey data structure of applications, formatted for type checker.""" - data = list( - chain.from_iterable( - [ - _data_product_application_sankey_links(data_product) - for data_product in response.data_products - ], + links = list( + set( + chain.from_iterable( + [ + *[an.input_links for an in assessment_nodes], + *[an.output_links for an in assessment_nodes], + ], + ), ), ) - return data - + links_simplified: list[HighchartsSankeySeriesLink] = [ + { + "from": _node_id(link.source_assessment_node.node), + "to": _node_id(link.target_assessment_node.node), + "weight": link.performance_rating, + } + for link in links + ] -def _data_products_sankey(response: Response) -> list[tuple[str, str, int]]: - """Provide a sankey data structure of applications, formatted for type checker.""" - data = list( - chain.from_iterable( - [ - _observing_system_data_product_sankey_links(observing_system) - for observing_system in response.observing_systems - ], - ), + series = _handle_unlinked_sankey_nodes( + { + "data": links_simplified, + "nodes": nodes_simplified, + }, ) - return data + return series -def _data_product_application_sankey_links( - data_product: ResponseDataProduct, -) -> list[tuple[str, str, int]]: - data = [ - (data_product.short_name, r.application.short_name, r.performance_rating) - for r in data_product.output_relationships - ] - return data +def _node_id(node: Node) -> str: + """Generate a unique node id. + + The IDs of the node elements need to be made unique by adding the "type" (class + name) as a prefix. + """ + return f"{node.type.value}_{node.id}" + + +def _handle_unlinked_sankey_nodes( + series: HighchartsSankeySeries, +) -> HighchartsSankeySeries: + """Add a dummy link for every unlinked node. + + Highcharts doesn't show unlinked nodes, so we need to make a link to a fake node + to display them. We set the weight to 10 for usability. + NOTE: Tooltips are hidden in the javascript. -def _observing_system_data_product_sankey_links( - observing_system: ResponseObservingSystem, -) -> list[tuple[str, str, int]]: - data = [ - (observing_system.short_name, r.data_product.short_name, r.performance_rating) - for r in observing_system.output_relationships + See Also + -------- + https://stackoverflow.com/questions/73033817/highcharts-sankey-node-without-links + """ + orphan_nodes = [ + n for n in series["nodes"] if n["id"] not in _node_ids_in_links(series["data"]) ] - return data - - -def _application_societal_benefit_area_sankey_links( - application: ResponseApplication, -) -> list[tuple[str, str, int]]: - data = [ - ( - application.short_name, - r.societal_benefit_area.societal_benefit_area_id, - r.performance_rating, - ) - for r in application.output_relationships + if not orphan_nodes: + return series + + dummy_links: list[HighchartsSankeySeriesLink] = [ + { + "from": DUMMY_NODE_ID, + "to": n["id"], + "weight": 10, + "color": "transparent", + } + for n in orphan_nodes ] - return data + dummy_node: HighchartsSankeySeriesNode = { + "id": DUMMY_NODE_ID, + "name": "WARNING: Please ensure all nodes have links!", + "type": DUMMY_NODE_ID, + "color": "transparent", + } + return { + "data": dummy_links + series["data"], + "nodes": [dummy_node] + series["nodes"], + } + + +def _node_ids_in_links(links: list[HighchartsSankeySeriesLink]) -> set[str]: + """Get the unique node IDs present in `series["data"]`.""" + node_id_tuples = [(link["from"], link["to"]) for link in links] + node_ids = set(chain.from_iterable(node_id_tuples)) + + return node_ids