From de3ce43f353753c0bc173d8b57ab654466c77c14 Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Mon, 19 Feb 2024 20:17:18 +0100 Subject: [PATCH 01/21] updated imports --- src/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main.py b/src/main.py index d362393..7b0b5ea 100644 --- a/src/main.py +++ b/src/main.py @@ -6,7 +6,7 @@ from pathlib import Path import logging import os -from web import app +from src.web import server # load default values config = ConfigParser() @@ -93,6 +93,7 @@ def initialise_app() -> None: if __name__ == "__main__": + from src.web import app # initialise_app() app.run(debug=True) logging.info("started app") \ No newline at end of file From 954e1f898f639cb2d0acd90f6cef3171da233960 Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Mon, 19 Feb 2024 20:17:53 +0100 Subject: [PATCH 02/21] updated copy target dir --- Dockerfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 8f6a6ca..5671d4d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -34,7 +34,7 @@ ENV PATH="${POETRY_HOME}/bin:$PATH" # install runtime dependencies WORKDIR ${APP_HOME} COPY poetry.lock pyproject.toml ./ -RUN --mount=type=cache,target=${POETRY_CACHE_DIR} poetry install --no-root +RUN --mount=type=cache,target=${POETRY_CACHE_DIR} poetry install ######### # FINAL # @@ -56,9 +56,9 @@ RUN mkdir -p /home/app && \ # add code while changing ownership WORKDIR $APP_HOME -COPY --chown=app:app ./src . +COPY --chown=app:app ./src ./src # change to the app user USER app -ENTRYPOINT [ "gunicorn", "web:server", "-b", "0.0.0.0:8050" ] \ No newline at end of file +ENTRYPOINT [ "gunicorn", "src.main:server", "-b", "0.0.0.0:8050" ] \ No newline at end of file From 5fa8d737aed2cb2aaaddcf762d85a30b52294d2a Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Mon, 19 Feb 2024 20:18:01 +0100 Subject: [PATCH 03/21] installed packages --- poetry.lock | 214 ++++++++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 7 +- 2 files changed, 216 insertions(+), 5 deletions(-) diff --git a/poetry.lock b/poetry.lock index 2fd2902..5aa1f26 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,5 +1,16 @@ # This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +[[package]] +name = "annotated-types" +version = "0.6.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"}, + {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, +] + [[package]] name = "blinker" version = "1.7.0" @@ -448,6 +459,91 @@ files = [ {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, ] +[[package]] +name = "pillow" +version = "10.2.0" +description = "Python Imaging Library (Fork)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pillow-10.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:7823bdd049099efa16e4246bdf15e5a13dbb18a51b68fa06d6c1d4d8b99a796e"}, + {file = "pillow-10.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:83b2021f2ade7d1ed556bc50a399127d7fb245e725aa0113ebd05cfe88aaf588"}, + {file = "pillow-10.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fad5ff2f13d69b7e74ce5b4ecd12cc0ec530fcee76356cac6742785ff71c452"}, + {file = "pillow-10.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da2b52b37dad6d9ec64e653637a096905b258d2fc2b984c41ae7d08b938a67e4"}, + {file = "pillow-10.2.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:47c0995fc4e7f79b5cfcab1fc437ff2890b770440f7696a3ba065ee0fd496563"}, + {file = "pillow-10.2.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:322bdf3c9b556e9ffb18f93462e5f749d3444ce081290352c6070d014c93feb2"}, + {file = "pillow-10.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:51f1a1bffc50e2e9492e87d8e09a17c5eea8409cda8d3f277eb6edc82813c17c"}, + {file = "pillow-10.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:69ffdd6120a4737710a9eee73e1d2e37db89b620f702754b8f6e62594471dee0"}, + {file = "pillow-10.2.0-cp310-cp310-win32.whl", hash = "sha256:c6dafac9e0f2b3c78df97e79af707cdc5ef8e88208d686a4847bab8266870023"}, + {file = "pillow-10.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:aebb6044806f2e16ecc07b2a2637ee1ef67a11840a66752751714a0d924adf72"}, + {file = "pillow-10.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:7049e301399273a0136ff39b84c3678e314f2158f50f517bc50285fb5ec847ad"}, + {file = "pillow-10.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:35bb52c37f256f662abdfa49d2dfa6ce5d93281d323a9af377a120e89a9eafb5"}, + {file = "pillow-10.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c23f307202661071d94b5e384e1e1dc7dfb972a28a2310e4ee16103e66ddb67"}, + {file = "pillow-10.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:773efe0603db30c281521a7c0214cad7836c03b8ccff897beae9b47c0b657d61"}, + {file = "pillow-10.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11fa2e5984b949b0dd6d7a94d967743d87c577ff0b83392f17cb3990d0d2fd6e"}, + {file = "pillow-10.2.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:716d30ed977be8b37d3ef185fecb9e5a1d62d110dfbdcd1e2a122ab46fddb03f"}, + {file = "pillow-10.2.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a086c2af425c5f62a65e12fbf385f7c9fcb8f107d0849dba5839461a129cf311"}, + {file = "pillow-10.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c8de2789052ed501dd829e9cae8d3dcce7acb4777ea4a479c14521c942d395b1"}, + {file = "pillow-10.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:609448742444d9290fd687940ac0b57fb35e6fd92bdb65386e08e99af60bf757"}, + {file = "pillow-10.2.0-cp311-cp311-win32.whl", hash = "sha256:823ef7a27cf86df6597fa0671066c1b596f69eba53efa3d1e1cb8b30f3533068"}, + {file = "pillow-10.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:1da3b2703afd040cf65ec97efea81cfba59cdbed9c11d8efc5ab09df9509fc56"}, + {file = "pillow-10.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:edca80cbfb2b68d7b56930b84a0e45ae1694aeba0541f798e908a49d66b837f1"}, + {file = "pillow-10.2.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:1b5e1b74d1bd1b78bc3477528919414874748dd363e6272efd5abf7654e68bef"}, + {file = "pillow-10.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0eae2073305f451d8ecacb5474997c08569fb4eb4ac231ffa4ad7d342fdc25ac"}, + {file = "pillow-10.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7c2286c23cd350b80d2fc9d424fc797575fb16f854b831d16fd47ceec078f2c"}, + {file = "pillow-10.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e23412b5c41e58cec602f1135c57dfcf15482013ce6e5f093a86db69646a5aa"}, + {file = "pillow-10.2.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:52a50aa3fb3acb9cf7213573ef55d31d6eca37f5709c69e6858fe3bc04a5c2a2"}, + {file = "pillow-10.2.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:127cee571038f252a552760076407f9cff79761c3d436a12af6000cd182a9d04"}, + {file = "pillow-10.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:8d12251f02d69d8310b046e82572ed486685c38f02176bd08baf216746eb947f"}, + {file = "pillow-10.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:54f1852cd531aa981bc0965b7d609f5f6cc8ce8c41b1139f6ed6b3c54ab82bfb"}, + {file = "pillow-10.2.0-cp312-cp312-win32.whl", hash = "sha256:257d8788df5ca62c980314053197f4d46eefedf4e6175bc9412f14412ec4ea2f"}, + {file = "pillow-10.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:154e939c5f0053a383de4fd3d3da48d9427a7e985f58af8e94d0b3c9fcfcf4f9"}, + {file = "pillow-10.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:f379abd2f1e3dddb2b61bc67977a6b5a0a3f7485538bcc6f39ec76163891ee48"}, + {file = "pillow-10.2.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:8373c6c251f7ef8bda6675dd6d2b3a0fcc31edf1201266b5cf608b62a37407f9"}, + {file = "pillow-10.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:870ea1ada0899fd0b79643990809323b389d4d1d46c192f97342eeb6ee0b8483"}, + {file = "pillow-10.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4b6b1e20608493548b1f32bce8cca185bf0480983890403d3b8753e44077129"}, + {file = "pillow-10.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3031709084b6e7852d00479fd1d310b07d0ba82765f973b543c8af5061cf990e"}, + {file = "pillow-10.2.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:3ff074fc97dd4e80543a3e91f69d58889baf2002b6be64347ea8cf5533188213"}, + {file = "pillow-10.2.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:cb4c38abeef13c61d6916f264d4845fab99d7b711be96c326b84df9e3e0ff62d"}, + {file = "pillow-10.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b1b3020d90c2d8e1dae29cf3ce54f8094f7938460fb5ce8bc5c01450b01fbaf6"}, + {file = "pillow-10.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:170aeb00224ab3dc54230c797f8404507240dd868cf52066f66a41b33169bdbe"}, + {file = "pillow-10.2.0-cp38-cp38-win32.whl", hash = "sha256:c4225f5220f46b2fde568c74fca27ae9771536c2e29d7c04f4fb62c83275ac4e"}, + {file = "pillow-10.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:0689b5a8c5288bc0504d9fcee48f61a6a586b9b98514d7d29b840143d6734f39"}, + {file = "pillow-10.2.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:b792a349405fbc0163190fde0dc7b3fef3c9268292586cf5645598b48e63dc67"}, + {file = "pillow-10.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c570f24be1e468e3f0ce7ef56a89a60f0e05b30a3669a459e419c6eac2c35364"}, + {file = "pillow-10.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8ecd059fdaf60c1963c58ceb8997b32e9dc1b911f5da5307aab614f1ce5c2fb"}, + {file = "pillow-10.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c365fd1703040de1ec284b176d6af5abe21b427cb3a5ff68e0759e1e313a5e7e"}, + {file = "pillow-10.2.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:70c61d4c475835a19b3a5aa42492409878bbca7438554a1f89d20d58a7c75c01"}, + {file = "pillow-10.2.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b6f491cdf80ae540738859d9766783e3b3c8e5bd37f5dfa0b76abdecc5081f13"}, + {file = "pillow-10.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9d189550615b4948f45252d7f005e53c2040cea1af5b60d6f79491a6e147eef7"}, + {file = "pillow-10.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:49d9ba1ed0ef3e061088cd1e7538a0759aab559e2e0a80a36f9fd9d8c0c21591"}, + {file = "pillow-10.2.0-cp39-cp39-win32.whl", hash = "sha256:babf5acfede515f176833ed6028754cbcd0d206f7f614ea3447d67c33be12516"}, + {file = "pillow-10.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:0304004f8067386b477d20a518b50f3fa658a28d44e4116970abfcd94fac34a8"}, + {file = "pillow-10.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:0fb3e7fc88a14eacd303e90481ad983fd5b69c761e9e6ef94c983f91025da869"}, + {file = "pillow-10.2.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:322209c642aabdd6207517e9739c704dc9f9db943015535783239022002f054a"}, + {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3eedd52442c0a5ff4f887fab0c1c0bb164d8635b32c894bc1faf4c618dd89df2"}, + {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb28c753fd5eb3dd859b4ee95de66cc62af91bcff5db5f2571d32a520baf1f04"}, + {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:33870dc4653c5017bf4c8873e5488d8f8d5f8935e2f1fb9a2208c47cdd66efd2"}, + {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3c31822339516fb3c82d03f30e22b1d038da87ef27b6a78c9549888f8ceda39a"}, + {file = "pillow-10.2.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a2b56ba36e05f973d450582fb015594aaa78834fefe8dfb8fcd79b93e64ba4c6"}, + {file = "pillow-10.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:d8e6aeb9201e655354b3ad049cb77d19813ad4ece0df1249d3c793de3774f8c7"}, + {file = "pillow-10.2.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:2247178effb34a77c11c0e8ac355c7a741ceca0a732b27bf11e747bbc950722f"}, + {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15587643b9e5eb26c48e49a7b33659790d28f190fc514a322d55da2fb5c2950e"}, + {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753cd8f2086b2b80180d9b3010dd4ed147efc167c90d3bf593fe2af21265e5a5"}, + {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7c8f97e8e7a9009bcacbe3766a36175056c12f9a44e6e6f2d5caad06dcfbf03b"}, + {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d1b35bcd6c5543b9cb547dee3150c93008f8dd0f1fef78fc0cd2b141c5baf58a"}, + {file = "pillow-10.2.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:fe4c15f6c9285dc54ce6553a3ce908ed37c8f3825b5a51a15c91442bb955b868"}, + {file = "pillow-10.2.0.tar.gz", hash = "sha256:e87f0b2c78157e12d7686b27d63c070fd65d994e8ddae6f328e0dcf4a0cd007e"}, +] + +[package.extras] +docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"] +fpx = ["olefile"] +mic = ["olefile"] +tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] +typing = ["typing-extensions"] +xmp = ["defusedxml"] + [[package]] name = "plotly" version = "5.19.0" @@ -463,6 +559,116 @@ files = [ packaging = "*" tenacity = ">=6.2.0" +[[package]] +name = "pydantic" +version = "2.6.1" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic-2.6.1-py3-none-any.whl", hash = "sha256:0b6a909df3192245cb736509a92ff69e4fef76116feffec68e93a567347bae6f"}, + {file = "pydantic-2.6.1.tar.gz", hash = "sha256:4fd5c182a2488dc63e6d32737ff19937888001e2a6d86e94b3f233104a5d1fa9"}, +] + +[package.dependencies] +annotated-types = ">=0.4.0" +pydantic-core = "2.16.2" +typing-extensions = ">=4.6.1" + +[package.extras] +email = ["email-validator (>=2.0.0)"] + +[[package]] +name = "pydantic-core" +version = "2.16.2" +description = "" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic_core-2.16.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3fab4e75b8c525a4776e7630b9ee48aea50107fea6ca9f593c98da3f4d11bf7c"}, + {file = "pydantic_core-2.16.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8bde5b48c65b8e807409e6f20baee5d2cd880e0fad00b1a811ebc43e39a00ab2"}, + {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2924b89b16420712e9bb8192396026a8fbd6d8726224f918353ac19c4c043d2a"}, + {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:16aa02e7a0f539098e215fc193c8926c897175d64c7926d00a36188917717a05"}, + {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:936a787f83db1f2115ee829dd615c4f684ee48ac4de5779ab4300994d8af325b"}, + {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:459d6be6134ce3b38e0ef76f8a672924460c455d45f1ad8fdade36796df1ddc8"}, + {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9ee4febb249c591d07b2d4dd36ebcad0ccd128962aaa1801508320896575ef"}, + {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:40a0bd0bed96dae5712dab2aba7d334a6c67cbcac2ddfca7dbcc4a8176445990"}, + {file = "pydantic_core-2.16.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:870dbfa94de9b8866b37b867a2cb37a60c401d9deb4a9ea392abf11a1f98037b"}, + {file = "pydantic_core-2.16.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:308974fdf98046db28440eb3377abba274808bf66262e042c412eb2adf852731"}, + {file = "pydantic_core-2.16.2-cp310-none-win32.whl", hash = "sha256:a477932664d9611d7a0816cc3c0eb1f8856f8a42435488280dfbf4395e141485"}, + {file = "pydantic_core-2.16.2-cp310-none-win_amd64.whl", hash = "sha256:8f9142a6ed83d90c94a3efd7af8873bf7cefed2d3d44387bf848888482e2d25f"}, + {file = "pydantic_core-2.16.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:406fac1d09edc613020ce9cf3f2ccf1a1b2f57ab00552b4c18e3d5276c67eb11"}, + {file = "pydantic_core-2.16.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ce232a6170dd6532096cadbf6185271e4e8c70fc9217ebe105923ac105da9978"}, + {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a90fec23b4b05a09ad988e7a4f4e081711a90eb2a55b9c984d8b74597599180f"}, + {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8aafeedb6597a163a9c9727d8a8bd363a93277701b7bfd2749fbefee2396469e"}, + {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9957433c3a1b67bdd4c63717eaf174ebb749510d5ea612cd4e83f2d9142f3fc8"}, + {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b0d7a9165167269758145756db43a133608a531b1e5bb6a626b9ee24bc38a8f7"}, + {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dffaf740fe2e147fedcb6b561353a16243e654f7fe8e701b1b9db148242e1272"}, + {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8ed79883b4328b7f0bd142733d99c8e6b22703e908ec63d930b06be3a0e7113"}, + {file = "pydantic_core-2.16.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:cf903310a34e14651c9de056fcc12ce090560864d5a2bb0174b971685684e1d8"}, + {file = "pydantic_core-2.16.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:46b0d5520dbcafea9a8645a8164658777686c5c524d381d983317d29687cce97"}, + {file = "pydantic_core-2.16.2-cp311-none-win32.whl", hash = "sha256:70651ff6e663428cea902dac297066d5c6e5423fda345a4ca62430575364d62b"}, + {file = "pydantic_core-2.16.2-cp311-none-win_amd64.whl", hash = "sha256:98dc6f4f2095fc7ad277782a7c2c88296badcad92316b5a6e530930b1d475ebc"}, + {file = "pydantic_core-2.16.2-cp311-none-win_arm64.whl", hash = "sha256:ef6113cd31411eaf9b39fc5a8848e71c72656fd418882488598758b2c8c6dfa0"}, + {file = "pydantic_core-2.16.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:88646cae28eb1dd5cd1e09605680c2b043b64d7481cdad7f5003ebef401a3039"}, + {file = "pydantic_core-2.16.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7b883af50eaa6bb3299780651e5be921e88050ccf00e3e583b1e92020333304b"}, + {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bf26c2e2ea59d32807081ad51968133af3025c4ba5753e6a794683d2c91bf6e"}, + {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:99af961d72ac731aae2a1b55ccbdae0733d816f8bfb97b41909e143de735f522"}, + {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:02906e7306cb8c5901a1feb61f9ab5e5c690dbbeaa04d84c1b9ae2a01ebe9379"}, + {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5362d099c244a2d2f9659fb3c9db7c735f0004765bbe06b99be69fbd87c3f15"}, + {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ac426704840877a285d03a445e162eb258924f014e2f074e209d9b4ff7bf380"}, + {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b94cbda27267423411c928208e89adddf2ea5dd5f74b9528513f0358bba019cb"}, + {file = "pydantic_core-2.16.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:6db58c22ac6c81aeac33912fb1af0e930bc9774166cdd56eade913d5f2fff35e"}, + {file = "pydantic_core-2.16.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:396fdf88b1b503c9c59c84a08b6833ec0c3b5ad1a83230252a9e17b7dfb4cffc"}, + {file = "pydantic_core-2.16.2-cp312-none-win32.whl", hash = "sha256:7c31669e0c8cc68400ef0c730c3a1e11317ba76b892deeefaf52dcb41d56ed5d"}, + {file = "pydantic_core-2.16.2-cp312-none-win_amd64.whl", hash = "sha256:a3b7352b48fbc8b446b75f3069124e87f599d25afb8baa96a550256c031bb890"}, + {file = "pydantic_core-2.16.2-cp312-none-win_arm64.whl", hash = "sha256:a9e523474998fb33f7c1a4d55f5504c908d57add624599e095c20fa575b8d943"}, + {file = "pydantic_core-2.16.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:ae34418b6b389d601b31153b84dce480351a352e0bb763684a1b993d6be30f17"}, + {file = "pydantic_core-2.16.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:732bd062c9e5d9582a30e8751461c1917dd1ccbdd6cafb032f02c86b20d2e7ec"}, + {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4b52776a2e3230f4854907a1e0946eec04d41b1fc64069ee774876bbe0eab55"}, + {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ef551c053692b1e39e3f7950ce2296536728871110e7d75c4e7753fb30ca87f4"}, + {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ebb892ed8599b23fa8f1799e13a12c87a97a6c9d0f497525ce9858564c4575a4"}, + {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa6c8c582036275997a733427b88031a32ffa5dfc3124dc25a730658c47a572f"}, + {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4ba0884a91f1aecce75202473ab138724aa4fb26d7707f2e1fa6c3e68c84fbf"}, + {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7924e54f7ce5d253d6160090ddc6df25ed2feea25bfb3339b424a9dd591688bc"}, + {file = "pydantic_core-2.16.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69a7b96b59322a81c2203be537957313b07dd333105b73db0b69212c7d867b4b"}, + {file = "pydantic_core-2.16.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7e6231aa5bdacda78e96ad7b07d0c312f34ba35d717115f4b4bff6cb87224f0f"}, + {file = "pydantic_core-2.16.2-cp38-none-win32.whl", hash = "sha256:41dac3b9fce187a25c6253ec79a3f9e2a7e761eb08690e90415069ea4a68ff7a"}, + {file = "pydantic_core-2.16.2-cp38-none-win_amd64.whl", hash = "sha256:f685dbc1fdadb1dcd5b5e51e0a378d4685a891b2ddaf8e2bba89bd3a7144e44a"}, + {file = "pydantic_core-2.16.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:55749f745ebf154c0d63d46c8c58594d8894b161928aa41adbb0709c1fe78b77"}, + {file = "pydantic_core-2.16.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b30b0dd58a4509c3bd7eefddf6338565c4905406aee0c6e4a5293841411a1286"}, + {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18de31781cdc7e7b28678df7c2d7882f9692ad060bc6ee3c94eb15a5d733f8f7"}, + {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5864b0242f74b9dd0b78fd39db1768bc3f00d1ffc14e596fd3e3f2ce43436a33"}, + {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8f9186ca45aee030dc8234118b9c0784ad91a0bb27fc4e7d9d6608a5e3d386c"}, + {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc6f6c9be0ab6da37bc77c2dda5f14b1d532d5dbef00311ee6e13357a418e646"}, + {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa057095f621dad24a1e906747179a69780ef45cc8f69e97463692adbcdae878"}, + {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6ad84731a26bcfb299f9eab56c7932d46f9cad51c52768cace09e92a19e4cf55"}, + {file = "pydantic_core-2.16.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3b052c753c4babf2d1edc034c97851f867c87d6f3ea63a12e2700f159f5c41c3"}, + {file = "pydantic_core-2.16.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e0f686549e32ccdb02ae6f25eee40cc33900910085de6aa3790effd391ae10c2"}, + {file = "pydantic_core-2.16.2-cp39-none-win32.whl", hash = "sha256:7afb844041e707ac9ad9acad2188a90bffce2c770e6dc2318be0c9916aef1469"}, + {file = "pydantic_core-2.16.2-cp39-none-win_amd64.whl", hash = "sha256:9da90d393a8227d717c19f5397688a38635afec89f2e2d7af0df037f3249c39a"}, + {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5f60f920691a620b03082692c378661947d09415743e437a7478c309eb0e4f82"}, + {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:47924039e785a04d4a4fa49455e51b4eb3422d6eaacfde9fc9abf8fdef164e8a"}, + {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6294e76b0380bb7a61eb8a39273c40b20beb35e8c87ee101062834ced19c545"}, + {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe56851c3f1d6f5384b3051c536cc81b3a93a73faf931f404fef95217cf1e10d"}, + {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9d776d30cde7e541b8180103c3f294ef7c1862fd45d81738d156d00551005784"}, + {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:72f7919af5de5ecfaf1eba47bf9a5d8aa089a3340277276e5636d16ee97614d7"}, + {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:4bfcbde6e06c56b30668a0c872d75a7ef3025dc3c1823a13cf29a0e9b33f67e8"}, + {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ff7c97eb7a29aba230389a2661edf2e9e06ce616c7e35aa764879b6894a44b25"}, + {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:9b5f13857da99325dcabe1cc4e9e6a3d7b2e2c726248ba5dd4be3e8e4a0b6d0e"}, + {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a7e41e3ada4cca5f22b478c08e973c930e5e6c7ba3588fb8e35f2398cdcc1545"}, + {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:60eb8ceaa40a41540b9acae6ae7c1f0a67d233c40dc4359c256ad2ad85bdf5e5"}, + {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7beec26729d496a12fd23cf8da9944ee338c8b8a17035a560b585c36fe81af20"}, + {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:22c5f022799f3cd6741e24f0443ead92ef42be93ffda0d29b2597208c94c3753"}, + {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:eca58e319f4fd6df004762419612122b2c7e7d95ffafc37e890252f869f3fb2a"}, + {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ed957db4c33bc99895f3a1672eca7e80e8cda8bd1e29a80536b4ec2153fa9804"}, + {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:459c0d338cc55d099798618f714b21b7ece17eb1a87879f2da20a3ff4c7628e2"}, + {file = "pydantic_core-2.16.2.tar.gz", hash = "sha256:0ba503850d8b8dcc18391f10de896ae51d37fe5fe43dbfb6a35c5c5cad271a06"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + [[package]] name = "python-dotenv" version = "1.0.1" @@ -583,13 +789,13 @@ files = [ [[package]] name = "urllib3" -version = "2.2.0" +version = "2.2.1" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.8" files = [ - {file = "urllib3-2.2.0-py3-none-any.whl", hash = "sha256:ce3711610ddce217e6d113a2732fafad960a03fd0318c91faa79481e35c11224"}, - {file = "urllib3-2.2.0.tar.gz", hash = "sha256:051d961ad0c62a94e50ecf1af379c3aba230c66c710493493560c0c223c49f20"}, + {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, + {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, ] [package.extras] @@ -633,4 +839,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "b80bd2598b5e94ab048aff2a8615067d938b520c5e4e131547e4cbc1a8f664d5" +content-hash = "e4892a5e8db437b5c79b40f259ba6b512d1c0b18677b4f13163fee518276a28d" diff --git a/pyproject.toml b/pyproject.toml index eb5a355..ce99a52 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,9 +1,12 @@ [tool.poetry] -name = "visual-critical-discourse-analysis" +name = "visual_critical_discourse_analysis" version = "0.1.0" description = "" authors = ["Brian Bjarke Jensen "] readme = "README.md" +packages = [ + { include = "src" }, +] [tool.poetry.dependencies] python = "^3.12" @@ -13,6 +16,8 @@ python-dotenv = "^1.0.1" dash = "^2.15.0" dash-bootstrap-components = "^1.5.0" dash-mantine-components = "^0.12.1" +pydantic = "^2.6.1" +pillow = "^10.2.0" [build-system] From e72abf41b7db1f2005cce025eb72c57c94395078 Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Mon, 19 Feb 2024 20:18:20 +0100 Subject: [PATCH 04/21] added init file --- src/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/__init__.py diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 From dd5760dca00ee75e50ceb683e40e7f56edcb61bc Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Mon, 19 Feb 2024 20:18:55 +0100 Subject: [PATCH 05/21] updated layout --- src/web/app.py | 17 +++---- src/web/body.py | 127 +++++++++++++++++++++++++++++------------------- 2 files changed, 84 insertions(+), 60 deletions(-) diff --git a/src/web/app.py b/src/web/app.py index 807b8b8..eac840a 100644 --- a/src/web/app.py +++ b/src/web/app.py @@ -4,9 +4,6 @@ import dash_mantine_components as dmc from .header import generate_header from .body import generate_body -from model_experiential import model_labels as experiential_labels -from model_interpersonal import model_labels as interpersonal_labels -from model_textual import model_labels as textual_labels app = Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP]) @@ -20,14 +17,12 @@ app.layout = dmc.MantineProvider( }, }, children=[ - dmc.Container([ - generate_header(), - generate_body( - experiential_labels, - interpersonal_labels, - textual_labels - ), - ]), + dmc.Container( + [ + generate_header(), + generate_body(), + ], fluid=True + ), ], ) \ No newline at end of file diff --git a/src/web/body.py b/src/web/body.py index dbab304..b31f69f 100644 --- a/src/web/body.py +++ b/src/web/body.py @@ -1,71 +1,100 @@ import dash_mantine_components as dmc from dash import dcc, html +from typing import List -def generate_body( - experiential_labels, - interpersonal_labels, - textual_labels -): +from src.model_experiential import ExperientialModelOutput +from src.model_interpersonal import ( + ContactModelOutput, + AngleModelOutput, + PointOfViewModelOutput, + DistanceModelOutput, + ModalityLightingModelOutput, + ModalityColorModelOutput, + ModalityDepthModelOutput +) +from src.model_textual import ( + InformationValueModelOutput, + FramingModelOutput, + SalienceModelOutput +) + +def generate_option_labels(model) -> List[str]: + """Generate presentable list of attributes from an OutputModel.""" + labels = [ + label.replace('_', ' ').title() + for label in model.list_fields() + ] + return labels + +def generate_options_map(): + """Generate map of titles and options for labels container.""" + options_map = {} + # add experiential labels + options_map["experiential".title()] = generate_option_labels(ExperientialModelOutput) + # add interpersonal labels + options_map["contact".title()] = generate_option_labels(ContactModelOutput) + options_map["angle".title()] = generate_option_labels(AngleModelOutput) + options_map["point of view".title()] = generate_option_labels(PointOfViewModelOutput) + options_map["distance".title()] = generate_option_labels(DistanceModelOutput) + options_map["modality lighting".title()] = generate_option_labels(ModalityLightingModelOutput) + options_map["modality color".title()] = generate_option_labels(ModalityColorModelOutput) + options_map["modality depth".title()] = generate_option_labels(ModalityDepthModelOutput) + # add textual labels + options_map["information value".title()] = generate_option_labels(InformationValueModelOutput) + options_map["framing".title()] = generate_option_labels(FramingModelOutput) + options_map["salience".title()] = generate_option_labels(SalienceModelOutput) + return options_map + +def generate_body(): image_container = dmc.Image( - width=400, - height=400, + width=600, + height=600, withPlaceholder=True, placeholder=[dmc.Loader(color="gray", size="md")], ) - experiential_labels_container = dmc.Container( - children=[ - html.H4("experiential labels".title()), - dcc.RadioItems(options=list(experiential_labels)), - ] - ) - - interpersonal_labels_container = dmc.Container( - children=[] - ) - for category, options in interpersonal_labels.items(): - interpersonal_labels_container.children.append(html.H4(category.title())) - interpersonal_labels_container.children.append(dcc.RadioItems(options)) - - textual_labels_container = dmc.Container( - children=[] - ) - for category, options in textual_labels.items(): - textual_labels_container.children.append(html.H4(category.title())) - textual_labels_container.children.append(dcc.RadioItems(options)) - - label_container = dmc.Container( - children=[ - experiential_labels_container, - dmc.Divider(), - interpersonal_labels_container, - dmc.Divider(), - textual_labels_container, - dmc.Divider(), - html.Button( - "confirm", - id="submit-button" - ) - ] + # prepare labels container + label_container = dmc.Grid( + children=[], ) + # add fields to labels container + for title, options in generate_options_map().items(): + label_container.children.append( + dmc.Col([ + html.H4(title), + dcc.RadioItems(options=options), + ], span=3) + ) + # build the full body container body_container = dmc.Container( dmc.Grid( children=[ dmc.Col( + dmc.Center( image_container, - span=5, - ), - dmc.Col( - dmc.Divider(orientation="vertical"), - span=1, + ), + span=4, ), dmc.Col( # radio buttons part - label_container, - span=5, + children = [ + label_container, + dmc.Button( + "confirm", + id="submit-button", + fullWidth=True, + color="lime", + radius="sm", + size="md", + style={ + "height": "50px" + } + ), + ], span=8, ), + # dmc.Col(span=1), ], grow=True - ) + ), fluid=True ) return body_container \ No newline at end of file From 5388502f42cc19c7b7118c804632fbccd403ca39 Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Mon, 19 Feb 2024 20:19:20 +0100 Subject: [PATCH 06/21] updated model output definitions --- src/model_experiential/__init__.py | 25 ++++- src/model_experiential/classes.py | 61 ++++++++++++ src/model_experiential/output.py | 3 +- src/model_interpersonal/__init__.py | 10 +- src/model_interpersonal/classes.py | 149 ++++++++++++++++++++++++++++ src/model_interpersonal/output.py | 4 +- src/model_textual/__init__.py | 6 +- src/model_textual/classes.py | 75 ++++++++++++++ 8 files changed, 326 insertions(+), 7 deletions(-) create mode 100644 src/model_experiential/classes.py create mode 100644 src/model_interpersonal/classes.py create mode 100644 src/model_textual/classes.py diff --git a/src/model_experiential/__init__.py b/src/model_experiential/__init__.py index b0d4f72..9a2791c 100644 --- a/src/model_experiential/__init__.py +++ b/src/model_experiential/__init__.py @@ -1 +1,24 @@ -from .output import model_labels \ No newline at end of file +from .classes import ExperientialModelOutput + + + +# CLASS_NAME_LIST = Literal[ +# "non transactional action", +# "non transactional reaction", +# "unidirectional transactional action", +# "unidirectional transactional reaction", +# "bidirectional transactional action", +# "bidirectional transactional reaction", +# "conversion", +# "speech process", +# "classification overt taxonomy", +# "analytical exhaustive", +# "analytical disarranged", +# "analytical temporal", +# "analytical distributed", +# "anaytical topological", +# "analytical exploded", +# "analytical inclusive", +# "symbolic suggestive", +# "symbolic attributive" +# ] \ No newline at end of file diff --git a/src/model_experiential/classes.py b/src/model_experiential/classes.py new file mode 100644 index 0000000..9d9bdcd --- /dev/null +++ b/src/model_experiential/classes.py @@ -0,0 +1,61 @@ +from __future__ import annotations +from pydantic import BaseModel +from typing import List +import random + + +class ExperientialModelOutput(BaseModel): + non_transactional_action: float + non_transactional_reaction: float + unidirectional_transactional_action: float + unidirectional_transactional_reaction: float + bidirectional_transactional_action: float + bidirectional_transactional_reaction: float + conversion: float + speech_process: float + classification_overt_taxonomy: float + analytical_exhaustive: float + analytical_disarranged: float + analytical_temporal: float + analytical_distributed: float + analytical_topological: float + analytical_exploded: float + analytical_inclusive: float + symbolic_suggestive: float + symbolic_attributive: float + + @classmethod + def list_fields(cls) -> List[str]: + """List options that are stored as attributes.""" + return list(cls.model_fields.keys()) + + @classmethod + def from_random(cls) -> ExperientialModelOutput: + """Instantiate with random numbers.""" + kwargs = {field: random.random() for field in cls.list_fields()} + return ExperientialModelOutput(**kwargs) + + def __repr__(self) -> str: + model_dict = self.model_dump() + model_repr_str = "ExperientialModelOutput(" + model_repr_str += ", ".join([f"{field}={value:.3f}" for field, value in model_dict.items()]) + model_repr_str += ")" + return model_repr_str + + def highest_score_field(self) -> str: + """Return name of field with highest score.""" + model_dict = self.model_dump() + return max(model_dict, key=lambda k: model_dict[k]) + + def highest_score_value(self) -> float: + """Return value of field with highest score.""" + model_dict = self.model_dump() + return max(model_dict.values()) + + +if __name__ == '__main__': + m = ExperientialModelOutput.from_random() + print(m) + print(repr(m)) + print(m.highest_score_field()) + print(m.highest_score_value()) diff --git a/src/model_experiential/output.py b/src/model_experiential/output.py index bddaf06..188234f 100644 --- a/src/model_experiential/output.py +++ b/src/model_experiential/output.py @@ -1,5 +1,4 @@ - -model_labels = [ +CLASS_NAMES = [ "non transactional action", "non transactional reaction", "unidirectional transactional action", diff --git a/src/model_interpersonal/__init__.py b/src/model_interpersonal/__init__.py index b0d4f72..74211a9 100644 --- a/src/model_interpersonal/__init__.py +++ b/src/model_interpersonal/__init__.py @@ -1 +1,9 @@ -from .output import model_labels \ No newline at end of file +from .classes import ( + ContactModelOutput, + AngleModelOutput, + PointOfViewModelOutput, + DistanceModelOutput, + ModalityLightingModelOutput, + ModalityColorModelOutput, + ModalityDepthModelOutput +) \ No newline at end of file diff --git a/src/model_interpersonal/classes.py b/src/model_interpersonal/classes.py new file mode 100644 index 0000000..60c16ad --- /dev/null +++ b/src/model_interpersonal/classes.py @@ -0,0 +1,149 @@ +from pydantic import BaseModel +from typing import List, Dict +import random + + +class ModelOutput(BaseModel): + + @classmethod + def classname(cls) -> str: + """Return classname.""" + return cls.__name__ + + @classmethod + def list_fields(cls) -> List[str]: + """List options that are stored as attributes.""" + return list(cls.model_fields.keys()) + + @classmethod + def from_random(cls): + """Instantiate with random numbers.""" + kwargs = {field: random.random() for field in cls.list_fields()} + return cls(**kwargs) + + def __repr__(self) -> str: + model_dict = self.model_dump() + model_repr_str = f"{self.classname()}(" + model_repr_str += ", ".join([f"{field}={value:.3f}" for field, value in model_dict.items()]) + model_repr_str += ")" + return model_repr_str + + def highest_score_field(self) -> str: + """Return name of field with highest score.""" + model_dict = self.model_dump() + return max(model_dict, key=lambda k: model_dict[k]) + + def highest_score_value(self) -> float: + """Return value of field with highest score.""" + model_dict = self.model_dump() + return max(model_dict.values()) + + +class ContactModelOutput(ModelOutput): + offer: float + demand: float + + +class AngleModelOutput(ModelOutput): + high: float + eye_level: float + low: float + + +class PointOfViewModelOutput(ModelOutput): + frontal: float + oblique: float + + +class DistanceModelOutput(ModelOutput): + long: float + medium: float + close: float + + +class ModalityLightingModelOutput(ModelOutput): + high: float + medium: float + low: float + + +class ModalityColorModelOutput(ModelOutput): + high: float + medium: float + low: float + + +class ModalityDepthModelOutput(ModelOutput): + high: float + medium: float + low: float + + +# class InterpersonalModelOutput(BaseModel): +# contact: ContactModelOutput +# angle: AngleModelOutput +# point_of_view: PointOfViewModelOutput +# distance: DistanceModelOutput +# modality_lighting: ModalityLightingModelOutput +# modality_color: ModalityColorModelOutput +# modality_depth: ModalityDepthModelOutput + +# @classmethod +# def list_fields(cls) -> List[str]: +# """List options that are stored as attributes.""" +# return list(cls.model_fields.keys()) + +# @classmethod +# def from_random(cls) -> InterpersonalModelOutput: +# """Instantiate with random numbers.""" +# print(cls.model_fields) +# kwargs = {field: info.annotation.from_random() for field, info in cls.model_fields.items()} +# return InterpersonalModelOutput(**kwargs) + +# def __repr__(self) -> str: +# model_dict = self.model_dump() +# model_repr_str = "ExperientialModelOutput(" +# model_repr_str += ", ".join([f"{field}={value:.3f}" for field, value in model_dict.items()]) +# model_repr_str += ")" +# return model_repr_str + +# def highest_score_field(self) -> str: +# """Return name of field with highest score.""" +# model_dict = self.model_dump() +# return max(model_dict, key=lambda k: model_dict[k]) + +# def highest_score_value(self) -> float: +# """Return value of field with highest score.""" +# model_dict = self.model_dump() +# return max(model_dict.values()) + + +if __name__ == '__main__': + m = ContactModelOutput.from_random() + print(repr(m)) + print(m.highest_score_field()) + print(m.highest_score_value()) + m = AngleModelOutput.from_random() + print(repr(m)) + print(m.highest_score_field()) + print(m.highest_score_value()) + m = PointOfViewModelOutput.from_random() + print(repr(m)) + print(m.highest_score_field()) + print(m.highest_score_value()) + m = DistanceModelOutput.from_random() + print(repr(m)) + print(m.highest_score_field()) + print(m.highest_score_value()) + m = ModalityLightingModelOutput.from_random() + print(repr(m)) + print(m.highest_score_field()) + print(m.highest_score_value()) + m = ModalityColorModelOutput.from_random() + print(repr(m)) + print(m.highest_score_field()) + print(m.highest_score_value()) + m = ModalityDepthModelOutput.from_random() + print(repr(m)) + print(m.highest_score_field()) + print(m.highest_score_value()) diff --git a/src/model_interpersonal/output.py b/src/model_interpersonal/output.py index a95e916..14c84a9 100644 --- a/src/model_interpersonal/output.py +++ b/src/model_interpersonal/output.py @@ -1,8 +1,8 @@ model_labels = { "contact": [ - "contact offer", - "contact demand" + "offer", + "demand" ], "angle": [ "high", diff --git a/src/model_textual/__init__.py b/src/model_textual/__init__.py index b0d4f72..9e7b87c 100644 --- a/src/model_textual/__init__.py +++ b/src/model_textual/__init__.py @@ -1 +1,5 @@ -from .output import model_labels \ No newline at end of file +from .classes import ( + InformationValueModelOutput, + FramingModelOutput, + SalienceModelOutput +) \ No newline at end of file diff --git a/src/model_textual/classes.py b/src/model_textual/classes.py new file mode 100644 index 0000000..8b135f5 --- /dev/null +++ b/src/model_textual/classes.py @@ -0,0 +1,75 @@ +from pydantic import BaseModel +from typing import List, Dict +import random + + +class ModelOutput(BaseModel): + + @classmethod + def classname(cls) -> str: + """Return classname.""" + return cls.__name__ + + @classmethod + def list_fields(cls) -> List[str]: + """List options that are stored as attributes.""" + return list(cls.model_fields.keys()) + + @classmethod + def from_random(cls): + """Instantiate with random numbers.""" + kwargs = {field: random.random() for field in cls.list_fields()} + return cls(**kwargs) + + def __repr__(self) -> str: + model_dict = self.model_dump() + model_repr_str = f"{self.classname()}(" + model_repr_str += ", ".join([f"{field}={value:.3f}" for field, value in model_dict.items()]) + model_repr_str += ")" + return model_repr_str + + def highest_score_field(self) -> str: + """Return name of field with highest score.""" + model_dict = self.model_dump() + return max(model_dict, key=lambda k: model_dict[k]) + + def highest_score_value(self) -> float: + """Return value of field with highest score.""" + model_dict = self.model_dump() + return max(model_dict.values()) + + +class InformationValueModelOutput(ModelOutput): + given_new: float + ideal_real: float + central_marginal: float + + +class FramingModelOutput(ModelOutput): + frame_lines: float + empty_space: float + colour_contrast: float + form_contrast: float + + +class SalienceModelOutput(ModelOutput): + size: float + colour: float + tone: float + form: float + positioning: float + + +if __name__ == '__main__': + m = InformationValueModelOutput.from_random() + print(repr(m)) + print(m.highest_score_field()) + print(m.highest_score_value()) + m = FramingModelOutput.from_random() + print(repr(m)) + print(m.highest_score_field()) + print(m.highest_score_value()) + m = SalienceModelOutput.from_random() + print(repr(m)) + print(m.highest_score_field()) + print(m.highest_score_value()) \ No newline at end of file From 7059338ede40ca22ad98407d597d701e485ddbb0 Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Mon, 19 Feb 2024 20:21:18 +0100 Subject: [PATCH 07/21] updated models --- src/model_experiential/classes.py | 67 +++++++++++++++++------------- src/model_interpersonal/classes.py | 31 +------------- src/model_textual/classes.py | 2 +- 3 files changed, 39 insertions(+), 61 deletions(-) diff --git a/src/model_experiential/classes.py b/src/model_experiential/classes.py index 9d9bdcd..29f550e 100644 --- a/src/model_experiential/classes.py +++ b/src/model_experiential/classes.py @@ -1,10 +1,45 @@ -from __future__ import annotations from pydantic import BaseModel from typing import List import random -class ExperientialModelOutput(BaseModel): +class ModelOutput(BaseModel): + + @classmethod + def classname(cls) -> str: + """Return classname.""" + return cls.__name__ + + @classmethod + def list_fields(cls) -> List[str]: + """List options that are stored as attributes.""" + return list(cls.model_fields.keys()) + + @classmethod + def from_random(cls): + """Instantiate with random numbers.""" + kwargs = {field: random.random() for field in cls.list_fields()} + return cls(**kwargs) + + def __repr__(self) -> str: + model_dict = self.model_dump() + model_repr_str = f"{self.classname()}(" + model_repr_str += ", ".join([f"{field}={value:.3f}" for field, value in model_dict.items()]) + model_repr_str += ")" + return model_repr_str + + def highest_score_field(self) -> str: + """Return name of field with highest score.""" + model_dict = self.model_dump() + return max(model_dict, key=lambda k: model_dict[k]) + + def highest_score_value(self) -> float: + """Return value of field with highest score.""" + model_dict = self.model_dump() + return max(model_dict.values()) + + +class ExperientialModelOutput(ModelOutput): non_transactional_action: float non_transactional_reaction: float unidirectional_transactional_action: float @@ -24,34 +59,6 @@ class ExperientialModelOutput(BaseModel): symbolic_suggestive: float symbolic_attributive: float - @classmethod - def list_fields(cls) -> List[str]: - """List options that are stored as attributes.""" - return list(cls.model_fields.keys()) - - @classmethod - def from_random(cls) -> ExperientialModelOutput: - """Instantiate with random numbers.""" - kwargs = {field: random.random() for field in cls.list_fields()} - return ExperientialModelOutput(**kwargs) - - def __repr__(self) -> str: - model_dict = self.model_dump() - model_repr_str = "ExperientialModelOutput(" - model_repr_str += ", ".join([f"{field}={value:.3f}" for field, value in model_dict.items()]) - model_repr_str += ")" - return model_repr_str - - def highest_score_field(self) -> str: - """Return name of field with highest score.""" - model_dict = self.model_dump() - return max(model_dict, key=lambda k: model_dict[k]) - - def highest_score_value(self) -> float: - """Return value of field with highest score.""" - model_dict = self.model_dump() - return max(model_dict.values()) - if __name__ == '__main__': m = ExperientialModelOutput.from_random() diff --git a/src/model_interpersonal/classes.py b/src/model_interpersonal/classes.py index 60c16ad..d57717d 100644 --- a/src/model_interpersonal/classes.py +++ b/src/model_interpersonal/classes.py @@ -1,5 +1,5 @@ from pydantic import BaseModel -from typing import List, Dict +from typing import List import random @@ -88,35 +88,6 @@ class ModalityDepthModelOutput(ModelOutput): # modality_color: ModalityColorModelOutput # modality_depth: ModalityDepthModelOutput -# @classmethod -# def list_fields(cls) -> List[str]: -# """List options that are stored as attributes.""" -# return list(cls.model_fields.keys()) - -# @classmethod -# def from_random(cls) -> InterpersonalModelOutput: -# """Instantiate with random numbers.""" -# print(cls.model_fields) -# kwargs = {field: info.annotation.from_random() for field, info in cls.model_fields.items()} -# return InterpersonalModelOutput(**kwargs) - -# def __repr__(self) -> str: -# model_dict = self.model_dump() -# model_repr_str = "ExperientialModelOutput(" -# model_repr_str += ", ".join([f"{field}={value:.3f}" for field, value in model_dict.items()]) -# model_repr_str += ")" -# return model_repr_str - -# def highest_score_field(self) -> str: -# """Return name of field with highest score.""" -# model_dict = self.model_dump() -# return max(model_dict, key=lambda k: model_dict[k]) - -# def highest_score_value(self) -> float: -# """Return value of field with highest score.""" -# model_dict = self.model_dump() -# return max(model_dict.values()) - if __name__ == '__main__': m = ContactModelOutput.from_random() diff --git a/src/model_textual/classes.py b/src/model_textual/classes.py index 8b135f5..a9bc4d1 100644 --- a/src/model_textual/classes.py +++ b/src/model_textual/classes.py @@ -1,5 +1,5 @@ from pydantic import BaseModel -from typing import List, Dict +from typing import List import random From 6402150301d4f94aaeb920e83bb975abb2544134 Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Mon, 19 Feb 2024 21:00:39 +0100 Subject: [PATCH 08/21] added mongodb and mongo-express --- docker-compose.local.yml | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/docker-compose.local.yml b/docker-compose.local.yml index 7f01e12..baecb8d 100644 --- a/docker-compose.local.yml +++ b/docker-compose.local.yml @@ -1,3 +1,4 @@ +version: '3.7' services: app: image: visual_critical_discourse_analysis:dev @@ -9,7 +10,35 @@ services: - 8050:8050 networks: - backend + depends_on: + - mongo + mongo: + image: mongo:latest + container_name: mongo + environment: + MONGO_INITDB_ROOT: root + MONGO_INITDB_PASSWORD: vSH7I7RxsDvb + ports: + - "27017:27017" + networks: + - backend + mongo-express: + image: mongo-express + ports: + - 8081:8081 + environment: + ME_CONFIG_MONGODB_ADMINUSERNAME: root + ME_CONFIG_MONGODB_ADMINPASSWORD: vSH7I7RxsDvb + ME_CONFIG_MONGODB_PORT: 27017 + ME_CONFIG_BASICAUTH_USERNAME: admin + ME_CONFIG_BASICAUTH_PASSWORD: q + links: + - mongo + networks: + - backend + depends_on: + - mongo networks: backend: - external: false \ No newline at end of file + driver: bridge \ No newline at end of file From 8e23442345aa43821d2a88b9201286feeaf3bdb5 Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Tue, 20 Feb 2024 18:05:39 +0100 Subject: [PATCH 09/21] added category titles --- src/web/body.py | 73 +++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 58 insertions(+), 15 deletions(-) diff --git a/src/web/body.py b/src/web/body.py index b31f69f..1e5e1c9 100644 --- a/src/web/body.py +++ b/src/web/body.py @@ -26,11 +26,16 @@ def generate_option_labels(model) -> List[str]: ] return labels -def generate_options_map(): - """Generate map of titles and options for labels container.""" +def generate_experiential_options_map(): + """Generate map of titles and options for experiential labels.""" options_map = {} # add experiential labels options_map["experiential".title()] = generate_option_labels(ExperientialModelOutput) + return options_map + +def generate_interpersonal_options_map(): + """Generate map of titles and options for interpersonal labels.""" + options_map = {} # add interpersonal labels options_map["contact".title()] = generate_option_labels(ContactModelOutput) options_map["angle".title()] = generate_option_labels(AngleModelOutput) @@ -39,6 +44,11 @@ def generate_options_map(): options_map["modality lighting".title()] = generate_option_labels(ModalityLightingModelOutput) options_map["modality color".title()] = generate_option_labels(ModalityColorModelOutput) options_map["modality depth".title()] = generate_option_labels(ModalityDepthModelOutput) + return options_map + +def generate_textual_options_map(): + """Generate map of titles and options for textual labels.""" + options_map = {} # add textual labels options_map["information value".title()] = generate_option_labels(InformationValueModelOutput) options_map["framing".title()] = generate_option_labels(FramingModelOutput) @@ -52,20 +62,53 @@ def generate_body(): withPlaceholder=True, placeholder=[dmc.Loader(color="gray", size="md")], ) - + # prepare experiential container + experiential_map = generate_experiential_options_map() + experiential_container = dmc.Col( + children=[ + dmc.Container([ + html.H4(list(experiential_map.keys())[0]), + html.B("visual syntax".title()), + dcc.RadioItems(options=list(experiential_map.values())[0]), + ]) + ], span=4 + ) + # prepare interpersonal container + interpersonal_map = generate_interpersonal_options_map() + interpersonal_container = dmc.Col( + children=[ + html.H4("interpersonal".title()), + ], span=4 + ) + for title, options in interpersonal_map.items(): + interpersonal_container.children.append( + dmc.Container([ + html.B(title), + dcc.RadioItems(options) + ]) + ) + # prepare textual container + textual_map = generate_textual_options_map() + textual_container = dmc.Col( + children=[ + html.H4("textual".title()), + ], span=4 + ) + for title, options in textual_map.items(): + textual_container.children.append( + dmc.Container([ + html.B(title), + dcc.RadioItems(options) + ]) + ) # prepare labels container label_container = dmc.Grid( - children=[], + children=[ + experiential_container, + interpersonal_container, + textual_container, + ], ) - # add fields to labels container - for title, options in generate_options_map().items(): - label_container.children.append( - dmc.Col([ - html.H4(title), - dcc.RadioItems(options=options), - ], span=3) - ) - # build the full body container body_container = dmc.Container( dmc.Grid( @@ -74,7 +117,7 @@ def generate_body(): dmc.Center( image_container, ), - span=4, + span=5, ), dmc.Col( # radio buttons part @@ -91,7 +134,7 @@ def generate_body(): "height": "50px" } ), - ], span=8, + ], span=7, ), # dmc.Col(span=1), ], grow=True From f0b3de27ed0725c6e5a8ba7b40236b9807b1c80a Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Tue, 20 Feb 2024 18:18:37 +0100 Subject: [PATCH 10/21] added ignore for *.env --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 33e7851..30ffc03 100644 --- a/.gitignore +++ b/.gitignore @@ -121,7 +121,7 @@ celerybeat.pid *.sage.py # Environments -.env +*.env .venv env/ venv/ From c166420909a7c91bd488d0f877b97b61c6888448 Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Tue, 20 Feb 2024 19:55:53 +0100 Subject: [PATCH 11/21] updated database interface --- src/database/__init__.py | 5 +++ src/database/classes.py | 69 ++++++++++++++++++++++++++++++++++++++++ src/database/database.py | 22 +++++++++++++ 3 files changed, 96 insertions(+) create mode 100644 src/database/__init__.py create mode 100644 src/database/classes.py create mode 100644 src/database/database.py diff --git a/src/database/__init__.py b/src/database/__init__.py new file mode 100644 index 0000000..4083b77 --- /dev/null +++ b/src/database/__init__.py @@ -0,0 +1,5 @@ +from .classes import ( + ModelOutputs, + VisualCommunication +) +from .database import connect \ No newline at end of file diff --git a/src/database/classes.py b/src/database/classes.py new file mode 100644 index 0000000..620142a --- /dev/null +++ b/src/database/classes.py @@ -0,0 +1,69 @@ +from __future__ import annotations +from pydantic import BaseModel, field_validator +from PIL import Image +from io import BytesIO +from pathlib import Path + +from src.model_experiential import ExperientialModelOutput +from src.model_interpersonal import ( + ContactModelOutput, + AngleModelOutput, + PointOfViewModelOutput, + DistanceModelOutput, + ModalityLightingModelOutput, + ModalityColorModelOutput, + ModalityDepthModelOutput +) +from src.model_textual import ( + InformationValueModelOutput, + FramingModelOutput, + SalienceModelOutput +) + +class ModelOutputs(BaseModel): + experiential: ExperientialModelOutput + contact: ContactModelOutput + angle: AngleModelOutput + point_of_view: PointOfViewModelOutput + distance: DistanceModelOutput + modality_lighting: ModalityLightingModelOutput + modality_color: ModalityColorModelOutput + modality_depth: ModalityDepthModelOutput + information_value: InformationValueModelOutput + framing: FramingModelOutput + salience: SalienceModelOutput + + +class VisualCommunication(BaseModel): + name: str + image: bytes + annotation: ModelOutputs | None = None + prediction: ModelOutputs | None = None + + @classmethod + def classname(cls) -> str: + """Return classname.""" + return cls.__name__ + + @classmethod + def from_file(cls, path: Path) -> VisualCommunication: + """Instantiate from file.""" + name = path.stem + image = Image.open(path) + return VisualCommunication(name=name, image=image) + + @field_validator("image", mode="before") + @classmethod + def convert_to_bytes(cls, raw: Image.Image | BytesIO | bytes) -> bytes: + if isinstance(raw, Image.Image): + raw = raw.tobytes() + if isinstance(raw, BytesIO): + raw = raw.read() + return raw + + def __repr__(self) -> str: + return f"{self.classname()}(name='{self.name}')" + + @property + def image(self) -> Image.Image: + return Image.open(BytesIO(self.image)) diff --git a/src/database/database.py b/src/database/database.py new file mode 100644 index 0000000..8f5b429 --- /dev/null +++ b/src/database/database.py @@ -0,0 +1,22 @@ +from pymongo import MongoClient +from dotenv import load_dotenv +import os + +def connect(): + """Connect to MongoDB.""" + # load env vars + load_dotenv() + necessary_env_vars = [ + "MONGO_HOST", + "MONGO_DB", + "MONGO_COLLECTION" + ] + for env_var in necessary_env_vars: + assert env_var in os.environ, f"{env_var} not found" + # connect to database + client = MongoClient(os.getenv("MONGO_HOST")) + db = client[os.getenv("MONGO_DB")] + collection = db[os.getenv("MONGO_COLLECTION")] + return collection, db, client + + From 7068cff2dae2fe886b8ad092da1caa0716a1a4be Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Tue, 20 Feb 2024 19:57:23 +0100 Subject: [PATCH 12/21] updated env vars --- docker-compose.local.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docker-compose.local.yml b/docker-compose.local.yml index baecb8d..f322e48 100644 --- a/docker-compose.local.yml +++ b/docker-compose.local.yml @@ -6,6 +6,10 @@ services: build: context: . dockerfile: Dockerfile + env_file: + - mongodb.env + environment: + - MONGO_HOST=mongo ports: - 8050:8050 networks: @@ -15,9 +19,8 @@ services: mongo: image: mongo:latest container_name: mongo - environment: - MONGO_INITDB_ROOT: root - MONGO_INITDB_PASSWORD: vSH7I7RxsDvb + env_file: + - mongodb.env ports: - "27017:27017" networks: From e5a09a791dc5f53483e17f55826cfdd9f9fc88a3 Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Tue, 20 Feb 2024 19:57:31 +0100 Subject: [PATCH 13/21] installed packages --- poetry.lock | 125 ++++++++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 1 + 2 files changed, 125 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index 5aa1f26..fa70713 100644 --- a/poetry.lock +++ b/poetry.lock @@ -268,6 +268,26 @@ requests = ">=2.28.1,<3.0.0" [package.extras] async = ["httpx (>=0.23.0,<0.24.0)"] +[[package]] +name = "dnspython" +version = "2.6.1" +description = "DNS toolkit" +optional = false +python-versions = ">=3.8" +files = [ + {file = "dnspython-2.6.1-py3-none-any.whl", hash = "sha256:5ef3b9680161f6fa89daf8ad451b5f1a33b18ae8a1c6778cdf4b43f08c0a6e50"}, + {file = "dnspython-2.6.1.tar.gz", hash = "sha256:e8f0f9c23a7b7cb99ded64e6c3a6f3e701d78f50c55e002b839dea7225cff7cc"}, +] + +[package.extras] +dev = ["black (>=23.1.0)", "coverage (>=7.0)", "flake8 (>=7)", "mypy (>=1.8)", "pylint (>=3)", "pytest (>=7.4)", "pytest-cov (>=4.1.0)", "sphinx (>=7.2.0)", "twine (>=4.0.0)", "wheel (>=0.42.0)"] +dnssec = ["cryptography (>=41)"] +doh = ["h2 (>=4.1.0)", "httpcore (>=1.0.0)", "httpx (>=0.26.0)"] +doq = ["aioquic (>=0.9.25)"] +idna = ["idna (>=3.6)"] +trio = ["trio (>=0.23)"] +wmi = ["wmi (>=1.5.1)"] + [[package]] name = "flask" version = "3.0.2" @@ -669,6 +689,109 @@ files = [ [package.dependencies] typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" +[[package]] +name = "pymongo" +version = "4.6.1" +description = "Python driver for MongoDB " +optional = false +python-versions = ">=3.7" +files = [ + {file = "pymongo-4.6.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4344c30025210b9fa80ec257b0e0aab5aa1d5cca91daa70d82ab97b482cc038e"}, + {file = "pymongo-4.6.1-cp310-cp310-manylinux1_i686.whl", hash = "sha256:1c5654bb8bb2bdb10e7a0bc3c193dd8b49a960b9eebc4381ff5a2043f4c3c441"}, + {file = "pymongo-4.6.1-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:eaf2f65190c506def2581219572b9c70b8250615dc918b3b7c218361a51ec42e"}, + {file = "pymongo-4.6.1-cp310-cp310-manylinux2014_i686.whl", hash = "sha256:262356ea5fcb13d35fb2ab6009d3927bafb9504ef02339338634fffd8a9f1ae4"}, + {file = "pymongo-4.6.1-cp310-cp310-manylinux2014_ppc64le.whl", hash = "sha256:2dd2f6960ee3c9360bed7fb3c678be0ca2d00f877068556785ec2eb6b73d2414"}, + {file = "pymongo-4.6.1-cp310-cp310-manylinux2014_s390x.whl", hash = "sha256:ff925f1cca42e933376d09ddc254598f8c5fcd36efc5cac0118bb36c36217c41"}, + {file = "pymongo-4.6.1-cp310-cp310-manylinux2014_x86_64.whl", hash = "sha256:3cadf7f4c8e94d8a77874b54a63c80af01f4d48c4b669c8b6867f86a07ba994f"}, + {file = "pymongo-4.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55dac73316e7e8c2616ba2e6f62b750918e9e0ae0b2053699d66ca27a7790105"}, + {file = "pymongo-4.6.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:154b361dcb358ad377d5d40df41ee35f1cc14c8691b50511547c12404f89b5cb"}, + {file = "pymongo-4.6.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2940aa20e9cc328e8ddeacea8b9a6f5ddafe0b087fedad928912e787c65b4909"}, + {file = "pymongo-4.6.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:010bc9aa90fd06e5cc52c8fac2c2fd4ef1b5f990d9638548dde178005770a5e8"}, + {file = "pymongo-4.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e470fa4bace5f50076c32f4b3cc182b31303b4fefb9b87f990144515d572820b"}, + {file = "pymongo-4.6.1-cp310-cp310-win32.whl", hash = "sha256:da08ea09eefa6b960c2dd9a68ec47949235485c623621eb1d6c02b46765322ac"}, + {file = "pymongo-4.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:13d613c866f9f07d51180f9a7da54ef491d130f169e999c27e7633abe8619ec9"}, + {file = "pymongo-4.6.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6a0ae7a48a6ef82ceb98a366948874834b86c84e288dbd55600c1abfc3ac1d88"}, + {file = "pymongo-4.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bd94c503271e79917b27c6e77f7c5474da6930b3fb9e70a12e68c2dff386b9a"}, + {file = "pymongo-4.6.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2d4ccac3053b84a09251da8f5350bb684cbbf8c8c01eda6b5418417d0a8ab198"}, + {file = "pymongo-4.6.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:349093675a2d3759e4fb42b596afffa2b2518c890492563d7905fac503b20daa"}, + {file = "pymongo-4.6.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88beb444fb438385e53dc9110852910ec2a22f0eab7dd489e827038fdc19ed8d"}, + {file = "pymongo-4.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d8e62d06e90f60ea2a3d463ae51401475568b995bafaffd81767d208d84d7bb1"}, + {file = "pymongo-4.6.1-cp311-cp311-win32.whl", hash = "sha256:5556e306713e2522e460287615d26c0af0fe5ed9d4f431dad35c6624c5d277e9"}, + {file = "pymongo-4.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:b10d8cda9fc2fcdcfa4a000aa10413a2bf8b575852cd07cb8a595ed09689ca98"}, + {file = "pymongo-4.6.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b435b13bb8e36be11b75f7384a34eefe487fe87a6267172964628e2b14ecf0a7"}, + {file = "pymongo-4.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e438417ce1dc5b758742e12661d800482200b042d03512a8f31f6aaa9137ad40"}, + {file = "pymongo-4.6.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8b47ebd89e69fbf33d1c2df79759d7162fc80c7652dacfec136dae1c9b3afac7"}, + {file = "pymongo-4.6.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bbed8cccebe1169d45cedf00461b2842652d476d2897fd1c42cf41b635d88746"}, + {file = "pymongo-4.6.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c30a9e06041fbd7a7590693ec5e407aa8737ad91912a1e70176aff92e5c99d20"}, + {file = "pymongo-4.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8729dbf25eb32ad0dc0b9bd5e6a0d0b7e5c2dc8ec06ad171088e1896b522a74"}, + {file = "pymongo-4.6.1-cp312-cp312-win32.whl", hash = "sha256:3177f783ae7e08aaf7b2802e0df4e4b13903520e8380915e6337cdc7a6ff01d8"}, + {file = "pymongo-4.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:00c199e1c593e2c8b033136d7a08f0c376452bac8a896c923fcd6f419e07bdd2"}, + {file = "pymongo-4.6.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6dcc95f4bb9ed793714b43f4f23a7b0c57e4ef47414162297d6f650213512c19"}, + {file = "pymongo-4.6.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:13552ca505366df74e3e2f0a4f27c363928f3dff0eef9f281eb81af7f29bc3c5"}, + {file = "pymongo-4.6.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:77e0df59b1a4994ad30c6d746992ae887f9756a43fc25dec2db515d94cf0222d"}, + {file = "pymongo-4.6.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:3a7f02a58a0c2912734105e05dedbee4f7507e6f1bd132ebad520be0b11d46fd"}, + {file = "pymongo-4.6.1-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:026a24a36394dc8930cbcb1d19d5eb35205ef3c838a7e619e04bd170713972e7"}, + {file = "pymongo-4.6.1-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:3b287e814a01deddb59b88549c1e0c87cefacd798d4afc0c8bd6042d1c3d48aa"}, + {file = "pymongo-4.6.1-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:9a710c184ba845afb05a6f876edac8f27783ba70e52d5eaf939f121fc13b2f59"}, + {file = "pymongo-4.6.1-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:30b2c9caf3e55c2e323565d1f3b7e7881ab87db16997dc0cbca7c52885ed2347"}, + {file = "pymongo-4.6.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff62ba8ff70f01ab4fe0ae36b2cb0b5d1f42e73dfc81ddf0758cd9f77331ad25"}, + {file = "pymongo-4.6.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:547dc5d7f834b1deefda51aedb11a7af9c51c45e689e44e14aa85d44147c7657"}, + {file = "pymongo-4.6.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1de3c6faf948f3edd4e738abdb4b76572b4f4fdfc1fed4dad02427e70c5a6219"}, + {file = "pymongo-4.6.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2831e05ce0a4df10c4ac5399ef50b9a621f90894c2a4d2945dc5658765514ed"}, + {file = "pymongo-4.6.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:144a31391a39a390efce0c5ebcaf4bf112114af4384c90163f402cec5ede476b"}, + {file = "pymongo-4.6.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:33bb16a07d3cc4e0aea37b242097cd5f7a156312012455c2fa8ca396953b11c4"}, + {file = "pymongo-4.6.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b7b1a83ce514700276a46af3d9e481ec381f05b64939effc9065afe18456a6b9"}, + {file = "pymongo-4.6.1-cp37-cp37m-win32.whl", hash = "sha256:3071ec998cc3d7b4944377e5f1217c2c44b811fae16f9a495c7a1ce9b42fb038"}, + {file = "pymongo-4.6.1-cp37-cp37m-win_amd64.whl", hash = "sha256:2346450a075625c4d6166b40a013b605a38b6b6168ce2232b192a37fb200d588"}, + {file = "pymongo-4.6.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:061598cbc6abe2f382ab64c9caa83faa2f4c51256f732cdd890bcc6e63bfb67e"}, + {file = "pymongo-4.6.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:d483793a384c550c2d12cb794ede294d303b42beff75f3b3081f57196660edaf"}, + {file = "pymongo-4.6.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:f9756f1d25454ba6a3c2f1ef8b7ddec23e5cdeae3dc3c3377243ae37a383db00"}, + {file = "pymongo-4.6.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:1ed23b0e2dac6f84f44c8494fbceefe6eb5c35db5c1099f56ab78fc0d94ab3af"}, + {file = "pymongo-4.6.1-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:3d18a9b9b858ee140c15c5bfcb3e66e47e2a70a03272c2e72adda2482f76a6ad"}, + {file = "pymongo-4.6.1-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:c258dbacfff1224f13576147df16ce3c02024a0d792fd0323ac01bed5d3c545d"}, + {file = "pymongo-4.6.1-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:f7acc03a4f1154ba2643edeb13658d08598fe6e490c3dd96a241b94f09801626"}, + {file = "pymongo-4.6.1-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:76013fef1c9cd1cd00d55efde516c154aa169f2bf059b197c263a255ba8a9ddf"}, + {file = "pymongo-4.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f0e6a6c807fa887a0c51cc24fe7ea51bb9e496fe88f00d7930063372c3664c3"}, + {file = "pymongo-4.6.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd1fa413f8b9ba30140de198e4f408ffbba6396864c7554e0867aa7363eb58b2"}, + {file = "pymongo-4.6.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d219b4508f71d762368caec1fc180960569766049bbc4d38174f05e8ef2fe5b"}, + {file = "pymongo-4.6.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27b81ecf18031998ad7db53b960d1347f8f29e8b7cb5ea7b4394726468e4295e"}, + {file = "pymongo-4.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:56816e43c92c2fa8c11dc2a686f0ca248bea7902f4a067fa6cbc77853b0f041e"}, + {file = "pymongo-4.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ef801027629c5b511cf2ba13b9be29bfee36ae834b2d95d9877818479cdc99ea"}, + {file = "pymongo-4.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d4c2be9760b112b1caf649b4977b81b69893d75aa86caf4f0f398447be871f3c"}, + {file = "pymongo-4.6.1-cp38-cp38-win32.whl", hash = "sha256:39d77d8bbb392fa443831e6d4ae534237b1f4eee6aa186f0cdb4e334ba89536e"}, + {file = "pymongo-4.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:4497d49d785482cc1a44a0ddf8830b036a468c088e72a05217f5b60a9e025012"}, + {file = "pymongo-4.6.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:69247f7a2835fc0984bbf0892e6022e9a36aec70e187fcfe6cae6a373eb8c4de"}, + {file = "pymongo-4.6.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:7bb0e9049e81def6829d09558ad12d16d0454c26cabe6efc3658e544460688d9"}, + {file = "pymongo-4.6.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:6a1810c2cbde714decf40f811d1edc0dae45506eb37298fd9d4247b8801509fe"}, + {file = "pymongo-4.6.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:e2aced6fb2f5261b47d267cb40060b73b6527e64afe54f6497844c9affed5fd0"}, + {file = "pymongo-4.6.1-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:d0355cff58a4ed6d5e5f6b9c3693f52de0784aa0c17119394e2a8e376ce489d4"}, + {file = "pymongo-4.6.1-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:3c74f4725485f0a7a3862cfd374cc1b740cebe4c133e0c1425984bcdcce0f4bb"}, + {file = "pymongo-4.6.1-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:9c79d597fb3a7c93d7c26924db7497eba06d58f88f58e586aa69b2ad89fee0f8"}, + {file = "pymongo-4.6.1-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:8ec75f35f62571a43e31e7bd11749d974c1b5cd5ea4a8388725d579263c0fdf6"}, + {file = "pymongo-4.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5e641f931c5cd95b376fd3c59db52770e17bec2bf86ef16cc83b3906c054845"}, + {file = "pymongo-4.6.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9aafd036f6f2e5ad109aec92f8dbfcbe76cff16bad683eb6dd18013739c0b3ae"}, + {file = "pymongo-4.6.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f2b856518bfcfa316c8dae3d7b412aecacf2e8ba30b149f5eb3b63128d703b9"}, + {file = "pymongo-4.6.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ec31adc2e988fd7db3ab509954791bbc5a452a03c85e45b804b4bfc31fa221d"}, + {file = "pymongo-4.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9167e735379ec43d8eafa3fd675bfbb12e2c0464f98960586e9447d2cf2c7a83"}, + {file = "pymongo-4.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1461199b07903fc1424709efafe379205bf5f738144b1a50a08b0396357b5abf"}, + {file = "pymongo-4.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:3094c7d2f820eecabadae76bfec02669567bbdd1730eabce10a5764778564f7b"}, + {file = "pymongo-4.6.1-cp39-cp39-win32.whl", hash = "sha256:c91ea3915425bd4111cb1b74511cdc56d1d16a683a48bf2a5a96b6a6c0f297f7"}, + {file = "pymongo-4.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:ef102a67ede70e1721fe27f75073b5314911dbb9bc27cde0a1c402a11531e7bd"}, + {file = "pymongo-4.6.1.tar.gz", hash = "sha256:31dab1f3e1d0cdd57e8df01b645f52d43cc1b653ed3afd535d2891f4fc4f9712"}, +] + +[package.dependencies] +dnspython = ">=1.16.0,<3.0.0" + +[package.extras] +aws = ["pymongo-auth-aws (<2.0.0)"] +encryption = ["certifi", "pymongo[aws]", "pymongocrypt (>=1.6.0,<2.0.0)"] +gssapi = ["pykerberos", "winkerberos (>=0.5.0)"] +ocsp = ["certifi", "cryptography (>=2.5)", "pyopenssl (>=17.2.0)", "requests (<3.0.0)", "service-identity (>=18.1.0)"] +snappy = ["python-snappy"] +test = ["pytest (>=7)"] +zstd = ["zstandard"] + [[package]] name = "python-dotenv" version = "1.0.1" @@ -839,4 +962,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "e4892a5e8db437b5c79b40f259ba6b512d1c0b18677b4f13163fee518276a28d" +content-hash = "e4aacea5a98281d935411e0d96152d1d24680f6c1e5288e9a1be913a0536b78e" diff --git a/pyproject.toml b/pyproject.toml index ce99a52..f71a170 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,7 @@ dash-bootstrap-components = "^1.5.0" dash-mantine-components = "^0.12.1" pydantic = "^2.6.1" pillow = "^10.2.0" +pymongo = "^4.6.1" [build-system] From d3ecc50075aef3e48f06018581da040089e84c6a Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Tue, 20 Feb 2024 19:58:03 +0100 Subject: [PATCH 14/21] added image upload to database test --- tests/imgs/18-12-14-ESP-01a.jpeg | Bin 0 -> 26799 bytes tests/imgs/18-12-14-ESP-01b.jpeg | Bin 0 -> 30012 bytes tests/imgs/18-12-14-ESP-01c.jpeg | Bin 0 -> 38186 bytes tests/test_image_upload.py | 26 ++++++++++++++++++++++++++ 4 files changed, 26 insertions(+) create mode 100644 tests/imgs/18-12-14-ESP-01a.jpeg create mode 100644 tests/imgs/18-12-14-ESP-01b.jpeg create mode 100644 tests/imgs/18-12-14-ESP-01c.jpeg create mode 100644 tests/test_image_upload.py diff --git a/tests/imgs/18-12-14-ESP-01a.jpeg b/tests/imgs/18-12-14-ESP-01a.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..ebaa5ee8a0058640371c7c91ad5a12a6bae66908 GIT binary patch literal 26799 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 O>kd~c~~hJeBoPj2Ai}_*4n34 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 Ck+Tc{ literal 0 HcmV?d00001 diff --git a/tests/imgs/18-12-14-ESP-01b.jpeg b/tests/imgs/18-12-14-ESP-01b.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..060f79a79a4367478e5e70cd6a4368df32588d53 GIT binary patch literal 30012 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 Ldv$ufK#%{~@Umlm literal 0 HcmV?d00001 diff --git a/tests/imgs/18-12-14-ESP-01c.jpeg b/tests/imgs/18-12-14-ESP-01c.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..a2d79a71cade6d98f8e3d166ece45cbae077a689 GIT binary patch literal 38186 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 NKiw5DTnn3L|JhLB)=B^X literal 0 HcmV?d00001 diff --git a/tests/test_image_upload.py b/tests/test_image_upload.py new file mode 100644 index 0000000..f63b901 --- /dev/null +++ b/tests/test_image_upload.py @@ -0,0 +1,26 @@ +from pathlib import Path +from dotenv import load_dotenv +from pymongo import MongoClient +import os + +from src.database import VisualCommunication, connect + +if __name__ == "__main__": + # get list of image paths + test_dir = Path(__file__).parent + img_dir = test_dir / "imgs" + img_path_list = [path for path in img_dir.glob("*.jpeg") if path.is_file()] + print(img_path_list) + # instantiate data object + vis_com_list = [VisualCommunication.from_file(path) for path in img_path_list] + for vis_com in vis_com_list: + print(repr(vis_com)) + # upload images to database + env_path = test_dir.parent / "mongodb.env" + assert env_path.exists() + load_dotenv(env_path) + collection, db, client = connect() + print(client.server_info()) + for vis_com in vis_com_list: + result = collection.insert_one(vis_com.model_dump_json()) + print(f"inserted document: {result}") \ No newline at end of file From 82f9bf2a1d7a9e0b5e12a48eb10773070bfae8d9 Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Wed, 21 Feb 2024 18:27:04 +0100 Subject: [PATCH 15/21] updated to use local.env file --- docker-compose.local.yml | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/docker-compose.local.yml b/docker-compose.local.yml index f322e48..247052c 100644 --- a/docker-compose.local.yml +++ b/docker-compose.local.yml @@ -7,9 +7,7 @@ services: context: . dockerfile: Dockerfile env_file: - - mongodb.env - environment: - - MONGO_HOST=mongo + - local.env ports: - 8050:8050 networks: @@ -20,7 +18,7 @@ services: image: mongo:latest container_name: mongo env_file: - - mongodb.env + - local.env ports: - "27017:27017" networks: @@ -29,12 +27,8 @@ services: image: mongo-express ports: - 8081:8081 - environment: - ME_CONFIG_MONGODB_ADMINUSERNAME: root - ME_CONFIG_MONGODB_ADMINPASSWORD: vSH7I7RxsDvb - ME_CONFIG_MONGODB_PORT: 27017 - ME_CONFIG_BASICAUTH_USERNAME: admin - ME_CONFIG_BASICAUTH_PASSWORD: q + env_file: + - local.env links: - mongo networks: From ca7e0b5be7e917492c62b73292ac8424fe375dbf Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Wed, 21 Feb 2024 18:27:22 +0100 Subject: [PATCH 16/21] added definition of server access test --- docker-compose.server.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 docker-compose.server.yml diff --git a/docker-compose.server.yml b/docker-compose.server.yml new file mode 100644 index 0000000..44feb18 --- /dev/null +++ b/docker-compose.server.yml @@ -0,0 +1,20 @@ +version: '3.7' +services: + app: + image: visual_critical_discourse_analysis:dev + container_name: visual_critical_discourse_analysis + build: + context: . + dockerfile: Dockerfile + env_file: + - server.env + ports: + - 8050:8050 + networks: + - backend + depends_on: + - mongo + +networks: + backend: + driver: bridge \ No newline at end of file From 68916197b55986de307567022a5b41d9de191245 Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Wed, 21 Feb 2024 18:29:05 +0100 Subject: [PATCH 17/21] removed ununsed dependency statement --- docker-compose.server.yml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/docker-compose.server.yml b/docker-compose.server.yml index 44feb18..73a4d10 100644 --- a/docker-compose.server.yml +++ b/docker-compose.server.yml @@ -10,11 +10,3 @@ services: - server.env ports: - 8050:8050 - networks: - - backend - depends_on: - - mongo - -networks: - backend: - driver: bridge \ No newline at end of file From a3b95f88bab6b2278d80984e354031793ce01820 Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Wed, 21 Feb 2024 20:04:48 +0100 Subject: [PATCH 18/21] added unique indexing to name-attribute --- src/database/database.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/database/database.py b/src/database/database.py index 8f5b429..692eb70 100644 --- a/src/database/database.py +++ b/src/database/database.py @@ -17,6 +17,5 @@ def connect(): client = MongoClient(os.getenv("MONGO_HOST")) db = client[os.getenv("MONGO_DB")] collection = db[os.getenv("MONGO_COLLECTION")] + collection.create_index("name", unique=True) return collection, db, client - - From 68417a7afc7e50edb606216460cc09f24e620eb8 Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Wed, 21 Feb 2024 20:05:27 +0100 Subject: [PATCH 19/21] defined serialization of images --- src/database/classes.py | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/src/database/classes.py b/src/database/classes.py index 620142a..39aad3a 100644 --- a/src/database/classes.py +++ b/src/database/classes.py @@ -1,5 +1,5 @@ from __future__ import annotations -from pydantic import BaseModel, field_validator +from pydantic import BaseModel, field_validator, field_serializer from PIL import Image from io import BytesIO from pathlib import Path @@ -36,34 +36,40 @@ class ModelOutputs(BaseModel): class VisualCommunication(BaseModel): name: str - image: bytes + image: Image.Image | BytesIO | bytes annotation: ModelOutputs | None = None prediction: ModelOutputs | None = None - + + class Config: + arbitrary_types_allowed = True + @classmethod def classname(cls) -> str: """Return classname.""" return cls.__name__ - + @classmethod def from_file(cls, path: Path) -> VisualCommunication: """Instantiate from file.""" name = path.stem image = Image.open(path) + image.load() return VisualCommunication(name=name, image=image) - + + @field_serializer("image") + def serialize_image(image: Image.Image) -> bytes: + buffer = BytesIO() + image.save(buffer, format="JPEG") + return buffer.getvalue() + @field_validator("image", mode="before") @classmethod - def convert_to_bytes(cls, raw: Image.Image | BytesIO | bytes) -> bytes: - if isinstance(raw, Image.Image): - raw = raw.tobytes() - if isinstance(raw, BytesIO): - raw = raw.read() - return raw - + def convert_to_image(cls, image: Image.Image | BytesIO | bytes) -> Image.Image: + if isinstance(image, bytes): + image = BytesIO(image) + if isinstance(image, BytesIO): + image = Image.open(image) + return image + def __repr__(self) -> str: return f"{self.classname()}(name='{self.name}')" - - @property - def image(self) -> Image.Image: - return Image.open(BytesIO(self.image)) From 3b0fbaa85198a95aee88085ba4593f0ff4f771e6 Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Wed, 21 Feb 2024 20:05:56 +0100 Subject: [PATCH 20/21] updated and added tests --- tests/test_image_download.py | 27 +++++++++++++++++++++++++++ tests/test_image_upload.py | 17 ++++++++++++----- 2 files changed, 39 insertions(+), 5 deletions(-) create mode 100644 tests/test_image_download.py diff --git a/tests/test_image_download.py b/tests/test_image_download.py new file mode 100644 index 0000000..5782c01 --- /dev/null +++ b/tests/test_image_download.py @@ -0,0 +1,27 @@ +from pathlib import Path +from dotenv import load_dotenv +from pymongo import MongoClient +from typing import List +import os + +from src.database import VisualCommunication, connect + +if __name__ == "__main__": + # prepare env vars + env_path = Path(__file__).parent.parent / "local.env" + assert env_path.exists() + load_dotenv(env_path) + os.environ["MONGO_HOST"] = "localhost" + # connect to database + collection, db, client = connect() + print(client.server_info()) + # download images + data = None + for data in collection.find().limit(3): + if data is None: + print("no document found") + break + vis_com: VisualCommunication = VisualCommunication.model_validate(data) + print(repr(vis_com)) + if data is not None: + print(vis_com.image) \ No newline at end of file diff --git a/tests/test_image_upload.py b/tests/test_image_upload.py index f63b901..f4231f9 100644 --- a/tests/test_image_upload.py +++ b/tests/test_image_upload.py @@ -1,6 +1,6 @@ from pathlib import Path from dotenv import load_dotenv -from pymongo import MongoClient +from pymongo.errors import DuplicateKeyError import os from src.database import VisualCommunication, connect @@ -15,12 +15,19 @@ if __name__ == "__main__": vis_com_list = [VisualCommunication.from_file(path) for path in img_path_list] for vis_com in vis_com_list: print(repr(vis_com)) - # upload images to database - env_path = test_dir.parent / "mongodb.env" + # prepare env vars + env_path = test_dir.parent / "local.env" assert env_path.exists() load_dotenv(env_path) + os.environ["MONGO_HOST"] = "localhost" + # connect to database collection, db, client = connect() print(client.server_info()) + # upload images for vis_com in vis_com_list: - result = collection.insert_one(vis_com.model_dump_json()) - print(f"inserted document: {result}") \ No newline at end of file + try: + result = collection.insert_one(vis_com.model_dump()) + except DuplicateKeyError as exc: + print("ignoring:\n", exc) + else: + print(f"inserted document: {result}") From ff261987212526e4d7563f6104f2305674627963 Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Wed, 21 Feb 2024 20:06:04 +0100 Subject: [PATCH 21/21] updated and added tests --- tests/test_image_download.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_image_download.py b/tests/test_image_download.py index 5782c01..2e2b860 100644 --- a/tests/test_image_download.py +++ b/tests/test_image_download.py @@ -24,4 +24,4 @@ if __name__ == "__main__": vis_com: VisualCommunication = VisualCommunication.model_validate(data) print(repr(vis_com)) if data is not None: - print(vis_com.image) \ No newline at end of file + print(vis_com.image)