From bc653a0be4d9b4214e69b97da35aca555b6a049e Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Tue, 7 May 2024 20:19:18 +0200 Subject: [PATCH 01/17] added new folder and ran tests --- docker-compose.local.yml | 33 ++-- docker-compose.server.yml | 3 +- web_ui/Dockerfile | 57 +++++++ web_ui/src/app/__init__.py | 3 + web_ui/src/app/app.py | 251 +++++++++++++++++++++++++++++ web_ui/src/app/layout/__init__.py | 3 + web_ui/src/app/layout/alerts.py | 23 +++ web_ui/src/app/layout/body.py | 20 +++ web_ui/src/app/layout/header.py | 62 +++++++ web_ui/src/app/layout/image.py | 24 +++ web_ui/src/app/layout/init_img.png | Bin 0 -> 9479 bytes web_ui/src/app/layout/inputs.py | 25 +++ web_ui/src/app/layout/labels.py | 113 +++++++++++++ web_ui/src/app/layout/layout.py | 36 +++++ web_ui/src/app/layout/stores.py | 13 ++ web_ui/src/main.py | 53 ++++++ 16 files changed, 700 insertions(+), 19 deletions(-) create mode 100644 web_ui/Dockerfile create mode 100644 web_ui/src/app/__init__.py create mode 100644 web_ui/src/app/app.py create mode 100644 web_ui/src/app/layout/__init__.py create mode 100644 web_ui/src/app/layout/alerts.py create mode 100644 web_ui/src/app/layout/body.py create mode 100644 web_ui/src/app/layout/header.py create mode 100644 web_ui/src/app/layout/image.py create mode 100644 web_ui/src/app/layout/init_img.png create mode 100644 web_ui/src/app/layout/inputs.py create mode 100644 web_ui/src/app/layout/labels.py create mode 100644 web_ui/src/app/layout/layout.py create mode 100644 web_ui/src/app/layout/stores.py create mode 100644 web_ui/src/main.py diff --git a/docker-compose.local.yml b/docker-compose.local.yml index 011ee49..a190a93 100644 --- a/docker-compose.local.yml +++ b/docker-compose.local.yml @@ -1,21 +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: - # - local.env - # environment: - # - ENV=DEV - # ports: - # - 8050:8050 - # networks: - # - backend - # depends_on: - # - mongo + app: + image: visual_critical_discourse_analysis:dev + container_name: visual_critical_discourse_analysis + build: + context: . + dockerfile: ./web_ui/Dockerfile + env_file: + - local.env + environment: + - ENV=DEV + ports: + - 8050:8050 + networks: + - backend + depends_on: + - mongo mongo: image: mongo:latest container_name: mongo diff --git a/docker-compose.server.yml b/docker-compose.server.yml index 73a4d10..2e8007f 100644 --- a/docker-compose.server.yml +++ b/docker-compose.server.yml @@ -1,11 +1,10 @@ -version: '3.7' services: app: image: visual_critical_discourse_analysis:dev container_name: visual_critical_discourse_analysis build: context: . - dockerfile: Dockerfile + dockerfile: ./web_ui/Dockerfile env_file: - server.env ports: diff --git a/web_ui/Dockerfile b/web_ui/Dockerfile new file mode 100644 index 0000000..59b4f9e --- /dev/null +++ b/web_ui/Dockerfile @@ -0,0 +1,57 @@ +# build stage +FROM python:3.12-slim-bookworm as BUILDER + +# set environment variables +ENV PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + PIP_NO_CACHE_DIR=off \ + PIP_DISABLE_PIP_VERSION_CHECK=ON \ + PIP_DEFAULT_TIMEOUT=100 \ + DEBIAN_FRONTEND=noninteractive \ + POETRY_HOME=/etc/poetry \ + POETRY_VERSION=1.7.1 \ + POETRY_VIRTUALENVS_IN_PROJECT=1 \ + POETRY_VIRTUALENVS_CREATE=1 \ + POETRY_NO_INTERACTION=1 \ + POETRY_CACHE_DIR=/tmp/poetry_cache \ + APP_HOME=/home/app + +# update system +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + build-essential \ + curl \ + && apt-get clean + +# install poetry +RUN curl -sSL https://install.python-poetry.org | python3 - +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 --without model + +# final stage +FROM python:3.12-slim-bookworm + +# copy virtualenv made by poetry +ENV APP_HOME=/home/app \ + VIRTUAL_ENV=/home/app/.venv +COPY --from=builder ${VIRTUAL_ENV} ${VIRTUAL_ENV} +ENV PATH="${VIRTUAL_ENV}/bin:${PATH}" + +# create home directory and app user +RUN mkdir -p /home/app && \ + addgroup --system app && \ + adduser --system --group app + +# add code while changing ownership +WORKDIR $APP_HOME +COPY --chown=app:app ./core ./core +COPY --chown=app:app ./src ./src + +# change to the app user +USER app + +ENTRYPOINT [ "gunicorn", "src.main:server", "-b", "0.0.0.0:8050" ] diff --git a/web_ui/src/app/__init__.py b/web_ui/src/app/__init__.py new file mode 100644 index 0000000..238bf79 --- /dev/null +++ b/web_ui/src/app/__init__.py @@ -0,0 +1,3 @@ +from __future__ import annotations + +from .app import app diff --git a/web_ui/src/app/app.py b/web_ui/src/app/app.py new file mode 100644 index 0000000..c1d5083 --- /dev/null +++ b/web_ui/src/app/app.py @@ -0,0 +1,251 @@ +from __future__ import annotations + +import logging +import os + +import dash_bootstrap_components as dbc +from dash import ALL +from dash import Dash +from dash import Input +from dash import Output +from dash import State +from dash_auth import BasicAuth +from pydantic import ValidationError + +from .layout import app_layout +from core.database import connect +from core.database import count_documents +from core.database import get_visual_communication +from core.database import NoDocumentFoundException +from core.database import upsert_annotation +from core.database import upsert_visual_communication +from core.database import VisualCommunication +from core.dto import ModelData + +# setup app +app = Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP]) +app.title = 'visual critical discourse analysis'.title() +app.layout = app_layout +server = app.server + +# setup authentication +AUTH_DICT = { + os.getenv('DASH_AUTH_USERNAME'): os.getenv('DASH_AUTH_PASSWORD'), +} +BasicAuth(app, AUTH_DICT) + +# connect to database +collection, db, client = connect() + + +# define callbacks +@app.callback( + Output('alert-element', 'is_open'), + Output('alert-element', 'children'), + Input('alert-message', 'data'), +) +def show_alert( + msg: str | None, +): + if msg is None or msg == '': + return False, '' + logging.info(f'updated alert message: {msg}') + return True, msg + + +@app.callback( + Output('success-element', 'is_open'), + Output('success-element', 'children'), + Input('success-message', 'data'), +) +def show_success( + msg: str | None, +): + if msg is None or msg == '': + return False, '' + logging.info(f"updated success message: {msg}") + return True, msg + + +@app.callback( + Output('progress-bar', 'value'), + Output('progress-bar', 'max'), + Output('progress-bar', 'label'), + Input('next-button', 'n_clicks'), + Input('upload-data', 'filename'), +) +def update_progress_bar( + n_clicks: int, + filename_list: list[str] | None, +): + logging.info('began updating progress bar') + global collection + # get values from database + num_total = count_documents( + collection=collection, + only_with_annotation=False, + ) + num_handled = count_documents( + collection=collection, + only_with_annotation=True, + ) + limit = int(num_total/20) + label_str = f"{num_handled}/{num_total}" if num_handled >= limit else '' + return num_handled, num_total, label_str + + +@app.callback( + Output('success-message', 'data', allow_duplicate=True), + Output('alert-message', 'data', allow_duplicate=True), + Input('upload-data', 'contents'), + State('upload-data', 'filename'), + prevent_initial_call=True, +) +def upload_images( + content_list: list[str] | None, + filename_list: list[str] | None, +): + logging.info('began uploading images') + global collection + # stop early if possible + if content_list is None or filename_list is None: + logging.warning('content_list is None -> nothing to upload') + return None, 'nothing selected to upload'.title() + # build list of visual communication + visual_communication_list = [] + for content, filename in zip(content_list, filename_list): + try: + image = VisualCommunication.decode_image(content) + vis_com = VisualCommunication( + name=filename, + image=image, + ) + except Exception as exc: + logging.warning( + ( + 'failed creating VisualCommunication object ' + f"from file {filename}" + ), + exc, + ) + else: + visual_communication_list.append(vis_com) + # upsert documents + success = upsert_visual_communication( + collection=collection, + visual_communication_list=visual_communication_list, + ) + if success: + logging.info(f"succesfully uploaded {len(filename_list)} images") + return 'successfully uploaded images'.title(), None + logging.warning('failed inserting images into database') + return None, 'failed uploading images'.title() + + +@app.callback( + Output('alert-message', 'data', allow_duplicate=True), + Output('vis-com-name', 'data'), + Output('image-container', 'src'), + Output({'type': 'annotation', 'index': ALL}, 'value'), + Input('next-button', 'n_clicks'), + State('vis-com-name', 'data'), + State('image-container', 'src'), + State({'type': 'annotation', 'index': ALL}, 'id'), + State({'type': 'annotation', 'index': ALL}, 'value'), + prevent_initial_call=True, +) +def cycle_visual_communication_data( + n_clicks: int, + vis_com_name: str, + image_src: str, + annotation_keys: list, + annotation_values: list, +): + logging.info('began cycling visual communication data') + global collection + # prepare default response + response = [ + '', + vis_com_name, + image_src, + annotation_values, + ] + # check if next-button clicked + if n_clicks == 0: + logging.info('stopping early: next-button has not yet been clicked') + return response + # check if visual communication name is set + if len(vis_com_name) > 0: + logging.info('saving annotations to database: %s', vis_com_name) + try: + # extract option keys + annotation_keys = [ + elem['index'] + for elem in annotation_keys + ] + # ensure all options are set + logging.info(annotation_keys) + for option, value in zip(annotation_keys, annotation_values): + if value is None: + raise ValueError(f"{option} is not set") + # prepare data to save + annotation_keys = [ + elem.replace(' ', '_') + for elem + in annotation_keys + ] + annotation_values = [ + elem.replace(' ', '_').lower() + for elem + in annotation_values + ] + annotation_map = { + key: value + for key, value + in zip(annotation_keys, annotation_values) + } + # instantiate ModelOutputs object + annotations = ModelData.from_annotations(**annotation_map) + # save data to database + upsert_annotation( + collection=collection, + vis_com_name=vis_com_name, + annotations=annotations, + ) + except (ValueError, ValidationError) as exc: + msg = f"failed saving annotation: {exc}" + logging.warning(msg) + response[0] = msg + return tuple(response) + # get new visual communication + logging.info('trying to get new visual communication') + try: + # get data + vis_com = get_visual_communication( + collection=collection, + with_annotation=False, + ) + # set variables + vis_com_name = vis_com.name + image_src = vis_com.webencoded_image() + if vis_com.prediction is not None: + # TODO: update to use optional predictions + pass + else: + # reset annotations + annotation_values = [None for elem in annotation_values] + except NoDocumentFoundException: + msg = 'no unannotated data in database' + logging.warning(msg) + response[0] = msg + return tuple(response) + else: + response[1] = vis_com_name + response[2] = image_src + response[3] = annotation_values + logging.info('finished getting visual communication: %s', vis_com_name) + return tuple(response) + + +if __name__ == '__main__': + app.run(debug=True) diff --git a/web_ui/src/app/layout/__init__.py b/web_ui/src/app/layout/__init__.py new file mode 100644 index 0000000..a74fbeb --- /dev/null +++ b/web_ui/src/app/layout/__init__.py @@ -0,0 +1,3 @@ +from __future__ import annotations + +from .layout import app_layout diff --git a/web_ui/src/app/layout/alerts.py b/web_ui/src/app/layout/alerts.py new file mode 100644 index 0000000..8be4a69 --- /dev/null +++ b/web_ui/src/app/layout/alerts.py @@ -0,0 +1,23 @@ +from __future__ import annotations + +import dash_bootstrap_components as dbc +from dash import html + + +alerts_element = html.Div( + children=[ + dbc.Alert( + children='', + id='alert-element', + dismissable=True, + fade=False, + is_open=False, + ), + dbc.Alert( + children='', + id='success-element', + is_open=False, + duration=1000, + ), + ], +) diff --git a/web_ui/src/app/layout/body.py b/web_ui/src/app/layout/body.py new file mode 100644 index 0000000..374483b --- /dev/null +++ b/web_ui/src/app/layout/body.py @@ -0,0 +1,20 @@ +from __future__ import annotations + +import dash_mantine_components as dmc + +from .image import image_element +from .inputs import inputs_element + + +body_element = dmc.Container( + fluid=True, + children=[ + dmc.Grid( + grow=True, + children=[ + dmc.Col([image_element], span=5), + dmc.Col([inputs_element], span=7), + ], + ), + ], +) diff --git a/web_ui/src/app/layout/header.py b/web_ui/src/app/layout/header.py new file mode 100644 index 0000000..a4436f5 --- /dev/null +++ b/web_ui/src/app/layout/header.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +import dash_bootstrap_components as dbc +import dash_mantine_components as dmc +from dash import dcc +from dash import html + +title_element = dmc.Center( + children=[ + html.H2( + children=[ + 'Visual Critical Discourse Analysis Tool', + ], + ), + ], +) + +progress_bar_element = dbc.Progress( + id='progress-bar', + max=100, + value=0, + color='lime', + label='', + style={ + 'width': '400px', + }, +) + +upload_button_element = dcc.Upload( + children=[ + dmc.Button( + 'upload images'.title(), + id='upload-button', + color='lime', + n_clicks=0, + variant='outline', + radius='sm', + size='md', + ), + ], + multiple=True, + id='upload-data', + accept='image/png,image/jpeg,image/jpg', +) + +header_element = dmc.Header( + height=80, + children=[ + dmc.Group( + children=[ + title_element, + progress_bar_element, + upload_button_element, + ], + position='apart', + style={ + 'margin': '10px', + 'padding': '10px', + }, + ), + ], +) diff --git a/web_ui/src/app/layout/image.py b/web_ui/src/app/layout/image.py new file mode 100644 index 0000000..aaada8d --- /dev/null +++ b/web_ui/src/app/layout/image.py @@ -0,0 +1,24 @@ +from __future__ import annotations + +from base64 import b64encode +from pathlib import Path + +import dash_mantine_components as dmc +from dash import html + +# read init img +init_img_path = Path(__file__).parent / 'init_img.png' +with open(init_img_path.absolute(), 'rb') as fh: + init_img_enc = b64encode(fh.read()).decode('utf-8') +# generate init img string +init_img_src = f"data:image/png;base64, {init_img_enc}" + +image_element = dmc.Center( + html.Img( + style={ + 'width': '100%', + }, + id='image-container', + src=init_img_src, + ), +) diff --git a/web_ui/src/app/layout/init_img.png b/web_ui/src/app/layout/init_img.png new file mode 100644 index 0000000000000000000000000000000000000000..7d821dfcd976138a85122865a919288bf9b54707 GIT binary patch literal 9479 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 Date: Tue, 7 May 2024 20:23:22 +0200 Subject: [PATCH 02/17] removed unused files --- src/__init__.py | 0 src/main.py | 52 ------ src/model_experiential/__init__.py | 1 - src/model_experiential/classes.py | 89 ---------- src/model_experiential/output.py | 20 --- src/model_interpersonal/__init__.py | 9 - src/model_interpersonal/classes.py | 137 --------------- src/model_interpersonal/output.py | 36 ---- src/model_textual/__init__.py | 5 - src/model_textual/classes.py | 92 ----------- src/model_textual/output.py | 21 --- src/web/__init__.py | 4 - src/web/app.py | 247 ---------------------------- src/web/layout/__init__.py | 1 - src/web/layout/alerts.py | 23 --- src/web/layout/body.py | 18 -- src/web/layout/header.py | 62 ------- src/web/layout/image.py | 21 --- src/web/layout/init_img.png | Bin 9479 -> 0 bytes src/web/layout/inputs.py | 23 --- src/web/layout/labels.py | 113 ------------- src/web/layout/layout.py | 34 ---- src/web/layout/stores.py | 13 -- 23 files changed, 1021 deletions(-) delete mode 100644 src/__init__.py delete mode 100644 src/main.py delete mode 100644 src/model_experiential/__init__.py delete mode 100644 src/model_experiential/classes.py delete mode 100644 src/model_experiential/output.py delete mode 100644 src/model_interpersonal/__init__.py delete mode 100644 src/model_interpersonal/classes.py delete mode 100644 src/model_interpersonal/output.py delete mode 100644 src/model_textual/__init__.py delete mode 100644 src/model_textual/classes.py delete mode 100644 src/model_textual/output.py delete mode 100644 src/web/__init__.py delete mode 100644 src/web/app.py delete mode 100644 src/web/layout/__init__.py delete mode 100644 src/web/layout/alerts.py delete mode 100644 src/web/layout/body.py delete mode 100644 src/web/layout/header.py delete mode 100644 src/web/layout/image.py delete mode 100644 src/web/layout/init_img.png delete mode 100644 src/web/layout/inputs.py delete mode 100644 src/web/layout/labels.py delete mode 100644 src/web/layout/layout.py delete mode 100644 src/web/layout/stores.py diff --git a/src/__init__.py b/src/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/main.py b/src/main.py deleted file mode 100644 index 803d203..0000000 --- a/src/main.py +++ /dev/null @@ -1,52 +0,0 @@ -from __future__ import annotations - -import logging -import os -from pathlib import Path - -from dotenv import load_dotenv - -from src.web import app - -# prepare optional local setup -env_path = Path(__file__).parent.parent / 'local.env' -load_dotenv(env_path) - -# ensure env vars set -necesasary_var_list = { - 'MONGO_HOST', - 'MONGO_DB', - 'MONGO_COLLECTION', - 'MONGO_USER', - 'MONGO_PASSWORD', - 'DASH_AUTH_USERNAME', - 'DASH_AUTH_PASSWORD', -} -for env_var in necesasary_var_list: - # ensure env var set - assert ( - env_var in os.environ - ), ( - f"environment variable not set: {env_var}" - ) - -# setup logging stream handler -fmt = ( - '%(asctime)s | ' - '%(levelname)s | ' - '%(filename)s | ' - '%(funcName)s | ' - '%(message)s' -) -datefmt = '%Y-%m-%d %H:%M:%S' -logging.basicConfig(format=fmt, datefmt=datefmt, level=logging.INFO) - -logging.info('initialized app') -server = app.server - -if __name__ == '__main__': - # prepare local env vars - os.environ['MONGO_HOST'] = 'localhost' - # run app - app.run(debug=True) - logging.info('started app') diff --git a/src/model_experiential/__init__.py b/src/model_experiential/__init__.py deleted file mode 100644 index a63e45d..0000000 --- a/src/model_experiential/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .classes import VisualSyntaxModelOutput diff --git a/src/model_experiential/classes.py b/src/model_experiential/classes.py deleted file mode 100644 index 1739af4..0000000 --- a/src/model_experiential/classes.py +++ /dev/null @@ -1,89 +0,0 @@ -from pydantic import BaseModel, ValidationError -from typing import List -import random - - -class OptionNotSetException(Exception): - pass - - -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) - - @classmethod - def from_choice(cls, option: str): - """Instantiate from choice.""" - if option is None: - raise ValidationError() - assert isinstance(option, str), "option is not a string" - allowed_options_list = cls.list_fields() - assert option in allowed_options_list, \ - f"{option} is not among allowed fields {allowed_options_list}" - kwargs = {field: 0 for field in cls.list_fields()} - kwargs[option] = 1 - 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 VisualSyntaxModelOutput(ModelOutput): - 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 - - -if __name__ == '__main__': - m = VisualSyntaxModelOutput.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 deleted file mode 100644 index 188234f..0000000 --- a/src/model_experiential/output.py +++ /dev/null @@ -1,20 +0,0 @@ -CLASS_NAMES = [ - "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" -] diff --git a/src/model_interpersonal/__init__.py b/src/model_interpersonal/__init__.py deleted file mode 100644 index 4599135..0000000 --- a/src/model_interpersonal/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -from .classes import ( - ContactModelOutput, - AngleModelOutput, - PointOfViewModelOutput, - DistanceModelOutput, - ModalityLightingModelOutput, - ModalityColorModelOutput, - ModalityDepthModelOutput -) diff --git a/src/model_interpersonal/classes.py b/src/model_interpersonal/classes.py deleted file mode 100644 index 8444d64..0000000 --- a/src/model_interpersonal/classes.py +++ /dev/null @@ -1,137 +0,0 @@ -from pydantic import BaseModel, ValidationError -from typing import List -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) - - @classmethod - def from_choice(cls, option: str): - """Instantiate from choice.""" - if option is None: - raise ValidationError() - assert isinstance(option, str) - allowed_options_list = cls.list_fields() - assert option in allowed_options_list, \ - f"{option} is not among allowed fields {allowed_options_list}" - kwargs = {field: 0 for field in cls.list_fields()} - kwargs[option] = 1 - 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 - - -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 deleted file mode 100644 index 14c84a9..0000000 --- a/src/model_interpersonal/output.py +++ /dev/null @@ -1,36 +0,0 @@ - -model_labels = { - "contact": [ - "offer", - "demand" - ], - "angle": [ - "high", - "eye-level", - "low" - ], - "point-of-view": [ - "frontal", - "oblique" - ], - "distance": [ - "long", - "medium", - "close" - ], - "modality lighting": [ - "high", - "medium", - "low" - ], - "modality color": [ - "high", - "medium", - "low" - ], - "modality depth": [ - "high", - "medium", - "low" - ] -} diff --git a/src/model_textual/__init__.py b/src/model_textual/__init__.py deleted file mode 100644 index a60055f..0000000 --- a/src/model_textual/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .classes import ( - InformationValueModelOutput, - FramingModelOutput, - SalienceModelOutput -) diff --git a/src/model_textual/classes.py b/src/model_textual/classes.py deleted file mode 100644 index e0c269d..0000000 --- a/src/model_textual/classes.py +++ /dev/null @@ -1,92 +0,0 @@ -from pydantic import BaseModel, ValidationError -from typing import List -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) - - @classmethod - def from_choice(cls, option: str): - """Instantiate from choice.""" - if option is None: - raise ValidationError() - assert isinstance(option, str) - allowed_options_list = cls.list_fields() - assert option in allowed_options_list, \ - f"{option} is not among allowed fields {allowed_options_list}" - kwargs = {field: 0 for field in cls.list_fields()} - kwargs[option] = 1 - 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()) diff --git a/src/model_textual/output.py b/src/model_textual/output.py deleted file mode 100644 index 8a4e08a..0000000 --- a/src/model_textual/output.py +++ /dev/null @@ -1,21 +0,0 @@ - -model_labels = { - "information value": [ - "given-new", - "ideal-real", - "central-marginal" - ], - "framing": [ - "frame lines", - "empty space", - "colour contrast", - "form contrast" - ], - "salience": [ - "size", - "colour", - "tone", - "form", - "positioning" - ] -} diff --git a/src/web/__init__.py b/src/web/__init__.py deleted file mode 100644 index 3bb26dd..0000000 --- a/src/web/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from __future__ import annotations - -from src.web.app import app -from src.web.app import server diff --git a/src/web/app.py b/src/web/app.py deleted file mode 100644 index 151fcd9..0000000 --- a/src/web/app.py +++ /dev/null @@ -1,247 +0,0 @@ -from __future__ import annotations - -import logging -import os - -import dash_bootstrap_components as dbc -from dash import ALL -from dash import Dash -from dash import Input -from dash import Output -from dash import State -from dash_auth import BasicAuth -from pydantic import ValidationError - -from .layout import app_layout -from core.database import connect -from core.database import count_documents -from core.database import get_visual_communication -from core.database import NoDocumentFoundException -from core.database import upsert_annotation -from core.database import upsert_visual_communication -from core.database import VisualCommunication -from core.dto import ModelData - -# setup app -app = Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP]) -app.title = 'visual critical discourse analysis'.title() -app.layout = app_layout -server = app.server - -# setup authentication -AUTH_DICT = { - os.getenv('DASH_AUTH_USERNAME'): os.getenv('DASH_AUTH_PASSWORD'), -} -BasicAuth(app, AUTH_DICT) - -# connect to database -collection, db, client = connect() - - -# define callbacks -@app.callback( - Output('alert-element', 'is_open'), - Output('alert-element', 'children'), - Input('alert-message', 'data'), -) -def show_alert( - msg: str | None, -): - if msg is None or msg == '': - return False, '' - logging.info(f'updated alert message: {msg}') - return True, msg - - -@app.callback( - Output('success-element', 'is_open'), - Output('success-element', 'children'), - Input('success-message', 'data'), -) -def show_success( - msg: str | None, -): - if msg is None or msg == '': - return False, '' - logging.info(f"updated success message: {msg}") - return True, msg - - -@app.callback( - Output('progress-bar', 'value'), - Output('progress-bar', 'max'), - Output('progress-bar', 'label'), - Input('next-button', 'n_clicks'), - Input('upload-data', 'filename'), -) -def update_progress_bar( - n_clicks: int, - filename_list: list[str] | None, -): - logging.info('began updating progress bar') - global collection - # get values from database - num_total = count_documents( - collection=collection, - only_with_annotation=False, - ) - num_handled = count_documents( - collection=collection, - only_with_annotation=True, - ) - limit = int(num_total/20) - label_str = f"{num_handled}/{num_total}" if num_handled >= limit else '' - return num_handled, num_total, label_str - - -@app.callback( - Output('success-message', 'data', allow_duplicate=True), - Output('alert-message', 'data', allow_duplicate=True), - Input('upload-data', 'contents'), - State('upload-data', 'filename'), - prevent_initial_call=True, -) -def upload_images( - content_list: list[str] | None, - filename_list: list[str] | None, -): - logging.info('began uploading images') - global collection - # stop early if possible - if content_list is None or filename_list is None: - logging.warning('content_list is None -> nothing to upload') - return None, 'nothing selected to upload'.title() - # build list of visual communication - visual_communication_list = [] - for content, filename in zip(content_list, filename_list): - try: - image = VisualCommunication.decode_image(content) - vis_com = VisualCommunication( - name=filename, - image=image, - ) - except Exception as exc: - logging.warning( - ( - 'failed creating VisualCommunication object ' - f"from file {filename}" - ), - exc, - ) - else: - visual_communication_list.append(vis_com) - # upsert documents - success = upsert_visual_communication( - collection=collection, - visual_communication_list=visual_communication_list, - ) - if success: - logging.info(f"succesfully uploaded {len(filename_list)} images") - return 'successfully uploaded images'.title(), None - logging.warning('failed inserting images into database') - return None, 'failed uploading images'.title() - - -@app.callback( - Output('alert-message', 'data', allow_duplicate=True), - Output('vis-com-name', 'data'), - Output('image-container', 'src'), - Output({'type': 'annotation', 'index': ALL}, 'value'), - Input('next-button', 'n_clicks'), - State('vis-com-name', 'data'), - State('image-container', 'src'), - State({'type': 'annotation', 'index': ALL}, 'id'), - State({'type': 'annotation', 'index': ALL}, 'value'), - prevent_initial_call=True, -) -def cycle_visual_communication_data( - n_clicks: int, - vis_com_name: str, - image_src: str, - annotation_keys: list, - annotation_values: list, -): - logging.info('began cycling visual communication data') - global collection - # prepare default response - response = [ - '', - vis_com_name, - image_src, - annotation_values, - ] - # check if next-button clicked - if n_clicks == 0: - logging.info('stopping early: next-button has not yet been clicked') - return response - # check if visual communication name is set - if len(vis_com_name) > 0: - logging.info('saving annotations to database: %s', vis_com_name) - try: - # extract option keys - annotation_keys = [ - elem['index'] - for elem in annotation_keys - ] - # ensure all options are set - logging.info(annotation_keys) - for option, value in zip(annotation_keys, annotation_values): - if value is None: - raise ValueError(f"{option} is not set") - # prepare data to save - annotation_keys = [ - elem.replace(' ', '_') - for elem - in annotation_keys - ] - annotation_values = [ - elem.replace(' ', '_').lower() - for elem - in annotation_values - ] - annotation_map = { - key: value - for key, value - in zip(annotation_keys, annotation_values) - } - # instantiate ModelOutputs object - annotations = ModelData.from_annotations(**annotation_map) - # save data to database - upsert_annotation( - collection=collection, - vis_com_name=vis_com_name, - annotations=annotations, - ) - except (ValueError, ValidationError) as exc: - msg = f"failed saving annotation: {exc}" - logging.warning(msg) - response[0] = msg - return tuple(response) - # get new visual communication - logging.info('trying to get new visual communication') - try: - # get data - vis_com = get_visual_communication( - collection=collection, - with_annotation=False, - ) - # set variables - vis_com_name = vis_com.name - image_src = vis_com.webencoded_image() - if vis_com.prediction is not None: - # TODO: update to use optional predictions - pass - else: - # reset annotations - annotation_values = [None for elem in annotation_values] - except NoDocumentFoundException: - msg = 'no unannotated data in database' - logging.warning(msg) - response[0] = msg - return tuple(response) - else: - response[1] = vis_com_name - response[2] = image_src - response[3] = annotation_values - logging.info('finished getting visual communication: %s', vis_com_name) - return tuple(response) diff --git a/src/web/layout/__init__.py b/src/web/layout/__init__.py deleted file mode 100644 index e3ea6c9..0000000 --- a/src/web/layout/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .layout import app_layout diff --git a/src/web/layout/alerts.py b/src/web/layout/alerts.py deleted file mode 100644 index 8be4a69..0000000 --- a/src/web/layout/alerts.py +++ /dev/null @@ -1,23 +0,0 @@ -from __future__ import annotations - -import dash_bootstrap_components as dbc -from dash import html - - -alerts_element = html.Div( - children=[ - dbc.Alert( - children='', - id='alert-element', - dismissable=True, - fade=False, - is_open=False, - ), - dbc.Alert( - children='', - id='success-element', - is_open=False, - duration=1000, - ), - ], -) diff --git a/src/web/layout/body.py b/src/web/layout/body.py deleted file mode 100644 index 0765e34..0000000 --- a/src/web/layout/body.py +++ /dev/null @@ -1,18 +0,0 @@ -import dash_mantine_components as dmc - -from .image import image_element -from .inputs import inputs_element - - -body_element = dmc.Container( - fluid=True, - children=[ - dmc.Grid( - grow=True, - children=[ - dmc.Col([image_element], span=5), - dmc.Col([inputs_element], span=7) - ], - ) - ], -) diff --git a/src/web/layout/header.py b/src/web/layout/header.py deleted file mode 100644 index a4436f5..0000000 --- a/src/web/layout/header.py +++ /dev/null @@ -1,62 +0,0 @@ -from __future__ import annotations - -import dash_bootstrap_components as dbc -import dash_mantine_components as dmc -from dash import dcc -from dash import html - -title_element = dmc.Center( - children=[ - html.H2( - children=[ - 'Visual Critical Discourse Analysis Tool', - ], - ), - ], -) - -progress_bar_element = dbc.Progress( - id='progress-bar', - max=100, - value=0, - color='lime', - label='', - style={ - 'width': '400px', - }, -) - -upload_button_element = dcc.Upload( - children=[ - dmc.Button( - 'upload images'.title(), - id='upload-button', - color='lime', - n_clicks=0, - variant='outline', - radius='sm', - size='md', - ), - ], - multiple=True, - id='upload-data', - accept='image/png,image/jpeg,image/jpg', -) - -header_element = dmc.Header( - height=80, - children=[ - dmc.Group( - children=[ - title_element, - progress_bar_element, - upload_button_element, - ], - position='apart', - style={ - 'margin': '10px', - 'padding': '10px', - }, - ), - ], -) diff --git a/src/web/layout/image.py b/src/web/layout/image.py deleted file mode 100644 index a32819a..0000000 --- a/src/web/layout/image.py +++ /dev/null @@ -1,21 +0,0 @@ -import dash_mantine_components as dmc -from dash import html -from pathlib import Path -from base64 import b64encode - -# read init img -init_img_path = Path(__file__).parent / "init_img.png" -with open(init_img_path.absolute(), "rb") as fh: - init_img_enc = b64encode(fh.read()).decode("utf-8") -# generate init img string -init_img_src = f"data:image/png;base64, {init_img_enc}" - -image_element = dmc.Center( - html.Img( - style={ - "width": "100%", - }, - id="image-container", - src=init_img_src - ) -) diff --git a/src/web/layout/init_img.png b/src/web/layout/init_img.png deleted file mode 100644 index 7d821dfcd976138a85122865a919288bf9b54707..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9479 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 Date: Tue, 7 May 2024 20:23:40 +0200 Subject: [PATCH 03/17] fixed code import bug --- web_ui/Dockerfile | 2 +- web_ui/src/main.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/web_ui/Dockerfile b/web_ui/Dockerfile index 59b4f9e..336607e 100644 --- a/web_ui/Dockerfile +++ b/web_ui/Dockerfile @@ -49,7 +49,7 @@ RUN mkdir -p /home/app && \ # add code while changing ownership WORKDIR $APP_HOME COPY --chown=app:app ./core ./core -COPY --chown=app:app ./src ./src +COPY --chown=app:app ./web_ui/src ./src # change to the app user USER app diff --git a/web_ui/src/main.py b/web_ui/src/main.py index 71670ca..5beed84 100644 --- a/web_ui/src/main.py +++ b/web_ui/src/main.py @@ -4,12 +4,12 @@ import logging import os from pathlib import Path -from app import app from dotenv import load_dotenv +from .app import app + # prepare optional local setup env_path = Path(__file__).parent.parent.parent / 'local.env' -assert env_path.exists() load_dotenv(env_path) # ensure env vars set -- 2.54.0 From 9a625cb6becfd60ecab503c05ee8dea80f2e4278 Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Tue, 7 May 2024 21:21:52 +0200 Subject: [PATCH 04/17] updated CI script --- .gitea/workflows/default.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/default.yaml b/.gitea/workflows/default.yaml index c7fbce7..24eb2f1 100644 --- a/.gitea/workflows/default.yaml +++ b/.gitea/workflows/default.yaml @@ -21,7 +21,7 @@ jobs: poetry install - name: PEP8 Check run: | - poetry run flake8 ./src --benchmark + poetry run flake8 . --benchmark - name: Type Check run: | - poetry run mypy ./src --disable-error-code=import-untyped + poetry run mypy . --disable-error-code=import-untyped -- 2.54.0 From e3e836c9fa8fc545e3258abe7942caf9827b6f73 Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Tue, 7 May 2024 21:22:25 +0200 Subject: [PATCH 05/17] fixed imports --- image_download/get_images.py | 3 +-- tests/model_outputs_from_annotation_test.py | 5 ++++- tests/prediction_upload_test.py | 4 ++-- tests/total_annotated_test.py | 5 +++-- tests/total_documents_test.py | 5 +++-- 5 files changed, 13 insertions(+), 9 deletions(-) diff --git a/image_download/get_images.py b/image_download/get_images.py index 90b6861..f167edb 100644 --- a/image_download/get_images.py +++ b/image_download/get_images.py @@ -6,10 +6,9 @@ from pathlib import Path import pandas as pd import requests +from classes import Instagram from retry import retry -from image_download.classes import Instagram - def get_sources() -> pd.DataFrame: """Get sources dateframe.""" diff --git a/tests/model_outputs_from_annotation_test.py b/tests/model_outputs_from_annotation_test.py index c4844fb..5e002fc 100644 --- a/tests/model_outputs_from_annotation_test.py +++ b/tests/model_outputs_from_annotation_test.py @@ -11,6 +11,9 @@ if __name__ == '__main__': in range(3) ] # generate random predictions - [vis_com.generate_random_prediction() for vis_com in vis_com_list] + [ + vis_com.generate_random_prediction() + for vis_com in vis_com_list + ] # type: ignore for vis_com in vis_com_list: print(vis_com) diff --git a/tests/prediction_upload_test.py b/tests/prediction_upload_test.py index 94246c0..7d8987b 100644 --- a/tests/prediction_upload_test.py +++ b/tests/prediction_upload_test.py @@ -7,7 +7,7 @@ from pathlib import Path from dotenv import load_dotenv from core.database import connect -from core.database import upsert_predictions +from core.database import upsert_prediction from core.database.classes import VisualCommunication if __name__ == '__main__': @@ -45,7 +45,7 @@ if __name__ == '__main__': for vis_com in vis_com_list: if vis_com.prediction is None: continue - upsert_predictions( + upsert_prediction( collection=collection, vis_com_name=vis_com.name, predictions=vis_com.prediction, diff --git a/tests/total_annotated_test.py b/tests/total_annotated_test.py index 0dafadb..0fe3717 100644 --- a/tests/total_annotated_test.py +++ b/tests/total_annotated_test.py @@ -6,7 +6,7 @@ from pathlib import Path from dotenv import load_dotenv from core.database import connect -from core.database import total_documents +from core.database import count_documents if __name__ == '__main__': # prepare env vars @@ -17,7 +17,8 @@ if __name__ == '__main__': # connect to database collection, db, client = connect() # get visual communication - num_docs = total_documents( + num_docs = count_documents( collection=collection, + only_with_annotation=True, ) print(f"number of annotated documents in database: {num_docs}") diff --git a/tests/total_documents_test.py b/tests/total_documents_test.py index e6e04a7..d7955fb 100644 --- a/tests/total_documents_test.py +++ b/tests/total_documents_test.py @@ -6,7 +6,7 @@ from pathlib import Path from dotenv import load_dotenv from core.database import connect -from core.database import total_documents +from core.database import count_documents if __name__ == '__main__': # prepare env vars @@ -17,7 +17,8 @@ if __name__ == '__main__': # connect to database collection, db, client = connect() # get visual communication - num_docs = total_documents( + num_docs = count_documents( collection=collection, + only_with_annotation=False, ) print(f"total number of documents in database: {num_docs}") -- 2.54.0 From 72c60170a76ebd7fdccb6fe54bb4dc77b01b950c Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Tue, 7 May 2024 21:30:51 +0200 Subject: [PATCH 06/17] fixed model attribute function --- tests/model_outputs_from_annotation_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/model_outputs_from_annotation_test.py b/tests/model_outputs_from_annotation_test.py index 5e002fc..fa4a96e 100644 --- a/tests/model_outputs_from_annotation_test.py +++ b/tests/model_outputs_from_annotation_test.py @@ -12,8 +12,8 @@ if __name__ == '__main__': ] # generate random predictions [ - vis_com.generate_random_prediction() + vis_com.from_random() for vis_com in vis_com_list - ] # type: ignore + ] for vis_com in vis_com_list: print(vis_com) -- 2.54.0 From fd9140093d15c79c0f8c955301e6fb5b340b7c38 Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Mon, 20 May 2024 19:19:29 +0200 Subject: [PATCH 07/17] moved webui and updated poetry packages --- model/src/dataloader.py | 2 +- model/src/models/visual_communication.py | 22 +- poetry.lock | 212 +++++++------- pyproject.toml | 25 +- {core => shared}/__init__.py | 0 {core => shared}/database/__init__.py | 0 {core => shared}/database/classes/__init__.py | 0 {core => shared}/database/classes/dataset.py | 0 .../database/classes/exceptions.py | 0 .../database/classes/visual_communication.py | 2 +- {core => shared}/database/utils/__init__.py | 0 {core => shared}/database/utils/connect.py | 0 .../database/utils/count_documents.py | 0 .../database/utils/get_dataset.py | 0 .../utils/get_visual_communication.py | 4 +- {core => shared}/database/utils/list_names.py | 0 .../database/utils/save_dataset.py | 4 +- .../database/utils/upsert_annotation.py | 2 +- .../database/utils/upsert_prediction.py | 2 +- .../utils/upsert_visual_communication.py | 2 +- {core => shared}/dto/__init__.py | 0 {core => shared}/dto/angle.py | 0 {core => shared}/dto/contact.py | 0 {core => shared}/dto/data_model.py | 0 {core => shared}/dto/distance.py | 0 {core => shared}/dto/framing.py | 0 {core => shared}/dto/information_value.py | 0 {core => shared}/dto/modality_color.py | 0 {core => shared}/dto/modality_depth.py | 0 {core => shared}/dto/modality_lighting.py | 0 {core => shared}/dto/model_data.py | 0 {core => shared}/dto/point_of_view.py | 0 {core => shared}/dto/salience.py | 0 {core => shared}/dto/visual_syntax.py | 0 shared/utils/__init__.py | 4 + shared/utils/check_env.py | 24 ++ shared/utils/setup_logging.py | 21 ++ tests/generate_random_prediction_test.py | 2 +- tests/get_visual_communication_test.py | 4 +- tests/image_download_test.py | 4 +- tests/image_upload_test.py | 4 +- tests/image_upload_to_server_test.py | 4 +- tests/model_outputs_from_annotation_test.py | 2 +- tests/prediction_upload_test.py | 6 +- tests/total_annotated_test.py | 4 +- tests/total_documents_test.py | 4 +- web_ui/Dockerfile | 5 +- web_ui/docker-compose.server.yml | 13 + web_ui/src/app/__init__.py | 2 +- web_ui/src/app/app.py | 251 ----------------- web_ui/src/app/init_app.py | 260 ++++++++++++++++++ web_ui/src/app/layout/labels.py | 22 +- web_ui/src/app/layout/stores.py | 4 +- web_ui/src/main.py | 54 +--- 54 files changed, 511 insertions(+), 460 deletions(-) rename {core => shared}/__init__.py (100%) rename {core => shared}/database/__init__.py (100%) rename {core => shared}/database/classes/__init__.py (100%) rename {core => shared}/database/classes/dataset.py (100%) rename {core => shared}/database/classes/exceptions.py (100%) rename {core => shared}/database/classes/visual_communication.py (98%) rename {core => shared}/database/utils/__init__.py (100%) rename {core => shared}/database/utils/connect.py (100%) rename {core => shared}/database/utils/count_documents.py (100%) rename {core => shared}/database/utils/get_dataset.py (100%) rename {core => shared}/database/utils/get_visual_communication.py (90%) rename {core => shared}/database/utils/list_names.py (100%) rename {core => shared}/database/utils/save_dataset.py (89%) rename {core => shared}/database/utils/upsert_annotation.py (94%) rename {core => shared}/database/utils/upsert_prediction.py (94%) rename {core => shared}/database/utils/upsert_visual_communication.py (91%) rename {core => shared}/dto/__init__.py (100%) rename {core => shared}/dto/angle.py (100%) rename {core => shared}/dto/contact.py (100%) rename {core => shared}/dto/data_model.py (100%) rename {core => shared}/dto/distance.py (100%) rename {core => shared}/dto/framing.py (100%) rename {core => shared}/dto/information_value.py (100%) rename {core => shared}/dto/modality_color.py (100%) rename {core => shared}/dto/modality_depth.py (100%) rename {core => shared}/dto/modality_lighting.py (100%) rename {core => shared}/dto/model_data.py (100%) rename {core => shared}/dto/point_of_view.py (100%) rename {core => shared}/dto/salience.py (100%) rename {core => shared}/dto/visual_syntax.py (100%) create mode 100644 shared/utils/__init__.py create mode 100644 shared/utils/check_env.py create mode 100644 shared/utils/setup_logging.py create mode 100644 web_ui/docker-compose.server.yml delete mode 100644 web_ui/src/app/app.py create mode 100644 web_ui/src/app/init_app.py diff --git a/model/src/dataloader.py b/model/src/dataloader.py index 77c433e..681e885 100644 --- a/model/src/dataloader.py +++ b/model/src/dataloader.py @@ -14,7 +14,7 @@ from torchvision.transforms.functional import pad from torchvision.transforms.functional import resize from torchvision.transforms.functional import rotate -from core.database import connect +from shared.database import connect # resnet18 original normalization values RESNET_NORMALIZE_MEAN = [0.485, 0.456, 0.406] diff --git a/model/src/models/visual_communication.py b/model/src/models/visual_communication.py index 8d666f5..70c9ae6 100644 --- a/model/src/models/visual_communication.py +++ b/model/src/models/visual_communication.py @@ -14,17 +14,17 @@ from .point_of_view import PointOfViewTail from .resnet18_head import ResNet18Head from .salience import SalienceTail from .visual_syntax import VisualSyntaxTail -from core.dto import AngleData -from core.dto import ContactData -from core.dto import DistanceData -from core.dto import FramingData -from core.dto import InformationValueData -from core.dto import ModalityColorData -from core.dto import ModalityDepthData -from core.dto import ModalityLightingData -from core.dto import PointOfViewData -from core.dto import SalienceData -from core.dto import VisualSyntaxData +from shared.dto import AngleData +from shared.dto import ContactData +from shared.dto import DistanceData +from shared.dto import FramingData +from shared.dto import InformationValueData +from shared.dto import ModalityColorData +from shared.dto import ModalityDepthData +from shared.dto import ModalityLightingData +from shared.dto import PointOfViewData +from shared.dto import SalienceData +from shared.dto import VisualSyntaxData class VisualCommunicationModel(nn.Module): diff --git a/poetry.lock b/poetry.lock index 44b9a9a..3dad6fc 100644 --- a/poetry.lock +++ b/poetry.lock @@ -32,13 +32,13 @@ tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "p [[package]] name = "blinker" -version = "1.8.1" +version = "1.8.2" description = "Fast, simple object-to-object and broadcast signaling" optional = false python-versions = ">=3.8" files = [ - {file = "blinker-1.8.1-py3-none-any.whl", hash = "sha256:5f1cdeff423b77c31b89de0565cd03e5275a03028f44b2b15f912632a58cced6"}, - {file = "blinker-1.8.1.tar.gz", hash = "sha256:da44ec748222dcd0105ef975eed946da197d5bdf8bafb6aa92f5bc89da63fa25"}, + {file = "blinker-1.8.2-py3-none-any.whl", hash = "sha256:1779309f71bf239144b9399d06ae925637cf6634cf6bd131104184531bf67c01"}, + {file = "blinker-1.8.2.tar.gz", hash = "sha256:8f77b09d3bf7c795e969e9486f39c2c5e9c39d4ee07424be2bc594ece9642d83"}, ] [[package]] @@ -242,13 +242,13 @@ files = [ [[package]] name = "dash" -version = "2.16.1" +version = "2.17.0" description = "A Python framework for building reactive web-apps. Developed by Plotly." optional = false python-versions = ">=3.8" files = [ - {file = "dash-2.16.1-py3-none-any.whl", hash = "sha256:8a9d2a618e415113c0b2a4d25d5dc4df5cb921f733b33dde75559db2316b1df1"}, - {file = "dash-2.16.1.tar.gz", hash = "sha256:b2871d6b8d4c9dfd0a64f89f22d001c93292910b41d92d9ff2bb424a28283976"}, + {file = "dash-2.17.0-py3-none-any.whl", hash = "sha256:2421569023b2cd46ea2d4b2c14fe72c71b7436527a3102219b2265fa361e7c67"}, + {file = "dash-2.17.0.tar.gz", hash = "sha256:d065cd88771e45d0485993be0d27565e08918cb7edd18e31ee1c5b41252fc2fa"}, ] [package.dependencies] @@ -267,7 +267,7 @@ Werkzeug = "<3.1" [package.extras] celery = ["celery[redis] (>=5.1.2)", "redis (>=3.5.3)"] -ci = ["black (==22.3.0)", "dash-dangerously-set-inner-html", "dash-flow-example (==0.0.5)", "flake8 (==7.0.0)", "flaky (==3.7.0)", "flask-talisman (==1.0.0)", "jupyterlab (<4.0.0)", "mimesis (<=11.1.0)", "mock (==4.0.3)", "numpy (<=1.26.3)", "openpyxl", "orjson (==3.9.12)", "pandas (>=1.4.0)", "pyarrow", "pylint (==3.0.3)", "pytest-mock", "pytest-rerunfailures", "pytest-sugar (==0.9.6)", "pyzmq (==25.1.2)", "xlrd (>=2.0.1)"] +ci = ["black (==22.3.0)", "dash-dangerously-set-inner-html", "dash-flow-example (==0.0.5)", "flake8 (==7.0.0)", "flaky (==3.8.1)", "flask-talisman (==1.0.0)", "jupyterlab (<4.0.0)", "mimesis (<=11.1.0)", "mock (==4.0.3)", "numpy (<=1.26.3)", "openpyxl", "orjson (==3.9.12)", "pandas (>=1.4.0)", "pyarrow", "pylint (==3.0.3)", "pytest-mock", "pytest-rerunfailures", "pytest-sugar (==0.9.6)", "pyzmq (==25.1.2)", "xlrd (>=2.0.1)"] compress = ["flask-compress"] dev = ["PyYAML (>=5.4.1)", "coloredlogs (>=15.0.1)", "fire (>=0.4.0)"] diskcache = ["diskcache (>=5.2.1)", "multiprocess (>=0.70.12)", "psutil (>=5.8.0)"] @@ -440,13 +440,13 @@ dotenv = ["python-dotenv"] [[package]] name = "fsspec" -version = "2024.3.1" +version = "2024.5.0" description = "File-system specification" optional = false python-versions = ">=3.8" files = [ - {file = "fsspec-2024.3.1-py3-none-any.whl", hash = "sha256:918d18d41bf73f0e2b261824baeb1b124bcf771767e3a26425cd7dec3332f512"}, - {file = "fsspec-2024.3.1.tar.gz", hash = "sha256:f39780e282d7d117ffb42bb96992f8a90795e4d0fb0f661a70ca39fe9c43ded9"}, + {file = "fsspec-2024.5.0-py3-none-any.whl", hash = "sha256:e0fdbc446d67e182f49a70b82cf7889028a63588fde6b222521f10937b2b670c"}, + {file = "fsspec-2024.5.0.tar.gz", hash = "sha256:1d021b0b0f933e3b3029ed808eb400c08ba101ca2de4b3483fbc9ca23fcee94a"}, ] [package.extras] @@ -454,7 +454,7 @@ abfs = ["adlfs"] adl = ["adlfs"] arrow = ["pyarrow (>=1)"] dask = ["dask", "distributed"] -devel = ["pytest", "pytest-cov"] +dev = ["pre-commit", "ruff"] dropbox = ["dropbox", "dropboxdrivefs", "requests"] full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "dask", "distributed", "dropbox", "dropboxdrivefs", "fusepy", "gcsfs", "libarchive-c", "ocifs", "panel", "paramiko", "pyarrow (>=1)", "pygit2", "requests", "s3fs", "smbprotocol", "tqdm"] fuse = ["fusepy"] @@ -471,6 +471,9 @@ s3 = ["s3fs"] sftp = ["paramiko"] smb = ["smbprotocol"] ssh = ["paramiko"] +test = ["aiohttp (!=4.0.0a0,!=4.0.0a1)", "numpy", "pytest", "pytest-asyncio (!=0.22.0)", "pytest-benchmark", "pytest-cov", "pytest-mock", "pytest-recording", "pytest-rerunfailures", "requests"] +test-downstream = ["aiobotocore (>=2.5.4,<3.0.0)", "dask-expr", "dask[dataframe,test]", "moto[server] (>4,<5)", "pytest-timeout", "xarray"] +test-full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "cloudpickle", "dask", "distributed", "dropbox", "dropboxdrivefs", "fastparquet", "fusepy", "gcsfs", "jinja2", "kerchunk", "libarchive-c", "lz4", "notebook", "numpy", "ocifs", "pandas", "panel", "paramiko", "pyarrow", "pyarrow (>=1)", "pyftpdlib", "pygit2", "pytest", "pytest-asyncio (!=0.22.0)", "pytest-benchmark", "pytest-cov", "pytest-mock", "pytest-recording", "pytest-rerunfailures", "python-snappy", "requests", "smbprotocol", "tqdm", "urllib3", "zarr", "zstandard"] tqdm = ["tqdm"] [[package]] @@ -547,13 +550,13 @@ files = [ [[package]] name = "jinja2" -version = "3.1.3" +version = "3.1.4" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" files = [ - {file = "Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa"}, - {file = "Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"}, + {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, + {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, ] [package.dependencies] @@ -1113,13 +1116,13 @@ xmp = ["defusedxml"] [[package]] name = "plotly" -version = "5.21.0" +version = "5.22.0" description = "An open-source, interactive data visualization library for Python" optional = false python-versions = ">=3.8" files = [ - {file = "plotly-5.21.0-py3-none-any.whl", hash = "sha256:a33f41fd5922e45b2b253f795b200d14452eb625790bb72d0a72cf1328a6abbf"}, - {file = "plotly-5.21.0.tar.gz", hash = "sha256:69243f8c165d4be26c0df1c6f0b7b258e2dfeefe032763404ad7e7fb7d7c2073"}, + {file = "plotly-5.22.0-py3-none-any.whl", hash = "sha256:68fc1901f098daeb233cc3dd44ec9dc31fb3ca4f4e53189344199c43496ed006"}, + {file = "plotly-5.22.0.tar.gz", hash = "sha256:859fdadbd86b5770ae2466e542b761b247d1c6b49daed765b95bb8c7063e7469"}, ] [package.dependencies] @@ -1282,71 +1285,71 @@ files = [ [[package]] name = "pymongo" -version = "4.7.1" +version = "4.7.2" description = "Python driver for MongoDB " optional = false python-versions = ">=3.7" files = [ - {file = "pymongo-4.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f8822614975038e0cece47d12e7634a79c2ee590a0ae78ae64c37b9c6610a14c"}, - {file = "pymongo-4.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:57b5b485ef89270ed2e603814f43f0fdd9b8ba5d4039124d90878cdc2327000c"}, - {file = "pymongo-4.7.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e99dac3c7c2cb498937cc1767361851099da38861e921113318c87d71e3d127"}, - {file = "pymongo-4.7.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:253ed8fd6e7f4b2a1caa89e6b287b9e04f42613319ee1e1240c2db2afe1637e7"}, - {file = "pymongo-4.7.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8cee62188127a126f59ea45d3981868a5e35343be4ef4ad8712eaf42be37a00b"}, - {file = "pymongo-4.7.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31ed8ba3da0366346264604b3a443f5a4232cab5ed45f520bead6184cf0851a1"}, - {file = "pymongo-4.7.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:887d043ecc0c7d5591925bbc7abb67caf21c94d6e6e5d442cb49eb5d9d8ee76b"}, - {file = "pymongo-4.7.1-cp310-cp310-win32.whl", hash = "sha256:bfd5c7e5bb87171a5296fa32205adb50b27704a612036ec4395c3cd316fc0e91"}, - {file = "pymongo-4.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:5ae1aeeb405c29885266666dc7115792d647ed68cfdb6ed02e2e211d12f2e1c8"}, - {file = "pymongo-4.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e4a63ba6813a2168ebd35ea5369f6c33f7787525986cd77668b7956acc3d2a38"}, - {file = "pymongo-4.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:811a07bba9d35f1e34159ede632ac71dbc429b372a20004e32d6578af872db1a"}, - {file = "pymongo-4.7.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d227555be35078b53f506f6b58bd0b0e8fd4513e89e6f29e83a97efab439250"}, - {file = "pymongo-4.7.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:daf35ab13b86aba7cc8c4b019882f1fa8d287a26f586ef5eaf60a5233d3eaa52"}, - {file = "pymongo-4.7.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa354933a158e57494c98b592f46d5d24d1b109e6ba05a05179cde719d9f7fd3"}, - {file = "pymongo-4.7.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad360630c221aee7c0841a51851496a3ca6fdea87007098a982c1aa26e34083a"}, - {file = "pymongo-4.7.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5119c66af8c4197c8757b4b7d98c443e5b127c224ac92fb657dbe2b512ae2713"}, - {file = "pymongo-4.7.1-cp311-cp311-win32.whl", hash = "sha256:11f74dafde63ad2dc30c01f40b4c69d9af157f8ba5224b0c9d4de7158537266f"}, - {file = "pymongo-4.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:ec94d29103317aa920dae59ed385de9604cb0ef840b5b7137b5eaa7a2042580a"}, - {file = "pymongo-4.7.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b8b95e2163b73d03a913efa89b0f7c5012be82efd4e9dbce8aa62010a75a277c"}, - {file = "pymongo-4.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fb1a884b1c6aeac5ffeb8ccb696fbc242a7ae1bba36f2328c01f76fab7221b94"}, - {file = "pymongo-4.7.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ccc8dd4fe9aac18dde27c33a53271c6c90159b74c43fbdab1d33d5efc36c2f5"}, - {file = "pymongo-4.7.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7247c1dc7d8eed4e24eb1dd92c4c58ebf1e5159500015652552acfdebdeed256"}, - {file = "pymongo-4.7.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:45ac46f0d6bdc2baac34ced60aae27b2083170d77397330eff0ac5689ea29d38"}, - {file = "pymongo-4.7.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a848249d5b4763497add62f7dd7bd0ce1538129bf42f4cb132a76d24c61bf98d"}, - {file = "pymongo-4.7.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ff6d56ca1f0cd3687a13ce90a32a8efb3cc3a53728e5ac160c4c30d10385a72"}, - {file = "pymongo-4.7.1-cp312-cp312-win32.whl", hash = "sha256:e175d74c52b6c8414a4b4504a2dd42b0202d101b2eb9508a34c137357683864e"}, - {file = "pymongo-4.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:263c169302df636f9086b584994a51d0adfc8738fe27d7b8e2aacf46fd68b6cb"}, - {file = "pymongo-4.7.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:337d99f88d32a5f8056d6d2bc365ccf09d09583f3942882c50cf11b459e8fbc0"}, - {file = "pymongo-4.7.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30a9d891631d7e847b24f551b1d89ff2033539e7cd8e9af29714b4d0db7abb06"}, - {file = "pymongo-4.7.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73bf96ece4999b0bbab7169cb2b9c60918b434487009e48be4bd47eeb2aa7b14"}, - {file = "pymongo-4.7.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ef32a7cfe748c0c72fdad9e51459de5e0c6b16c5288b39f863abfff23503847"}, - {file = "pymongo-4.7.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24c8f1dd545360ec1b79007a3ba6573af565df6fde49f6dfc53813f3f475a751"}, - {file = "pymongo-4.7.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b897b60b2d55c26f3efea0effc11b655db68125c3731274bc3953375e9ccab73"}, - {file = "pymongo-4.7.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5a58b6cd7c423ba49db10d8445756062c931ad2246ba0da1e705bf22962fd9e9"}, - {file = "pymongo-4.7.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:ed6b3a0740efe98bb03ccf054578e9788ebcd06d021d548b8217ab2c82e45975"}, - {file = "pymongo-4.7.1-cp37-cp37m-win32.whl", hash = "sha256:85b8dd3756b73993b1e3ab6b1cba826b9e4987a094a5d5b6d37313776458cd94"}, - {file = "pymongo-4.7.1-cp37-cp37m-win_amd64.whl", hash = "sha256:297cdc87c4b4168782b571c8643540e9b0ad1d09266b43d2f5954f8632280835"}, - {file = "pymongo-4.7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7b10603ba64af08f5af7eb9a69d6b24e3c69d91fdd48c54b95e284686c1c582d"}, - {file = "pymongo-4.7.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:64b69b9cd8a6d23881a80490d575e92918f9afca43096a7d6c1013d6b3e5c75c"}, - {file = "pymongo-4.7.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c7e05454cdc5aa4702e03cad0df4205daccd6fd631bbbf0a85bbe598129a6cc"}, - {file = "pymongo-4.7.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e0a30a022ac8a9164ee5a4b761e13dbb3d10a21845f7258011e3415151fb645"}, - {file = "pymongo-4.7.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:13fc201e073644acd77860d9e91ccfc27addf510563e07381cadc9a55ac3a894"}, - {file = "pymongo-4.7.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4dd998e9f0f7694032c1648c7f57fccaa78903df6329b8f8ae20cfa7c4ceca34"}, - {file = "pymongo-4.7.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:455f9d603ed0990a787773d5718e871300bddf585ce543baf129c9f5ca3adb02"}, - {file = "pymongo-4.7.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d804eaf19a65211cc2c8c5db75be685c3f31c64cdab639794f66f13f8e258ba6"}, - {file = "pymongo-4.7.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a46c08ef0b273c415b1e8933f6739596be264ae700a4927f84e0b84e70fdf0eb"}, - {file = "pymongo-4.7.1-cp38-cp38-win32.whl", hash = "sha256:58989bcb94233233a71645236b972835d4f87a6bb1b7e818d38a7e6e6d4630de"}, - {file = "pymongo-4.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:d63f38454a2e23c117d3ceab3b661568f2418536825787256ad24e5baaedfd27"}, - {file = "pymongo-4.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d50969de00d3522b2c394f7e59b843871e2be4b525af92066da7f3bd02799fdc"}, - {file = "pymongo-4.7.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f2a720e787c9b9b912db5bb4c3e7123ccff1352d6c3ac0cb2c7ee392cdc95c00"}, - {file = "pymongo-4.7.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c808098f2cdb87d4035144e536ba5fa7709d0420c17b68e6ace5da18c38ded5f"}, - {file = "pymongo-4.7.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1829a7db720ff586aaf59c806e89e0a388548063aa844d21a570a231ad8ca87"}, - {file = "pymongo-4.7.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:615c7573d7a9c4837332a673fdc5a5f214b474dd52d846bcf4cc3d011550bee1"}, - {file = "pymongo-4.7.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e935712b17e7a42831022353bac91a346a792658a54e12bec907ec11695cc899"}, - {file = "pymongo-4.7.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbc32217c81d87750401fa1c2bc9450e854b23e6e30243c82d3514b8e58f39e3"}, - {file = "pymongo-4.7.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5bc87db2e9563295c4e45602ab978a2fcbaba3ab89e745503b24f895cddeb755"}, - {file = "pymongo-4.7.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:65c45682d5ed8c6618bde22cd6716b47a197f4ef800a025213b28d13a59e5fca"}, - {file = "pymongo-4.7.1-cp39-cp39-win32.whl", hash = "sha256:67cbee427c263a4483e3249fef480788ccc16edb1a4fc330c4c6cb0cb9db94a8"}, - {file = "pymongo-4.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:1bd1eef70c1eda838b26397ef75c9580d7a97fd94b6324971d7f3d2ad3552e9a"}, - {file = "pymongo-4.7.1.tar.gz", hash = "sha256:811c41c6227b7548afcb53e1b996c25262d837b5e5f519e2ddc2c7e59d8728a5"}, + {file = "pymongo-4.7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:268d8578c0500012140c5460755ea405cbfe541ef47c81efa9d6744f0f99aeca"}, + {file = "pymongo-4.7.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:827611beb6c483260d520cfa6a49662d980dfa5368a04296f65fa39e78fccea7"}, + {file = "pymongo-4.7.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a754e366c404d19ff3f077ddeed64be31e0bb515e04f502bf11987f1baa55a16"}, + {file = "pymongo-4.7.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c44efab10d9a3db920530f7bcb26af8f408b7273d2f0214081d3891979726328"}, + {file = "pymongo-4.7.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35b3f0c7d49724859d4df5f0445818d525824a6cd55074c42573d9b50764df67"}, + {file = "pymongo-4.7.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e37faf298a37ffb3e0809e77fbbb0a32b6a2d18a83c59cfc2a7b794ea1136b0"}, + {file = "pymongo-4.7.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1bcd58669e56c08f1e72c5758868b5df169fe267501c949ee83c418e9df9155"}, + {file = "pymongo-4.7.2-cp310-cp310-win32.whl", hash = "sha256:c72d16fede22efe7cdd1f422e8da15760e9498024040429362886f946c10fe95"}, + {file = "pymongo-4.7.2-cp310-cp310-win_amd64.whl", hash = "sha256:12d1fef77d25640cb78893d07ff7d2fac4c4461d8eec45bd3b9ad491a1115d6e"}, + {file = "pymongo-4.7.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fc5af24fcf5fc6f7f40d65446400d45dd12bea933d0299dc9e90c5b22197f1e9"}, + {file = "pymongo-4.7.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:730778b6f0964b164c187289f906bbc84cb0524df285b7a85aa355bbec43eb21"}, + {file = "pymongo-4.7.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47a1a4832ef2f4346dcd1a10a36ade7367ad6905929ddb476459abb4fd1b98cb"}, + {file = "pymongo-4.7.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6eab12c6385526d386543d6823b07187fefba028f0da216506e00f0e1855119"}, + {file = "pymongo-4.7.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:37e9ea81fa59ee9274457ed7d59b6c27f6f2a5fe8e26f184ecf58ea52a019cb8"}, + {file = "pymongo-4.7.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e9d9d2c0aae73aa4369bd373ac2ac59f02c46d4e56c4b6d6e250cfe85f76802"}, + {file = "pymongo-4.7.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb6e00a79dff22c9a72212ad82021b54bdb3b85f38a85f4fc466bde581d7d17a"}, + {file = "pymongo-4.7.2-cp311-cp311-win32.whl", hash = "sha256:02efd1bb3397e24ef2af45923888b41a378ce00cb3a4259c5f4fc3c70497a22f"}, + {file = "pymongo-4.7.2-cp311-cp311-win_amd64.whl", hash = "sha256:87bb453ac3eb44db95cb6d5a616fbc906c1c00661eec7f55696253a6245beb8a"}, + {file = "pymongo-4.7.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:12c466e02133b7f8f4ff1045c6b5916215c5f7923bc83fd6e28e290cba18f9f6"}, + {file = "pymongo-4.7.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f91073049c43d14e66696970dd708d319b86ee57ef9af359294eee072abaac79"}, + {file = "pymongo-4.7.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87032f818bf5052ab742812c715eff896621385c43f8f97cdd37d15b5d394e95"}, + {file = "pymongo-4.7.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6a87eef394039765679f75c6a47455a4030870341cb76eafc349c5944408c882"}, + {file = "pymongo-4.7.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d275596f840018858757561840767b39272ac96436fcb54f5cac6d245393fd97"}, + {file = "pymongo-4.7.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82102e353be13f1a6769660dd88115b1da382447672ba1c2662a0fbe3df1d861"}, + {file = "pymongo-4.7.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:194065c9d445017b3c82fb85f89aa2055464a080bde604010dc8eb932a6b3c95"}, + {file = "pymongo-4.7.2-cp312-cp312-win32.whl", hash = "sha256:db4380d1e69fdad1044a4b8f3bb105200542c49a0dde93452d938ff9db1d6d29"}, + {file = "pymongo-4.7.2-cp312-cp312-win_amd64.whl", hash = "sha256:fadc6e8db7707c861ebe25b13ad6aca19ea4d2c56bf04a26691f46c23dadf6e4"}, + {file = "pymongo-4.7.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2cb77d09bd012cb4b30636e7e38d00b5f9be5eb521c364bde66490c45ee6c4b4"}, + {file = "pymongo-4.7.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56bf8b706946952acdea0fe478f8e44f1ed101c4b87f046859e6c3abe6c0a9f4"}, + {file = "pymongo-4.7.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bcf337d1b252405779d9c79978d6ca15eab3cdaa2f44c100a79221bddad97c8a"}, + {file = "pymongo-4.7.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ffd1519edbe311df73c74ec338de7d294af535b2748191c866ea3a7c484cd15"}, + {file = "pymongo-4.7.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4d59776f435564159196d971aa89422ead878174aff8fe18e06d9a0bc6d648c"}, + {file = "pymongo-4.7.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:347c49cf7f0ba49ea87c1a5a1984187ecc5516b7c753f31938bf7b37462824fd"}, + {file = "pymongo-4.7.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:84bc00200c3cbb6c98a2bb964c9e8284b641e4a33cf10c802390552575ee21de"}, + {file = "pymongo-4.7.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:fcaf8c911cb29316a02356f89dbc0e0dfcc6a712ace217b6b543805690d2aefd"}, + {file = "pymongo-4.7.2-cp37-cp37m-win32.whl", hash = "sha256:b48a5650ee5320d59f6d570bd99a8d5c58ac6f297a4e9090535f6561469ac32e"}, + {file = "pymongo-4.7.2-cp37-cp37m-win_amd64.whl", hash = "sha256:5239ef7e749f1326ea7564428bf861d5250aa39d7f26d612741b1b1273227062"}, + {file = "pymongo-4.7.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d2dcf608d35644e8d276d61bf40a93339d8d66a0e5f3e3f75b2c155a421a1b71"}, + {file = "pymongo-4.7.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:25eeb2c18ede63891cbd617943dd9e6b9cbccc54f276e0b2e693a0cc40f243c5"}, + {file = "pymongo-4.7.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9349f0bb17a31371d4cacb64b306e4ca90413a3ad1fffe73ac7cd495570d94b5"}, + {file = "pymongo-4.7.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ffd4d7cb2e6c6e100e2b39606d38a9ffc934e18593dc9bb326196afc7d93ce3d"}, + {file = "pymongo-4.7.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9a8bd37f5dabc86efceb8d8cbff5969256523d42d08088f098753dba15f3b37a"}, + {file = "pymongo-4.7.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c78f156edc59b905c80c9003e022e1a764c54fd40ac4fea05b0764f829790e2"}, + {file = "pymongo-4.7.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9d892fb91e81cccb83f507cdb2ea0aa026ec3ced7f12a1d60f6a5bf0f20f9c1f"}, + {file = "pymongo-4.7.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:87832d6076c2c82f42870157414fd876facbb6554d2faf271ffe7f8f30ce7bed"}, + {file = "pymongo-4.7.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:ce1a374ea0e49808e0380ffc64284c0ce0f12bd21042b4bef1af3eb7bdf49054"}, + {file = "pymongo-4.7.2-cp38-cp38-win32.whl", hash = "sha256:eb0642e5f0dd7e86bb358749cc278e70b911e617f519989d346f742dc9520dfb"}, + {file = "pymongo-4.7.2-cp38-cp38-win_amd64.whl", hash = "sha256:4bdb5ffe1cd3728c9479671a067ef44dacafc3743741d4dc700c377c4231356f"}, + {file = "pymongo-4.7.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:743552033c63f0afdb56b9189ab04b5c1dbffd7310cf7156ab98eebcecf24621"}, + {file = "pymongo-4.7.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5239776633f7578b81207e5646245415a5a95f6ae5ef5dff8e7c2357e6264bfc"}, + {file = "pymongo-4.7.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:727ad07952c155cd20045f2ce91143c7dc4fb01a5b4e8012905a89a7da554b0c"}, + {file = "pymongo-4.7.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9385654f01a90f73827af4db90c290a1519f7d9102ba43286e187b373e9a78e9"}, + {file = "pymongo-4.7.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0d833651f1ba938bb7501f13e326b96cfbb7d98867b2d545ca6d69c7664903e0"}, + {file = "pymongo-4.7.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf17ea9cea14d59b0527403dd7106362917ced7c4ec936c4ba22bd36c912c8e0"}, + {file = "pymongo-4.7.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cecd2df037249d1c74f0af86fb5b766104a5012becac6ff63d85d1de53ba8b98"}, + {file = "pymongo-4.7.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:65b4c00dedbd333698b83cd2095a639a6f0d7c4e2a617988f6c65fb46711f028"}, + {file = "pymongo-4.7.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d9b6cbc037108ff1a0a867e7670d8513c37f9bcd9ee3d2464411bfabf70ca002"}, + {file = "pymongo-4.7.2-cp39-cp39-win32.whl", hash = "sha256:cf28430ec1924af1bffed37b69a812339084697fd3f3e781074a0148e6475803"}, + {file = "pymongo-4.7.2-cp39-cp39-win_amd64.whl", hash = "sha256:e004527ea42a6b99a8b8d5b42b42762c3bdf80f88fbdb5c3a9d47f3808495b86"}, + {file = "pymongo-4.7.2.tar.gz", hash = "sha256:9024e1661c6e40acf468177bf90ce924d1bc681d2b244adda3ed7b2f4c4d17d7"}, ] [package.dependencies] @@ -1414,13 +1417,13 @@ files = [ [[package]] name = "requests" -version = "2.31.0" +version = "2.32.0" description = "Python HTTP for Humans." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, - {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, + {file = "requests-2.32.0-py3-none-any.whl", hash = "sha256:f2c3881dddb70d056c5bd7600a4fae312b2a300e39be6a118d30b90bd27262b5"}, + {file = "requests-2.32.0.tar.gz", hash = "sha256:fa5490319474c82ef1d2c9bc459d3652e3ae4ef4c4ebdd18a21145a47ca4b6b8"}, ] [package.dependencies] @@ -1464,13 +1467,13 @@ six = ">=1.7.0" [[package]] name = "selenium" -version = "4.20.0" +version = "4.21.0" description = "" optional = false python-versions = ">=3.8" files = [ - {file = "selenium-4.20.0-py3-none-any.whl", hash = "sha256:b1d0c33b38ca27d0499183e48e1dd09ff26973481f5d3ef2983073813ae6588d"}, - {file = "selenium-4.20.0.tar.gz", hash = "sha256:0bd564ee166980d419a8aaf4ac00289bc152afcf2eadca5efe8c8e36711853fd"}, + {file = "selenium-4.21.0-py3-none-any.whl", hash = "sha256:4770ffe5a5264e609de7dc914be6b89987512040d5a8efb2abb181330d097993"}, + {file = "selenium-4.21.0.tar.gz", hash = "sha256:650dbfa5159895ff00ad16e5ddb6ceecb86b90c7ed2012b3f041f64e6e4904fe"}, ] [package.dependencies] @@ -1545,17 +1548,18 @@ mpmath = ">=0.19" [[package]] name = "tenacity" -version = "8.2.3" +version = "8.3.0" description = "Retry code until it succeeds" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "tenacity-8.2.3-py3-none-any.whl", hash = "sha256:ce510e327a630c9e1beaf17d42e6ffacc88185044ad85cf74c0a8887c6a0f88c"}, - {file = "tenacity-8.2.3.tar.gz", hash = "sha256:5398ef0d78e63f40007c1fb4c0bff96e1911394d2fa8d194f77619c05ff6cc8a"}, + {file = "tenacity-8.3.0-py3-none-any.whl", hash = "sha256:3649f6443dbc0d9b01b9d8020a9c4ec7a1ff5f6f3c6c8a036ef371f573fe9185"}, + {file = "tenacity-8.3.0.tar.gz", hash = "sha256:953d4e6ad24357bceffbc9707bc74349aca9d245f68eb65419cf0c249a1949a2"}, ] [package.extras] -doc = ["reno", "sphinx", "tornado (>=4.5)"] +doc = ["reno", "sphinx"] +test = ["pytest", "tornado (>=4.5)", "typeguard"] [[package]] name = "torch" @@ -1658,13 +1662,13 @@ scipy = ["scipy"] [[package]] name = "trio" -version = "0.25.0" +version = "0.25.1" description = "A friendly Python library for async concurrency and I/O" optional = false python-versions = ">=3.8" files = [ - {file = "trio-0.25.0-py3-none-any.whl", hash = "sha256:e6458efe29cc543e557a91e614e2b51710eba2961669329ce9c862d50c6e8e81"}, - {file = "trio-0.25.0.tar.gz", hash = "sha256:9b41f5993ad2c0e5f62d0acca320ec657fdb6b2a2c22b8c7aed6caf154475c4e"}, + {file = "trio-0.25.1-py3-none-any.whl", hash = "sha256:e42617ba091e7b2e50c899052e83a3c403101841de925187f61e7b7eaebdf3fb"}, + {file = "trio-0.25.1.tar.gz", hash = "sha256:9f5314f014ea3af489e77b001861c535005c3858d38ec46b6b071ebfa339d7fb"}, ] [package.dependencies] @@ -1692,13 +1696,13 @@ wsproto = ">=0.14" [[package]] name = "types-pillow" -version = "10.2.0.20240423" +version = "10.2.0.20240520" description = "Typing stubs for Pillow" optional = false python-versions = ">=3.8" files = [ - {file = "types-Pillow-10.2.0.20240423.tar.gz", hash = "sha256:696e68b9b6a58548fc307a8669830469237c5b11809ddf978ac77fafa79251cd"}, - {file = "types_Pillow-10.2.0.20240423-py3-none-any.whl", hash = "sha256:bd12923093b96c91d523efcdb66967a307f1a843bcfaf2d5a529146c10a9ced3"}, + {file = "types-Pillow-10.2.0.20240520.tar.gz", hash = "sha256:130b979195465fa1e1676d8e81c9c7c30319e8e95b12fae945e8f0d525213107"}, + {file = "types_Pillow-10.2.0.20240520-py3-none-any.whl", hash = "sha256:33c36494b380e2a269bb742181bea5d9b00820367822dbd3760f07210a1da23d"}, ] [[package]] @@ -1761,13 +1765,13 @@ requests = "*" [[package]] name = "werkzeug" -version = "3.0.2" +version = "3.0.3" description = "The comprehensive WSGI web application library." optional = false python-versions = ">=3.8" files = [ - {file = "werkzeug-3.0.2-py3-none-any.whl", hash = "sha256:3aac3f5da756f93030740bc235d3e09449efcf65f2f55e3602e1d851b8f48795"}, - {file = "werkzeug-3.0.2.tar.gz", hash = "sha256:e39b645a6ac92822588e7b39a692e7828724ceae0b0d702ef96701f90e70128d"}, + {file = "werkzeug-3.0.3-py3-none-any.whl", hash = "sha256:fc9645dc43e03e4d630d23143a04a7f947a9a3b5727cd535fdfe155a17cc48c8"}, + {file = "werkzeug-3.0.3.tar.gz", hash = "sha256:097e5bfda9f0aba8da6b8545146def481d06aa7d3266e7448e2cccf67dd8bd18"}, ] [package.dependencies] @@ -1792,20 +1796,20 @@ h11 = ">=0.9.0,<1" [[package]] name = "zipp" -version = "3.18.1" +version = "3.18.2" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.8" files = [ - {file = "zipp-3.18.1-py3-none-any.whl", hash = "sha256:206f5a15f2af3dbaee80769fb7dc6f249695e940acca08dfb2a4769fe61e538b"}, - {file = "zipp-3.18.1.tar.gz", hash = "sha256:2884ed22e7d8961de1c9a05142eb69a247f120291bc0206a00a7642f09b5b715"}, + {file = "zipp-3.18.2-py3-none-any.whl", hash = "sha256:dce197b859eb796242b0622af1b8beb0a722d52aa2f57133ead08edd5bf5374e"}, + {file = "zipp-3.18.2.tar.gz", hash = "sha256:6278d9ddbcfb1f1089a88fde84481528b07b0e10474e09dcfe53dad4069fa059"}, ] [package.extras] docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] +testing = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "6c4bb66c774e93a08e4180f3148942edf91e2b5620ea1f6c3e4c30045bb643e5" +content-hash = "2db221a28d46b210a8fa0b78ad2fa3ac606a52052ad5e02b50d5b07db1e1c5b7" diff --git a/pyproject.toml b/pyproject.toml index bbbc443..74fc46c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,20 +5,11 @@ description = "" authors = ["Brian Bjarke Jensen "] readme = "README.md" packages = [ - { include = "core" }, + { include = "shared" }, ] [tool.poetry.dependencies] python = "^3.12" -gunicorn = "^21.2.0" -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" -pymongo = "^4.6.1" -dash-auth = "^2.2.0" [tool.poetry.group.test.dependencies] @@ -38,6 +29,20 @@ retry = "^0.9.2" torch = "^2.2.1" torchvision = "^0.17.1" + +[tool.poetry.group.shared.dependencies] +pymongo = "^4.7.2" +pydantic = "^2.6.1" +pillow = "^10.2.0" + + +[tool.poetry.group.web_ui.dependencies] +dash = "^2.17.0" +gunicorn = "^21.2.0" +dash-bootstrap-components = "^1.5.0" +dash-mantine-components = "^0.12.1" +dash-auth = "^2.2.0" + [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" diff --git a/core/__init__.py b/shared/__init__.py similarity index 100% rename from core/__init__.py rename to shared/__init__.py diff --git a/core/database/__init__.py b/shared/database/__init__.py similarity index 100% rename from core/database/__init__.py rename to shared/database/__init__.py diff --git a/core/database/classes/__init__.py b/shared/database/classes/__init__.py similarity index 100% rename from core/database/classes/__init__.py rename to shared/database/classes/__init__.py diff --git a/core/database/classes/dataset.py b/shared/database/classes/dataset.py similarity index 100% rename from core/database/classes/dataset.py rename to shared/database/classes/dataset.py diff --git a/core/database/classes/exceptions.py b/shared/database/classes/exceptions.py similarity index 100% rename from core/database/classes/exceptions.py rename to shared/database/classes/exceptions.py diff --git a/core/database/classes/visual_communication.py b/shared/database/classes/visual_communication.py similarity index 98% rename from core/database/classes/visual_communication.py rename to shared/database/classes/visual_communication.py index 78e9e61..f20a0ab 100755 --- a/core/database/classes/visual_communication.py +++ b/shared/database/classes/visual_communication.py @@ -12,7 +12,7 @@ from pydantic import BaseModel from pydantic import field_serializer from pydantic import field_validator -from core.dto import ModelData +from shared.dto import ModelData class VisualCommunication(BaseModel): diff --git a/core/database/utils/__init__.py b/shared/database/utils/__init__.py similarity index 100% rename from core/database/utils/__init__.py rename to shared/database/utils/__init__.py diff --git a/core/database/utils/connect.py b/shared/database/utils/connect.py similarity index 100% rename from core/database/utils/connect.py rename to shared/database/utils/connect.py diff --git a/core/database/utils/count_documents.py b/shared/database/utils/count_documents.py similarity index 100% rename from core/database/utils/count_documents.py rename to shared/database/utils/count_documents.py diff --git a/core/database/utils/get_dataset.py b/shared/database/utils/get_dataset.py similarity index 100% rename from core/database/utils/get_dataset.py rename to shared/database/utils/get_dataset.py diff --git a/core/database/utils/get_visual_communication.py b/shared/database/utils/get_visual_communication.py similarity index 90% rename from core/database/utils/get_visual_communication.py rename to shared/database/utils/get_visual_communication.py index 9b0b043..739d1df 100755 --- a/core/database/utils/get_visual_communication.py +++ b/shared/database/utils/get_visual_communication.py @@ -5,8 +5,8 @@ import logging from pymongo.collection import Collection -from core.database import NoDocumentFoundException -from core.database import VisualCommunication +from shared.database import NoDocumentFoundException +from shared.database import VisualCommunication def get_visual_communication( diff --git a/core/database/utils/list_names.py b/shared/database/utils/list_names.py similarity index 100% rename from core/database/utils/list_names.py rename to shared/database/utils/list_names.py diff --git a/core/database/utils/save_dataset.py b/shared/database/utils/save_dataset.py similarity index 89% rename from core/database/utils/save_dataset.py rename to shared/database/utils/save_dataset.py index afb2a64..f4605ec 100755 --- a/core/database/utils/save_dataset.py +++ b/shared/database/utils/save_dataset.py @@ -4,7 +4,7 @@ import logging from pymongo.collection import Collection -from core.database import Dataset +from shared.database import Dataset def save_dataset( @@ -21,7 +21,7 @@ def save_dataset( if __name__ == '__main__': from dotenv import load_dotenv load_dotenv('local.env') - from core.database import list_names, connect + from shared.database import list_names, connect # connect to database collection, db, client = connect() print(client.server_info()) diff --git a/core/database/utils/upsert_annotation.py b/shared/database/utils/upsert_annotation.py similarity index 94% rename from core/database/utils/upsert_annotation.py rename to shared/database/utils/upsert_annotation.py index 2124d2f..ea7c8ae 100755 --- a/core/database/utils/upsert_annotation.py +++ b/shared/database/utils/upsert_annotation.py @@ -4,7 +4,7 @@ import logging from pymongo.collection import Collection -from core.dto import ModelData +from shared.dto import ModelData def upsert_annotation( diff --git a/core/database/utils/upsert_prediction.py b/shared/database/utils/upsert_prediction.py similarity index 94% rename from core/database/utils/upsert_prediction.py rename to shared/database/utils/upsert_prediction.py index f47d75b..88388d3 100755 --- a/core/database/utils/upsert_prediction.py +++ b/shared/database/utils/upsert_prediction.py @@ -4,7 +4,7 @@ import logging from pymongo.collection import Collection -from core.dto import ModelData +from shared.dto import ModelData def upsert_prediction( diff --git a/core/database/utils/upsert_visual_communication.py b/shared/database/utils/upsert_visual_communication.py similarity index 91% rename from core/database/utils/upsert_visual_communication.py rename to shared/database/utils/upsert_visual_communication.py index 6744e01..30c2e7e 100755 --- a/core/database/utils/upsert_visual_communication.py +++ b/shared/database/utils/upsert_visual_communication.py @@ -2,7 +2,7 @@ from __future__ import annotations from pymongo.collection import Collection -from core.database import VisualCommunication +from shared.database import VisualCommunication def upsert_visual_communication( diff --git a/core/dto/__init__.py b/shared/dto/__init__.py similarity index 100% rename from core/dto/__init__.py rename to shared/dto/__init__.py diff --git a/core/dto/angle.py b/shared/dto/angle.py similarity index 100% rename from core/dto/angle.py rename to shared/dto/angle.py diff --git a/core/dto/contact.py b/shared/dto/contact.py similarity index 100% rename from core/dto/contact.py rename to shared/dto/contact.py diff --git a/core/dto/data_model.py b/shared/dto/data_model.py similarity index 100% rename from core/dto/data_model.py rename to shared/dto/data_model.py diff --git a/core/dto/distance.py b/shared/dto/distance.py similarity index 100% rename from core/dto/distance.py rename to shared/dto/distance.py diff --git a/core/dto/framing.py b/shared/dto/framing.py similarity index 100% rename from core/dto/framing.py rename to shared/dto/framing.py diff --git a/core/dto/information_value.py b/shared/dto/information_value.py similarity index 100% rename from core/dto/information_value.py rename to shared/dto/information_value.py diff --git a/core/dto/modality_color.py b/shared/dto/modality_color.py similarity index 100% rename from core/dto/modality_color.py rename to shared/dto/modality_color.py diff --git a/core/dto/modality_depth.py b/shared/dto/modality_depth.py similarity index 100% rename from core/dto/modality_depth.py rename to shared/dto/modality_depth.py diff --git a/core/dto/modality_lighting.py b/shared/dto/modality_lighting.py similarity index 100% rename from core/dto/modality_lighting.py rename to shared/dto/modality_lighting.py diff --git a/core/dto/model_data.py b/shared/dto/model_data.py similarity index 100% rename from core/dto/model_data.py rename to shared/dto/model_data.py diff --git a/core/dto/point_of_view.py b/shared/dto/point_of_view.py similarity index 100% rename from core/dto/point_of_view.py rename to shared/dto/point_of_view.py diff --git a/core/dto/salience.py b/shared/dto/salience.py similarity index 100% rename from core/dto/salience.py rename to shared/dto/salience.py diff --git a/core/dto/visual_syntax.py b/shared/dto/visual_syntax.py similarity index 100% rename from core/dto/visual_syntax.py rename to shared/dto/visual_syntax.py diff --git a/shared/utils/__init__.py b/shared/utils/__init__.py new file mode 100644 index 0000000..fb7c48b --- /dev/null +++ b/shared/utils/__init__.py @@ -0,0 +1,4 @@ +from __future__ import annotations + +from .check_env import check_env +from .setup_logging import setup_logging diff --git a/shared/utils/check_env.py b/shared/utils/check_env.py new file mode 100644 index 0000000..0fb3a2f --- /dev/null +++ b/shared/utils/check_env.py @@ -0,0 +1,24 @@ +"""Definition of check_env function.""" +from __future__ import annotations + +import os + + +def check_env() -> None: + """Check necessary environment variables are set.""" + necesasary_var_list = { + 'MONGO_HOST', + 'MONGO_DB', + 'MONGO_COLLECTION', + 'MONGO_USER', + 'MONGO_PASSWORD', + 'DASH_AUTH_USERNAME', + 'DASH_AUTH_PASSWORD', + } + for env_var in necesasary_var_list: + # ensure env var set + assert ( + env_var in os.environ + ), ( + f"environment variable not set: {env_var}" + ) diff --git a/shared/utils/setup_logging.py b/shared/utils/setup_logging.py new file mode 100644 index 0000000..aa4ca14 --- /dev/null +++ b/shared/utils/setup_logging.py @@ -0,0 +1,21 @@ +"""Definition of setup_logging function.""" +from __future__ import annotations + +import logging +import os + + +def setup_logging() -> None: + """Setup logging.""" + requested_log_level = os.getenv('LOG_LEVEL', default='info') + level = getattr(logging, requested_log_level.upper()) + fmt = ( + '%(asctime)s | ' + '%(levelname)s | ' + '%(filename)s | ' + '%(funcName)s | ' + '%(message)s' + ) + datefmt = '%Y-%m-%d %H:%M:%S' + logging.basicConfig(format=fmt, datefmt=datefmt, level=level) + logging.debug('finished') diff --git a/tests/generate_random_prediction_test.py b/tests/generate_random_prediction_test.py index c97e54c..2fccdd5 100644 --- a/tests/generate_random_prediction_test.py +++ b/tests/generate_random_prediction_test.py @@ -2,7 +2,7 @@ from __future__ import annotations from pathlib import Path -from core.database.classes import VisualCommunication +from shared.database.classes import VisualCommunication if __name__ == '__main__': diff --git a/tests/get_visual_communication_test.py b/tests/get_visual_communication_test.py index 6cd29cc..ed1ad5e 100644 --- a/tests/get_visual_communication_test.py +++ b/tests/get_visual_communication_test.py @@ -5,8 +5,8 @@ from pathlib import Path from dotenv import load_dotenv -from core.database import connect -from core.database import get_visual_communication +from shared.database import connect +from shared.database import get_visual_communication if __name__ == '__main__': # prepare env vars diff --git a/tests/image_download_test.py b/tests/image_download_test.py index 8f94244..7020013 100644 --- a/tests/image_download_test.py +++ b/tests/image_download_test.py @@ -5,8 +5,8 @@ from pathlib import Path from dotenv import load_dotenv -from core.database import connect -from core.database.classes import VisualCommunication +from shared.database import connect +from shared.database.classes import VisualCommunication if __name__ == '__main__': # prepare env vars diff --git a/tests/image_upload_test.py b/tests/image_upload_test.py index 4a99c71..916db40 100644 --- a/tests/image_upload_test.py +++ b/tests/image_upload_test.py @@ -6,8 +6,8 @@ from pathlib import Path from dotenv import load_dotenv from pymongo.errors import DuplicateKeyError -from core.database import connect -from core.database.classes import VisualCommunication +from shared.database import connect +from shared.database.classes import VisualCommunication if __name__ == '__main__': # get list of image paths diff --git a/tests/image_upload_to_server_test.py b/tests/image_upload_to_server_test.py index 5db75e2..9ecc79b 100644 --- a/tests/image_upload_to_server_test.py +++ b/tests/image_upload_to_server_test.py @@ -5,8 +5,8 @@ from pathlib import Path from dotenv import load_dotenv from pymongo.errors import DuplicateKeyError -from core.database import connect -from core.database import VisualCommunication +from shared.database import connect +from shared.database import VisualCommunication if __name__ == '__main__': # get list of image paths diff --git a/tests/model_outputs_from_annotation_test.py b/tests/model_outputs_from_annotation_test.py index fa4a96e..c8181d9 100644 --- a/tests/model_outputs_from_annotation_test.py +++ b/tests/model_outputs_from_annotation_test.py @@ -1,6 +1,6 @@ from __future__ import annotations -from core.dto import ModelData +from shared.dto import ModelData if __name__ == '__main__': diff --git a/tests/prediction_upload_test.py b/tests/prediction_upload_test.py index 7d8987b..bf667f3 100644 --- a/tests/prediction_upload_test.py +++ b/tests/prediction_upload_test.py @@ -6,9 +6,9 @@ from pathlib import Path from dotenv import load_dotenv -from core.database import connect -from core.database import upsert_prediction -from core.database.classes import VisualCommunication +from shared.database import connect +from shared.database import upsert_prediction +from shared.database.classes import VisualCommunication if __name__ == '__main__': # setup logging diff --git a/tests/total_annotated_test.py b/tests/total_annotated_test.py index 0fe3717..8c94f27 100644 --- a/tests/total_annotated_test.py +++ b/tests/total_annotated_test.py @@ -5,8 +5,8 @@ from pathlib import Path from dotenv import load_dotenv -from core.database import connect -from core.database import count_documents +from shared.database import connect +from shared.database import count_documents if __name__ == '__main__': # prepare env vars diff --git a/tests/total_documents_test.py b/tests/total_documents_test.py index d7955fb..0242940 100644 --- a/tests/total_documents_test.py +++ b/tests/total_documents_test.py @@ -5,8 +5,8 @@ from pathlib import Path from dotenv import load_dotenv -from core.database import connect -from core.database import count_documents +from shared.database import connect +from shared.database import count_documents if __name__ == '__main__': # prepare env vars diff --git a/web_ui/Dockerfile b/web_ui/Dockerfile index 336607e..d83830c 100644 --- a/web_ui/Dockerfile +++ b/web_ui/Dockerfile @@ -30,7 +30,8 @@ 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 --without model +RUN --mount=type=cache,target=${POETRY_CACHE_DIR} poetry install \ + --with shared,web_ui # final stage FROM python:3.12-slim-bookworm @@ -48,7 +49,7 @@ RUN mkdir -p /home/app && \ # add code while changing ownership WORKDIR $APP_HOME -COPY --chown=app:app ./core ./core +COPY --chown=app:app ./shared ./shared COPY --chown=app:app ./web_ui/src ./src # change to the app user diff --git a/web_ui/docker-compose.server.yml b/web_ui/docker-compose.server.yml new file mode 100644 index 0000000..ffd8784 --- /dev/null +++ b/web_ui/docker-compose.server.yml @@ -0,0 +1,13 @@ +services: + web: + image: visual_critical_discourse_analysis:test + container_name: visual_critical_discourse_analysis_alone + build: + context: ../ + dockerfile: ./web_ui/Dockerfile + ports: + - 8050:8050 + env_file: + - ../server.env + environment: + - ENV=TEST diff --git a/web_ui/src/app/__init__.py b/web_ui/src/app/__init__.py index 238bf79..7e45697 100644 --- a/web_ui/src/app/__init__.py +++ b/web_ui/src/app/__init__.py @@ -1,3 +1,3 @@ from __future__ import annotations -from .app import app +from .init_app import init_app diff --git a/web_ui/src/app/app.py b/web_ui/src/app/app.py deleted file mode 100644 index c1d5083..0000000 --- a/web_ui/src/app/app.py +++ /dev/null @@ -1,251 +0,0 @@ -from __future__ import annotations - -import logging -import os - -import dash_bootstrap_components as dbc -from dash import ALL -from dash import Dash -from dash import Input -from dash import Output -from dash import State -from dash_auth import BasicAuth -from pydantic import ValidationError - -from .layout import app_layout -from core.database import connect -from core.database import count_documents -from core.database import get_visual_communication -from core.database import NoDocumentFoundException -from core.database import upsert_annotation -from core.database import upsert_visual_communication -from core.database import VisualCommunication -from core.dto import ModelData - -# setup app -app = Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP]) -app.title = 'visual critical discourse analysis'.title() -app.layout = app_layout -server = app.server - -# setup authentication -AUTH_DICT = { - os.getenv('DASH_AUTH_USERNAME'): os.getenv('DASH_AUTH_PASSWORD'), -} -BasicAuth(app, AUTH_DICT) - -# connect to database -collection, db, client = connect() - - -# define callbacks -@app.callback( - Output('alert-element', 'is_open'), - Output('alert-element', 'children'), - Input('alert-message', 'data'), -) -def show_alert( - msg: str | None, -): - if msg is None or msg == '': - return False, '' - logging.info(f'updated alert message: {msg}') - return True, msg - - -@app.callback( - Output('success-element', 'is_open'), - Output('success-element', 'children'), - Input('success-message', 'data'), -) -def show_success( - msg: str | None, -): - if msg is None or msg == '': - return False, '' - logging.info(f"updated success message: {msg}") - return True, msg - - -@app.callback( - Output('progress-bar', 'value'), - Output('progress-bar', 'max'), - Output('progress-bar', 'label'), - Input('next-button', 'n_clicks'), - Input('upload-data', 'filename'), -) -def update_progress_bar( - n_clicks: int, - filename_list: list[str] | None, -): - logging.info('began updating progress bar') - global collection - # get values from database - num_total = count_documents( - collection=collection, - only_with_annotation=False, - ) - num_handled = count_documents( - collection=collection, - only_with_annotation=True, - ) - limit = int(num_total/20) - label_str = f"{num_handled}/{num_total}" if num_handled >= limit else '' - return num_handled, num_total, label_str - - -@app.callback( - Output('success-message', 'data', allow_duplicate=True), - Output('alert-message', 'data', allow_duplicate=True), - Input('upload-data', 'contents'), - State('upload-data', 'filename'), - prevent_initial_call=True, -) -def upload_images( - content_list: list[str] | None, - filename_list: list[str] | None, -): - logging.info('began uploading images') - global collection - # stop early if possible - if content_list is None or filename_list is None: - logging.warning('content_list is None -> nothing to upload') - return None, 'nothing selected to upload'.title() - # build list of visual communication - visual_communication_list = [] - for content, filename in zip(content_list, filename_list): - try: - image = VisualCommunication.decode_image(content) - vis_com = VisualCommunication( - name=filename, - image=image, - ) - except Exception as exc: - logging.warning( - ( - 'failed creating VisualCommunication object ' - f"from file {filename}" - ), - exc, - ) - else: - visual_communication_list.append(vis_com) - # upsert documents - success = upsert_visual_communication( - collection=collection, - visual_communication_list=visual_communication_list, - ) - if success: - logging.info(f"succesfully uploaded {len(filename_list)} images") - return 'successfully uploaded images'.title(), None - logging.warning('failed inserting images into database') - return None, 'failed uploading images'.title() - - -@app.callback( - Output('alert-message', 'data', allow_duplicate=True), - Output('vis-com-name', 'data'), - Output('image-container', 'src'), - Output({'type': 'annotation', 'index': ALL}, 'value'), - Input('next-button', 'n_clicks'), - State('vis-com-name', 'data'), - State('image-container', 'src'), - State({'type': 'annotation', 'index': ALL}, 'id'), - State({'type': 'annotation', 'index': ALL}, 'value'), - prevent_initial_call=True, -) -def cycle_visual_communication_data( - n_clicks: int, - vis_com_name: str, - image_src: str, - annotation_keys: list, - annotation_values: list, -): - logging.info('began cycling visual communication data') - global collection - # prepare default response - response = [ - '', - vis_com_name, - image_src, - annotation_values, - ] - # check if next-button clicked - if n_clicks == 0: - logging.info('stopping early: next-button has not yet been clicked') - return response - # check if visual communication name is set - if len(vis_com_name) > 0: - logging.info('saving annotations to database: %s', vis_com_name) - try: - # extract option keys - annotation_keys = [ - elem['index'] - for elem in annotation_keys - ] - # ensure all options are set - logging.info(annotation_keys) - for option, value in zip(annotation_keys, annotation_values): - if value is None: - raise ValueError(f"{option} is not set") - # prepare data to save - annotation_keys = [ - elem.replace(' ', '_') - for elem - in annotation_keys - ] - annotation_values = [ - elem.replace(' ', '_').lower() - for elem - in annotation_values - ] - annotation_map = { - key: value - for key, value - in zip(annotation_keys, annotation_values) - } - # instantiate ModelOutputs object - annotations = ModelData.from_annotations(**annotation_map) - # save data to database - upsert_annotation( - collection=collection, - vis_com_name=vis_com_name, - annotations=annotations, - ) - except (ValueError, ValidationError) as exc: - msg = f"failed saving annotation: {exc}" - logging.warning(msg) - response[0] = msg - return tuple(response) - # get new visual communication - logging.info('trying to get new visual communication') - try: - # get data - vis_com = get_visual_communication( - collection=collection, - with_annotation=False, - ) - # set variables - vis_com_name = vis_com.name - image_src = vis_com.webencoded_image() - if vis_com.prediction is not None: - # TODO: update to use optional predictions - pass - else: - # reset annotations - annotation_values = [None for elem in annotation_values] - except NoDocumentFoundException: - msg = 'no unannotated data in database' - logging.warning(msg) - response[0] = msg - return tuple(response) - else: - response[1] = vis_com_name - response[2] = image_src - response[3] = annotation_values - logging.info('finished getting visual communication: %s', vis_com_name) - return tuple(response) - - -if __name__ == '__main__': - app.run(debug=True) diff --git a/web_ui/src/app/init_app.py b/web_ui/src/app/init_app.py new file mode 100644 index 0000000..6d17e14 --- /dev/null +++ b/web_ui/src/app/init_app.py @@ -0,0 +1,260 @@ +"""Definition of init_app function.""" +from __future__ import annotations + +import logging +import os + +import dash_bootstrap_components as dbc +from dash import ALL +from dash import Dash +from dash import Input +from dash import Output +from dash import State +from dash_auth import BasicAuth +from pydantic import ValidationError +from pymongo.collection import Collection + +from .layout import app_layout +from shared.database import count_documents +from shared.database import get_visual_communication +from shared.database import NoDocumentFoundException +from shared.database import upsert_annotation +from shared.database import upsert_visual_communication +from shared.database import VisualCommunication +from shared.dto import ModelData + + +def init_app( + collection: Collection, +) -> Dash: + """Initialise web UI application.""" + # setup app + app = Dash( + name='visual_critical_discourse_analysis_web_ui', + external_stylesheets=[ + dbc.themes.BOOTSTRAP, + ], + ) + app.title = 'visual critical discourse analysis'.title() + app.layout = app_layout + + # setup authentication + auth_dict = { + os.getenv('DASH_AUTH_USERNAME'): os.getenv('DASH_AUTH_PASSWORD'), + } + BasicAuth(app, auth_dict) + + # define callback: show warning + @app.callback( + Output('warning-element', 'is_open'), + Output('warning-element', 'children'), + Input('warning-message', 'data'), + ) + def show_warning( + msg: str | None, + ) -> tuple[bool, str]: + """Show warning message in web ui.""" + if msg is None or msg == '': + return False, '' + logging.debug('updated warning message: %s', msg) + return True, msg + + # define callback: show info + @app.callback( + Output('info-element', 'is_open'), + Output('info-element', 'children'), + Input('info-message', 'data'), + ) + def show_info( + msg: str | None, + ) -> tuple[bool, str]: + """Show info message in web ui.""" + if msg is None or msg == '': + return False, '' + logging.debug('updated info message: %s', msg) + return True, msg + + # define callback: update progress bar + @app.callback( + Output('progress-bar', 'value'), + Output('progress-bar', 'max'), + Output('progress-bar', 'label'), + Input('next-button', 'n_clicks'), + Input('upload-data', 'filename'), + ) + def update_progress_bar( + n_clicks: int, + filename_list: list[str] | None, + ) -> tuple[int, int, str]: + """Update progress bar in web ui.""" + assert isinstance(n_clicks, int) + if filename_list is not None: + assert isinstance(filename_list, list) + # get values from database + num_total = count_documents( + collection=collection, + only_with_annotation=False, + ) + num_handled = count_documents( + collection=collection, + only_with_annotation=True, + ) + limit = int(num_total/20) + label_str = f"{ + num_handled + }/{num_total}" if num_handled >= limit else '' + return num_handled, num_total, label_str + + # define callback: upload image + @app.callback( + Output('info-message', 'data', allow_duplicate=True), + Output('warning-message', 'data', allow_duplicate=True), + Input('upload-data', 'contents'), + State('upload-data', 'filename'), + prevent_initial_call=True, + ) + def upload_images( + content_list: list[str] | None, + filename_list: list[str] | None, + ) -> tuple[str | None, str | None]: + """Upload image to database through web ui.""" + # stop early if possible + if content_list is None or filename_list is None: + logging.info('nothing to upload.') + return None, 'nothing to upload'.title() + # build list of visual communication + vis_com_list = [] + for content, filename in zip(content_list, filename_list): + try: + image = VisualCommunication.decode_image(content=content) + vis_com = VisualCommunication( + name=filename, + image=image, + ) + except Exception as exc: + logging.error('failed handling data from file: %s', filename) + logging.debug(exc) + continue + vis_com_list.append(vis_com) + # upsert documents + success = upsert_visual_communication( + collection=collection, + visual_communication_list=vis_com_list, + ) + if success: + logging.info('uploaded %s files to database', len(vis_com_list)) + return 'successfully uploaded images'.title(), None + logging.error('failed uploading images to database') + return None, 'failed uploading images'.title() + + # define callback: cycle visual communication data + @app.callback( + Output('warning-message', 'data', allow_duplicate=True), + Output('vis-com-name', 'data'), + Output('image-container', 'src'), + Output({'type': 'annotation', 'index': ALL}, 'value'), + Input('next-button', 'n_clicks'), + State('vis-com-name', 'data'), + State('image-container', 'src'), + State({'type': 'annotation', 'index': ALL}, 'id'), + State({'type': 'annotation', 'index': ALL}, 'value'), + prevent_initial_call=True, + ) + def cycle_vis_com_data( + n_clicks: int, + vis_com_name: str, + image_src: str, + annotation_keys: list, + annotation_values: list, + ) -> tuple: + """Cycle visual communcation data.""" + assert isinstance(n_clicks, int) + assert isinstance(vis_com_name, str) + assert isinstance(image_src, str) + assert isinstance(annotation_keys, list) + assert isinstance(annotation_values, list) + # prepare default response + response = [ + '', + vis_com_name, + image_src, + annotation_values, + ] + # stop early if possible + if n_clicks == 0: + return tuple(response) + # check if visual communication name is set + if len(vis_com_name) > 0: + logging.info('saving annotations to database: %s', vis_com_name) + try: + # extract option keys + annotation_keys = [ + elem['index'] + for elem in annotation_keys + ] + # ensure all options are set + logging.info(annotation_keys) + for option, value in zip(annotation_keys, annotation_values): + if value is None: + raise ValueError(f"{option} is not set") + # prepare data to save + annotation_keys = [ + elem.replace(' ', '_') + for elem + in annotation_keys + ] + annotation_values = [ + elem.replace(' ', '_').lower() + for elem + in annotation_values + ] + annotation_map = { + key: value + for key, value + in zip(annotation_keys, annotation_values) + } + # instantiate ModelOutputs object + annotations = ModelData.from_annotations(**annotation_map) + # save data to database + upsert_annotation( + collection=collection, + vis_com_name=vis_com_name, + annotations=annotations, + ) + except (ValueError, ValidationError) as exc: + msg = f"failed saving annotation: {exc}" + logging.warning(msg) + response[0] = msg + return tuple(response) + # get new visual communication + logging.info('trying to get new visual communication') + try: + # get data + vis_com = get_visual_communication( + collection=collection, + with_annotation=False, + ) + # set variables + vis_com_name = vis_com.name + image_src = vis_com.webencoded_image() + if vis_com.prediction is not None: + # TODO: update to use optional predictions + pass + else: + # reset annotations + annotation_values = [None for elem in annotation_values] + except NoDocumentFoundException: + msg = 'no unannotated data in database' + logging.warning(msg) + response[0] = msg + return tuple(response) + else: + response[1] = vis_com_name + response[2] = image_src + response[3] = annotation_values + logging.info( + 'finished getting visual communication: %s', vis_com_name, + ) + return tuple(response) + logging.info('initialised app') + return app diff --git a/web_ui/src/app/layout/labels.py b/web_ui/src/app/layout/labels.py index 510b70f..c87f4a0 100644 --- a/web_ui/src/app/layout/labels.py +++ b/web_ui/src/app/layout/labels.py @@ -4,17 +4,17 @@ import dash_mantine_components as dmc from dash import dcc from dash import html -from core.dto import AngleData -from core.dto import ContactData -from core.dto import DistanceData -from core.dto import FramingData -from core.dto import InformationValueData -from core.dto import ModalityColorData -from core.dto import ModalityDepthData -from core.dto import ModalityLightingData -from core.dto import PointOfViewData -from core.dto import SalienceData -from core.dto import VisualSyntaxData +from shared.dto import AngleData +from shared.dto import ContactData +from shared.dto import DistanceData +from shared.dto import FramingData +from shared.dto import InformationValueData +from shared.dto import ModalityColorData +from shared.dto import ModalityDepthData +from shared.dto import ModalityLightingData +from shared.dto import PointOfViewData +from shared.dto import SalienceData +from shared.dto import VisualSyntaxData def generate_visual_syntax_options_map(): diff --git a/web_ui/src/app/layout/stores.py b/web_ui/src/app/layout/stores.py index 11c59da..e80a43d 100644 --- a/web_ui/src/app/layout/stores.py +++ b/web_ui/src/app/layout/stores.py @@ -6,8 +6,8 @@ from dash import html stores_element = html.Div( children=[ - dcc.Store(id='alert-message', data=''), - dcc.Store(id='success-message', data=''), + dcc.Store(id='warning-message', data=''), + dcc.Store(id='info-message', data=''), dcc.Store(id='vis-com-name', data=''), ], ) diff --git a/web_ui/src/main.py b/web_ui/src/main.py index 5beed84..c22b45d 100644 --- a/web_ui/src/main.py +++ b/web_ui/src/main.py @@ -1,53 +1,23 @@ +"""Definition of web_ui main script.""" from __future__ import annotations -import logging import os -from pathlib import Path -from dotenv import load_dotenv - -from .app import app - -# prepare optional local setup -env_path = Path(__file__).parent.parent.parent / 'local.env' -load_dotenv(env_path) +from .app import init_app +from shared.database import connect +from shared.utils import check_env +from shared.utils import setup_logging # ensure env vars set -necesasary_var_list = { - 'MONGO_HOST', - 'MONGO_DB', - 'MONGO_COLLECTION', - 'MONGO_USER', - 'MONGO_PASSWORD', - 'DASH_AUTH_USERNAME', - 'DASH_AUTH_PASSWORD', -} -for env_var in necesasary_var_list: - # ensure env var set - assert ( - env_var in os.environ - ), ( - f"environment variable not set: {env_var}" - ) +check_env() # setup logging stream handler -FMT = ( - '%(asctime)s | ' - '%(levelname)s | ' - '%(filename)s | ' - '%(funcName)s | ' - '%(message)s' -) -DATEFMT = '%Y-%m-%d %H:%M:%S' -logging.basicConfig(format=FMT, datefmt=DATEFMT, level=logging.INFO) +setup_logging() -logging.info('initialized app') +# connect to database +collection, db, client = connect() + +# initialise application +app = init_app(collection=collection) server = app.server server.config.update(SECRET_KEY=os.urandom(24)) - -if __name__ == '__main__': - # prepare local env vars - os.environ['MONGO_HOST'] = 'localhost' - # run app - app.run(debug=True) - logging.info('started app') -- 2.54.0 From 4a6d0dbe0920aea4c387a0eb3e6b2938f15e219c Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Mon, 20 May 2024 20:19:40 +0200 Subject: [PATCH 08/17] updated workflows --- .gitea/workflows/default.yaml | 2 -- .gitea/workflows/publish.yaml | 5 ++--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/.gitea/workflows/default.yaml b/.gitea/workflows/default.yaml index 24eb2f1..ce5f83a 100644 --- a/.gitea/workflows/default.yaml +++ b/.gitea/workflows/default.yaml @@ -1,8 +1,6 @@ name: Code Quality Pipeline run-name: ${{ gitea.actor }} is running the Code Quality Pipeline -runs-on: ubuntu-latest on: push -image: python:3.12 jobs: test: name: Test diff --git a/.gitea/workflows/publish.yaml b/.gitea/workflows/publish.yaml index fcbf00d..b6241ff 100644 --- a/.gitea/workflows/publish.yaml +++ b/.gitea/workflows/publish.yaml @@ -1,6 +1,5 @@ name: CI Pipeline run-name: ${{ gitea.actor }} is running the CI Pipeline -runs-on: ubuntu-latest on: pull_request: branches: @@ -23,10 +22,10 @@ jobs: poetry install - name: PEP8 Check run: | - poetry run flake8 ./src --benchmark + poetry run flake8 . --benchmark - name: Type Check run: | - poetry run mypy ./src --disable-error-code=import-untyped + poetry run mypy . --disable-error-code=import-untyped publish: name: Build and Publish runs-on: ubuntu-latest -- 2.54.0 From 8724bda2f1f1b8de5c2d6463bba74b253af14b53 Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Mon, 20 May 2024 20:38:03 +0200 Subject: [PATCH 09/17] updated publishing workflow --- .gitea/workflows/publish.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitea/workflows/publish.yaml b/.gitea/workflows/publish.yaml index b6241ff..035b2aa 100644 --- a/.gitea/workflows/publish.yaml +++ b/.gitea/workflows/publish.yaml @@ -44,8 +44,8 @@ jobs: - name: Build and Push Image uses: docker/build-push-action@v2 with: - context: . + context: ./web_ui push: true tags: | - ${{ vars.docker_repo_url }}/${{ gitea.repository }}:${{ env.sha_short }} - ${{ vars.docker_repo_url }}/${{ gitea.repository }}:latest + ${{ vars.docker_repo_url }}/${{ gitea.repository }}/web_ui:${{ env.sha_short }} + ${{ vars.docker_repo_url }}/${{ gitea.repository }}/web_ui:latest -- 2.54.0 From 291407dfac3fd2c5804c508a15859382f86252e6 Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Mon, 20 May 2024 20:56:10 +0200 Subject: [PATCH 10/17] updated dockerfile path --- .gitea/workflows/publish.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/publish.yaml b/.gitea/workflows/publish.yaml index 035b2aa..50bb012 100644 --- a/.gitea/workflows/publish.yaml +++ b/.gitea/workflows/publish.yaml @@ -42,9 +42,10 @@ jobs: echo "REPOSITORY: ${{ gitea.repository }}" echo "COMMIT_SHA: ${{ env.sha_short }}" - name: Build and Push Image - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v4 with: - context: ./web_ui + context: . + dockerfile: ./web_ui/Dockerfile push: true tags: | ${{ vars.docker_repo_url }}/${{ gitea.repository }}/web_ui:${{ env.sha_short }} -- 2.54.0 From 75e61bf05c6c674779f71f72ed671cc2bc705fbe Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Mon, 20 May 2024 21:41:40 +0200 Subject: [PATCH 11/17] updated relative path to dockerfile --- web_ui/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web_ui/Dockerfile b/web_ui/Dockerfile index d83830c..759db09 100644 --- a/web_ui/Dockerfile +++ b/web_ui/Dockerfile @@ -49,8 +49,8 @@ RUN mkdir -p /home/app && \ # add code while changing ownership WORKDIR $APP_HOME -COPY --chown=app:app ./shared ./shared -COPY --chown=app:app ./web_ui/src ./src +COPY --chown=app:app ../shared ./shared +COPY --chown=app:app ../web_ui/src ./src # change to the app user USER app -- 2.54.0 From 7d723285c8a08efdc7cd791b28ba70cd3efbfba6 Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Mon, 20 May 2024 21:56:49 +0200 Subject: [PATCH 12/17] updated workflow --- .gitea/workflows/publish.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/publish.yaml b/.gitea/workflows/publish.yaml index 50bb012..68dfe15 100644 --- a/.gitea/workflows/publish.yaml +++ b/.gitea/workflows/publish.yaml @@ -45,7 +45,7 @@ jobs: uses: docker/build-push-action@v4 with: context: . - dockerfile: ./web_ui/Dockerfile + dockerfile: ./web_ui/ push: true tags: | ${{ vars.docker_repo_url }}/${{ gitea.repository }}/web_ui:${{ env.sha_short }} -- 2.54.0 From a500e8a687da69c7a79fd7953f8a30c8f36a348f Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Mon, 20 May 2024 22:10:54 +0200 Subject: [PATCH 13/17] fixed ci --- .gitea/workflows/publish.yaml | 3 +- Dockerfile | 65 ----------------------------------- 2 files changed, 1 insertion(+), 67 deletions(-) delete mode 100644 Dockerfile diff --git a/.gitea/workflows/publish.yaml b/.gitea/workflows/publish.yaml index 68dfe15..aa8d19a 100644 --- a/.gitea/workflows/publish.yaml +++ b/.gitea/workflows/publish.yaml @@ -44,8 +44,7 @@ jobs: - name: Build and Push Image uses: docker/build-push-action@v4 with: - context: . - dockerfile: ./web_ui/ + context: ./web_ui/ push: true tags: | ${{ vars.docker_repo_url }}/${{ gitea.repository }}/web_ui:${{ env.sha_short }} diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index d481a47..0000000 --- a/Dockerfile +++ /dev/null @@ -1,65 +0,0 @@ -########### -# BUILDER # -########### - -# pull official base image -FROM python:3.12-slim-bullseye as BUILDER - -# set environment variables -ENV PYTHONUNBUFFERED=1 \ - PYTHONDONTWRITEBYTECODE=1 \ - PIP_NO_CACHE_DIR=off \ - PIP_DISABLE_PIP_VERSION_CHECK=ON \ - PIP_DEFAULT_TIMEOUT=100 \ - DEBIAN_FRONTEND=noninteractive \ - POETRY_HOME=/etc/poetry \ - POETRY_VERSION=1.7.1 \ - POETRY_VIRTUALENVS_IN_PROJECT=1 \ - POETRY_VIRTUALENVS_CREATE=1 \ - POETRY_NO_INTERACTION=1 \ - POETRY_CACHE_DIR=/tmp/poetry_cache \ - APP_HOME=/home/app - -# update system -RUN apt-get update \ - && apt-get install -y --no-install-recommends \ - build-essential \ - curl \ - && apt-get clean - -# install poetry -RUN curl -sSL https://install.python-poetry.org | python3 - -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 - -######### -# FINAL # -######### - -# pull official base image -FROM python:3.12-slim-bullseye - -# copy virtualenv made by poetry -ENV APP_HOME=/home/app \ - VIRTUAL_ENV=/home/app/.venv -COPY --from=builder ${VIRTUAL_ENV} ${VIRTUAL_ENV} -ENV PATH="${VIRTUAL_ENV}/bin:${PATH}" - -# create home directory and app user -RUN mkdir -p /home/app && \ - addgroup --system app && \ - adduser --system --group app - -# add code while changing ownership -WORKDIR $APP_HOME -COPY --chown=app:app ./src ./src -COPY --chown=app:app ./core ./core - -# change to the app user -USER app - -ENTRYPOINT [ "gunicorn", "src.main:server", "-b", "0.0.0.0:8050" ] -- 2.54.0 From 8db61b2b8a88c9a34ef66a45ecefa65ab3a13fa9 Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Mon, 20 May 2024 22:25:09 +0200 Subject: [PATCH 14/17] more ci --- web_ui/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web_ui/Dockerfile b/web_ui/Dockerfile index 759db09..d83830c 100644 --- a/web_ui/Dockerfile +++ b/web_ui/Dockerfile @@ -49,8 +49,8 @@ RUN mkdir -p /home/app && \ # add code while changing ownership WORKDIR $APP_HOME -COPY --chown=app:app ../shared ./shared -COPY --chown=app:app ../web_ui/src ./src +COPY --chown=app:app ./shared ./shared +COPY --chown=app:app ./web_ui/src ./src # change to the app user USER app -- 2.54.0 From 8c5b58ebf3a0f52227e6b61aa9770b8c54a14e88 Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Tue, 21 May 2024 08:11:41 +0200 Subject: [PATCH 15/17] added new dockerfile in root dir --- Dockerfile.web_ui | 58 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 Dockerfile.web_ui diff --git a/Dockerfile.web_ui b/Dockerfile.web_ui new file mode 100644 index 0000000..1cb4942 --- /dev/null +++ b/Dockerfile.web_ui @@ -0,0 +1,58 @@ +# build stage +FROM python:3.12-slim-bookworm as BUILDER + +# set environment variables +ENV PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + PIP_NO_CACHE_DIR=off \ + PIP_DISABLE_PIP_VERSION_CHECK=ON \ + PIP_DEFAULT_TIMEOUT=100 \ + DEBIAN_FRONTEND=noninteractive \ + POETRY_HOME=/etc/poetry \ + POETRY_VERSION=1.7.1 \ + POETRY_VIRTUALENVS_IN_PROJECT=1 \ + POETRY_VIRTUALENVS_CREATE=1 \ + POETRY_NO_INTERACTION=1 \ + POETRY_CACHE_DIR=/tmp/poetry_cache \ + APP_HOME=/home/app + +# update system +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + build-essential \ + curl \ + && apt-get clean + +# install poetry +RUN curl -sSL https://install.python-poetry.org | python3 - +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 \ + --with shared,web_ui + +# final stage +FROM python:3.12-slim-bookworm + +# copy virtualenv made by poetry +ENV APP_HOME=/home/app \ + VIRTUAL_ENV=/home/app/.venv +COPY --from=builder ${VIRTUAL_ENV} ${VIRTUAL_ENV} +ENV PATH="${VIRTUAL_ENV}/bin:${PATH}" + +# create home directory and app user +RUN mkdir -p /home/app && \ + addgroup --system app && \ + adduser --system --group app + +# add code while changing ownership +WORKDIR $APP_HOME +COPY --chown=app:app ./shared ./shared +COPY --chown=app:app ./web_ui/src ./src + +# change to the app user +USER app + +ENTRYPOINT [ "gunicorn", "src.main:server", "-b", "0.0.0.0:8050" ] -- 2.54.0 From 1f4f9949fe761ed8a7e1b1a92463977d92789804 Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Tue, 21 May 2024 08:12:00 +0200 Subject: [PATCH 16/17] updated compose to use new dockerfile in root dir --- web_ui/Dockerfile | 6 +++--- web_ui/docker-compose.server.yml | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/web_ui/Dockerfile b/web_ui/Dockerfile index d83830c..884386a 100644 --- a/web_ui/Dockerfile +++ b/web_ui/Dockerfile @@ -29,7 +29,7 @@ ENV PATH="${POETRY_HOME}/bin:$PATH" # install runtime dependencies WORKDIR ${APP_HOME} -COPY poetry.lock pyproject.toml ./ +COPY ../poetry.lock ../pyproject.toml ./ RUN --mount=type=cache,target=${POETRY_CACHE_DIR} poetry install \ --with shared,web_ui @@ -49,8 +49,8 @@ RUN mkdir -p /home/app && \ # add code while changing ownership WORKDIR $APP_HOME -COPY --chown=app:app ./shared ./shared -COPY --chown=app:app ./web_ui/src ./src +COPY --chown=app:app ../shared ./shared +COPY --chown=app:app ./src ./src # change to the app user USER app diff --git a/web_ui/docker-compose.server.yml b/web_ui/docker-compose.server.yml index ffd8784..d452ffc 100644 --- a/web_ui/docker-compose.server.yml +++ b/web_ui/docker-compose.server.yml @@ -1,10 +1,10 @@ services: - web: + web_ui: image: visual_critical_discourse_analysis:test container_name: visual_critical_discourse_analysis_alone build: context: ../ - dockerfile: ./web_ui/Dockerfile + dockerfile: Dockerfile.web_ui ports: - 8050:8050 env_file: -- 2.54.0 From c6f4c81dddbbfd1de6e3cb3f099cf1784ff032e2 Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Tue, 21 May 2024 08:13:07 +0200 Subject: [PATCH 17/17] updated to use new dockerfile in root dir --- .gitea/workflows/publish.yaml | 36 ++++++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/.gitea/workflows/publish.yaml b/.gitea/workflows/publish.yaml index aa8d19a..b6521d8 100644 --- a/.gitea/workflows/publish.yaml +++ b/.gitea/workflows/publish.yaml @@ -30,22 +30,44 @@ jobs: name: Build and Publish runs-on: ubuntu-latest needs: [test] + strategy: + fail-fast: false + matrix: + include: + - + dockerfile: ./Dockerfile.web_ui + image: ${{ vars.docker_repo_url }}/${{ gitea.repository }}/web_ui + # - + # dockerfile: ./model/Dockerfile + # image: ${{ vars.docker_repo_url }}/${{ gitea.repository }}/model steps: - - name: Checkout Code + - + name: Checkout Code uses: actions/checkout@v3 - - name: Set Environment Variables + - + name: Set Environment Variables run: | echo "sha_short=$(git rev-parse --short ${{ gitea.sha }} )" >> "$GITHUB_ENV" - - name: Show Environment Variables + - + name: Show Environment Variables run: | echo "DOCKER_REPO_URL: ${{ vars.docker_repo_url }}" echo "REPOSITORY: ${{ gitea.repository }}" echo "COMMIT_SHA: ${{ env.sha_short }}" - - name: Build and Push Image + - + name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ matrix.image }} + - + name: Build and Push Image uses: docker/build-push-action@v4 with: - context: ./web_ui/ + context: . + file: ${{ matrix.dockerfile }} push: true tags: | - ${{ vars.docker_repo_url }}/${{ gitea.repository }}/web_ui:${{ env.sha_short }} - ${{ vars.docker_repo_url }}/${{ gitea.repository }}/web_ui:latest + ${{ matrix.image }}:${{ env.sha_short }} + ${{ matrix.image }}:latest + labels: ${{ steps.meta.outputs.labels }} -- 2.54.0