Repository: statisticsnorway/datadoc-editor (language: python)

Issue:
Display enum value in chosen language



Discussion / hints:

Fix patch:
diff --git a/.coveragerc b/.coveragerc
index 75a78e94..703c6f66 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -2,3 +2,4 @@
 omit =
     datadoc/tests/*
     datadoc/__init__.py
+    datadoc/frontend/callbacks/register.py
diff --git a/.gitignore b/.gitignore
index 60bfc75d..e8268a22 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,6 +4,9 @@ klargjorte_data/person_data_v1__DOC.json
 # This file is changed every time we run it, avoid that being committed
 DataDoc.ipynb
 
+# Jetbrains IDE config
+.idea/
+
 # Byte-compiled / optimized / DLL files
 __pycache__/
 *.py[cod]
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 00000000..a88f8e88
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,42 @@
+repos:
+  - repo: https://github.com/pre-commit/pre-commit-hooks
+    rev: v4.0.1
+    hooks:
+      - id: check-ast
+      - id: check-added-large-files
+      - id: check-merge-conflict
+      - id: check-case-conflict
+      - id: check-docstring-first
+      - id: check-json
+      - id: check-yaml
+      - id: debug-statements
+      - id: end-of-file-fixer
+      - id: trailing-whitespace
+      - id: mixed-line-ending
+  - repo: local
+    hooks:
+      - id: autoflake
+        name: autoflake
+        entry: poetry run autoflake -r -i --remove-all-unused-imports --remove-unused-variables
+        language: system
+        types: [python]
+      - id: isort
+        name: isort
+        entry: poetry run isort
+        language: system
+        types: [python]
+      - id: black
+        name: black
+        entry: poetry run black
+        language: system
+        types: [python]
+      - id: pyupgrade
+        name: pyupgrade
+        entry: poetry run pyupgrade --py37-plus
+        language: system
+        types: [python]
+      - id: flake8
+        name: flake8
+        entry: poetry run flake8
+        language: system
+        types: [python]
diff --git a/.vscode/settings.json b/.vscode/settings.json
index a86289f6..3876d4a7 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -2,17 +2,15 @@
   "python.linting.flake8Enabled": true,
   "python.linting.flake8Args": [
     "--max-line-length=88",
-    "--ignore=E402,F841,F401,E302,E305,W503,E501",
-],
-"python.linting.enabled": true,
-"python.jediEnabled": false,
-"python.linting.pylintEnabled": false,
-"python.linting.mypyEnabled": false,
-"python.languageServer": "Pylance",
-"python.testing.pytestArgs": [],
-"python.testing.unittestEnabled": false,
-"python.testing.pytestEnabled": true,
-"jupyter.jupyterServerType": "local",
-"python.formatting.provider": "black",
-"editor.formatOnSave": true,
+    "--ignore=E402,F841,F401,E302,E305,W503,E501"
+  ],
+  "python.linting.enabled": true,
+  "python.linting.pylintEnabled": false,
+  "python.linting.mypyEnabled": false,
+  "python.languageServer": "Pylance",
+  "python.testing.pytestArgs": [],
+  "python.testing.unittestEnabled": false,
+  "python.testing.pytestEnabled": true,
+  "python.formatting.provider": "black",
+  "editor.formatOnSave": true
 }
diff --git a/datadoc/Enums.py b/datadoc/Enums.py
deleted file mode 100644
index a8f0a916..00000000
--- a/datadoc/Enums.py
+++ /dev/null
@@ -1,8 +0,0 @@
-from enum import Enum
-
-
-class SupportedLanguages(str, Enum):
-    "Reference: https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry"
-    NORSK_BOKMÅL = "nb"
-    NORSK_NYNORSK = "nn"
-    ENGLISH = "en"
diff --git a/datadoc/__init__.py b/datadoc/__init__.py
index f26c7f5f..b229d1a3 100644
--- a/datadoc/__init__.py
+++ b/datadoc/__init__.py
@@ -1,3 +1,3 @@
-from datadoc.app import main
+from datadoc.app import main  # noqa
 
 __version__ = "0.1.1"
diff --git a/datadoc/app.py b/datadoc/app.py
index f202ba78..d826a34a 100644
--- a/datadoc/app.py
+++ b/datadoc/app.py
@@ -6,22 +6,21 @@
 import dash_bootstrap_components as dbc
 from dash import Dash
 
-from datadoc.Enums import SupportedLanguages
-from datadoc.frontend.components.DatasetTab import get_dataset_tab
-from datadoc.frontend.components.VariablesTab import get_variables_tab
+import datadoc.state as state
+from datadoc.backend.DataDocMetadata import DataDocMetadata
+from datadoc.frontend.callbacks.register import register_callbacks
 from datadoc.frontend.components.Alerts import (
     dataset_validation_error,
-    variables_validation_error,
     success_toast,
+    variables_validation_error,
 )
+from datadoc.frontend.components.DatasetTab import get_dataset_tab
 from datadoc.frontend.components.HeaderBars import (
+    get_controls_bar,
     header,
     progress_bar,
-    get_controls_bar,
 )
-import datadoc.state as state
-from datadoc.frontend.callbacks.Callbacks import register_callbacks
-from datadoc.backend.DataDocMetadata import DataDocMetadata
+from datadoc.frontend.components.VariablesTab import get_variables_tab
 from datadoc.utils import running_in_notebook
 
 logger = logging.getLogger(__name__)
@@ -69,13 +68,17 @@ def build_app(dash_class: Type[Dash]) -> Dash:
 
 
 def main(dataset_path: str = None):
+    logging.basicConfig(level=logging.DEBUG)
     if dataset_path is None:
         # Get the supplied command line argument
         parser = argparse.ArgumentParser()
         parser.add_argument("--dataset-path", help="Specify the path to a dataset")
         args = parser.parse_args()
         # Use example dataset if nothing specified
-        dataset = args.dataset_path or "./klargjorte_data/person_data_v1.parquet"
+        dataset = (
+            args.dataset_path
+            or f"{os.path.dirname(__file__)}/../klargjorte_data/person_data_v1.parquet"
+        )
     else:
         dataset = dataset_path
 
@@ -84,15 +87,15 @@ def main(dataset_path: str = None):
     if running_in_notebook():
         logging.basicConfig(level=logging.WARNING)
         from jupyter_dash import JupyterDash
+
         JupyterDash.infer_jupyter_proxy_config()
         app = build_app(JupyterDash)
-        app.run_server(mode="inline")
+        app.run(mode="inline")
     else:
         # Assume running in server mode is better (largely for development purposes)
-        logging.basicConfig(level=logging.DEBUG)
         logger.debug("Starting in development mode")
         app = build_app(Dash)
-        app.run_server(debug=True)
+        app.run(debug=True)
 
 
 if __name__ == "__main__":
diff --git a/datadoc/backend/DataDocMetadata.py b/datadoc/backend/DataDocMetadata.py
index 33b8f803..16cf4e0a 100644
--- a/datadoc/backend/DataDocMetadata.py
+++ b/datadoc/backend/DataDocMetadata.py
@@ -5,15 +5,12 @@
 import pathlib
 from typing import Dict, Optional
 
-from datadoc_model.Enums import (
-    AdministrativeStatus,
-    DatasetState,
-)
-from datadoc.backend.DatasetReader import DatasetReader
-from datadoc_model import Model
 import datadoc.frontend.fields.DisplayDataset as DisplayDataset
 import datadoc.frontend.fields.DisplayVariables as DisplayVariables
+from datadoc.backend.DatasetReader import DatasetReader
 from datadoc.utils import calculate_percentage
+from datadoc_model import Model
+from datadoc_model.Enums import DatasetState
 
 logger = logging.getLogger(__name__)
 
@@ -52,7 +49,9 @@ def __init__(self, dataset):
         self.metadata_document_full_path = self.dataset_directory.joinpath(
             self.metadata_document_name
         )
-        self.dataset_state = self.get_dataset_state(self.dataset_full_path)
+        self.dataset_state: DatasetState = self.get_dataset_state(
+            self.dataset_full_path
+        )
         self.dataset_version = self.get_dataset_version(self.dataset_stem)
         try:
             self.current_user = os.environ["JUPYTERHUB_USER"]
@@ -108,8 +107,8 @@ def get_dataset_version(self, dataset_stem: str) -> Optional[str]:
     def read_metadata_document(self):
         fresh_metadata = {}
         if self.metadata_document_full_path.exists():
-            with open(self.metadata_document_full_path, "r", encoding="utf-8") as JSON:
-                fresh_metadata = json.load(JSON)
+            with open(self.metadata_document_full_path, encoding="utf-8") as file:
+                fresh_metadata = json.load(file)
             logger.info(
                 f"Opened existing metadata file {self.metadata_document_full_path}"
             )
@@ -130,32 +129,20 @@ def generate_new_metadata_document(self):
 
         self.meta.dataset = Model.DataDocDataSet(
             short_name=self.dataset_stem,
-            assessment=None,
             dataset_state=self.dataset_state,
-            name=None,
-            data_source=None,
-            population_description=None,
-            administrative_status=AdministrativeStatus.DRAFT,
             version=self.dataset_version,
-            unit_type=None,
-            temporality_type=None,
-            description=None,
-            spatial_coverage_description=None,
-            id=None,
-            owner=None,
             data_source_path=str(self.dataset_full_path),
             created_date=self.current_datetime,
             created_by=self.current_user,
-            last_updated_date=None,
-            last_updated_by=None,
         )
 
         self.meta.variables = self.ds_schema.get_fields()
 
     def write_metadata_document(self) -> None:
         """Write all currently known metadata to file"""
-        json_str = json.dumps(self.meta.dict(), indent=4, sort_keys=False, default=str)
-        self.metadata_document_full_path.write_text(json_str, encoding="utf-8")
+        self.metadata_document_full_path.write_text(
+            self.meta.json(indent=4, sort_keys=False), encoding="utf-8"
+        )
         logger.info(f"Saved metadata document {self.metadata_document_full_path}")
 
     @property
diff --git a/datadoc/backend/DatasetReader.py b/datadoc/backend/DatasetReader.py
index d99ffbd7..3b0b8e28 100644
--- a/datadoc/backend/DatasetReader.py
+++ b/datadoc/backend/DatasetReader.py
@@ -1,10 +1,10 @@
 import pathlib
 from abc import ABC, abstractmethod
-import pyarrow.parquet as pq
 from typing import List, Optional, TypeVar
+
 import pandas as pd
+import pyarrow.parquet as pq
 from datadoc import state
-
 from datadoc_model.Enums import Datatype
 from datadoc_model.Model import DataDocVariable
 
@@ -72,25 +72,26 @@ def __init__(self, dataset):
     @staticmethod
     def for_file(dataset: str) -> TDatasetReader:
         """Factory method to return the correct subclass based on the given dataset file"""
-        SUPPORTED_FILE_TYPES = {
+        supported_file_types = {
             "parquet": DatasetReaderParquet,
             "sas7bdat": DatasetReaderSas7bdat,
         }
         try:
             file_type = str(pathlib.Path(dataset)).lower().split(".")[1]
+            # Extract the appropriate reader class from the SUPPORTED_FILE_TYPES dict and return an instance of it
+            reader = supported_file_types[file_type](dataset)
         except IndexError as e:
             # Thrown when just one element is returned from split, meaning there is no file extension supplied
             raise FileNotFoundError(
-                f"Could not recognise file type for provided {dataset = }. Supported file types are: {', '.join(SUPPORTED_FILE_TYPES.keys())}"
+                f"Could not recognise file type for provided {dataset = }. Supported file types are: {', '.join(supported_file_types.keys())}"
             ) from e
-        try:
-            # Extract the appropriate reader class from the SUPPORTED_FILE_TYPES dict and return and instance of it
-            return SUPPORTED_FILE_TYPES[file_type](dataset)
-        except KeyError:
+        except KeyError as e:
             # In this case the file type is not supported so we throw a helpful exception
             raise NotImplementedError(
-                f"{file_type = } is not supported. Please open one of the following supported files types: {', '.join(SUPPORTED_FILE_TYPES.keys())} or contact the maintainers to request support."
-            )
+                f"{file_type = } is not supported. Please open one of the following supported files types: {', '.join(supported_file_types.keys())} or contact the maintainers to request support."
+            ) from e
+        else:
+            return reader
 
     @staticmethod
     def transform_data_type(data_type: str) -> Optional[Datatype]:
@@ -113,7 +114,6 @@ def transform_data_type(data_type: str) -> Optional[Datatype]:
     @abstractmethod
     def get_fields(self) -> List[DataDocVariable]:
         """Abstract method, must be implemented by subclasses"""
-        pass
 
 
 class DatasetReaderParquet(DatasetReader):
diff --git a/datadoc/backend/VariableDefinition.py b/datadoc/backend/VariableDefinition.py
index d2a67ec4..976eece1 100644
--- a/datadoc/backend/VariableDefinition.py
+++ b/datadoc/backend/VariableDefinition.py
@@ -1,4 +1,5 @@
 from xml.dom.minidom import parseString
+
 import requests
 
 
diff --git a/datadoc/frontend/Builders.py b/datadoc/frontend/Builders.py
index 581595fb..da5967c2 100644
--- a/datadoc/frontend/Builders.py
+++ b/datadoc/frontend/Builders.py
@@ -1,42 +1,7 @@
 import re
-from typing import List
-import dash_bootstrap_components as dbc
-from dash import html, dcc
-
-from datadoc.frontend.fields.DisplayDataset import DisplayDatasetMetadata
-
-DATASET_METADATA_INPUT = "dataset-metadata-input"
-
 
-def make_dataset_metadata_accordion_item(
-    title: str,
-    metadata_inputs: List[DisplayDatasetMetadata],
-) -> dbc.AccordionItem:
-    return dbc.AccordionItem(
-        title=title,
-        children=[
-            dbc.Row(
-                [
-                    dbc.Col(html.Label(i.display_name)),
-                    dbc.Col(
-                        i.component(
-                            placeholder=i.description,
-                            disabled=not i.editable,
-                            id={
-                                "type": DATASET_METADATA_INPUT,
-                                "id": i.identifier,
-                            },
-                            **i.extra_kwargs,
-                            **(i.options or {}),
-                        ),
-                        width=5,
-                    ),
-                    dbc.Col(width=4),
-                ]
-            )
-            for i in metadata_inputs
-        ],
-    )
+import dash_bootstrap_components as dbc
+from dash import dcc, html
 
 
 def make_ssb_styled_tab(label: str, content: dbc.Container) -> dbc.Tab:
diff --git a/datadoc/frontend/callbacks/Callbacks.py b/datadoc/frontend/callbacks/Callbacks.py
deleted file mode 100644
index 7c33db9d..00000000
--- a/datadoc/frontend/callbacks/Callbacks.py
+++ /dev/null
@@ -1,237 +0,0 @@
-import logging
-from typing import Any, Dict, List, Optional, Tuple
-from pydantic import ValidationError
-from datadoc_model import Model
-from dash import Dash, Output, Input, ctx, ALL
-
-import datadoc.state as state
-from datadoc.Enums import SupportedLanguages
-from datadoc.utils import get_display_values
-from datadoc.frontend.fields.DisplayDataset import (
-    MULTIPLE_LANGUAGE_DATASET_METADATA,
-    NON_EDITABLE_DATASET_METADATA,
-    OBLIGATORY_EDITABLE_DATASET_METADATA,
-    OPTIONAL_DATASET_METADATA,
-    DisplayDatasetMetadata,
-)
-from datadoc.frontend.fields.DisplayVariables import (
-    MULTIPLE_LANGUAGE_VARIABLES_METADATA,
-    VariableIdentifiers,
-)
-from datadoc.frontend.Builders import (
-    DATASET_METADATA_INPUT,
-)
-
-logger = logging.getLogger(__name__)
-
-
-def find_existing_language_string(
-    metadata_model_object: "Model.DataDocBaseModel",
-    value: str,
-    metadata_identifier: str,
-) -> Optional[Model.LanguageStrings]:
-    # In this case we need to set the string to the correct language code
-    language_strings = getattr(metadata_model_object, metadata_identifier)
-    if language_strings is None:
-        # This means that no strings have been saved yet so we need to construct
-        # a new LanguageStrings object
-        language_strings = Model.LanguageStrings(
-            **{state.current_metadata_language.value: value}
-        )
-    else:
-        # In this case there's an existing object so we save this string
-        # to the current language
-        setattr(language_strings, state.current_metadata_language.value, value)
-    return language_strings
-
-
-def accept_variable_metadata_input(
-    data: List[Dict], data_previous: List[Dict]
-) -> Tuple[List[Dict], bool, str]:
-    updated_row_id = None
-    updated_column_id = None
-    new_value = None
-    show_error = False
-    error_explanation = ""
-    output_data = data
-    update_diff = []
-    # What has changed?
-    for i in range(len(data)):
-        update_diff = list(data[i].items() - data_previous[i].items())
-        if update_diff:
-            updated_row_id = data[i][VariableIdentifiers.SHORT_NAME.value]
-            updated_column_id = update_diff[-1][0]
-            new_value = update_diff[-1][-1]
-            break  # We're only interested in one change so we break here
-
-    if update_diff:
-        try:
-            if updated_column_id in MULTIPLE_LANGUAGE_VARIABLES_METADATA:
-                if type(new_value) is str:
-                    new_value = find_existing_language_string(
-                        state.metadata.variables_lookup[updated_row_id],
-                        new_value,
-                        updated_column_id,
-                    )
-                elif new_value is None:
-                    # This edge case occurs when the user removes the text in an input field
-                    # We want to ensure we only remove the content for the current language,
-                    # not create a new blank object!
-                    new_value = find_existing_language_string(
-                        state.metadata.variables_lookup[updated_row_id],
-                        "",
-                        updated_column_id,
-                    )
-
-            logger.debug(
-                f"Row: {updated_row_id} Column: {updated_column_id} New value: {new_value}"
-            )
-            # Write the value to the variables structure
-            setattr(
-                state.metadata.variables_lookup[updated_row_id],
-                updated_column_id,
-                new_value,
-            )
-        except ValidationError as e:
-            show_error = True
-            error_explanation = f"`{e}`"
-            output_data = data_previous
-            logger.debug("Caught ValidationError:", exc_info=True)
-        else:
-            logger.debug(f"Successfully updated {updated_row_id} with {new_value}")
-
-    return output_data, show_error, error_explanation
-
-
-def accept_dataset_metadata_input(
-    value: Any, metadata_identifier: str
-) -> Tuple[bool, str]:
-    try:
-        if (
-            metadata_identifier in MULTIPLE_LANGUAGE_DATASET_METADATA
-            and type(value) is str
-        ):
-            value = find_existing_language_string(
-                state.metadata.meta.dataset, value, metadata_identifier
-            )
-
-        # Update the value in the model
-        setattr(
-            state.metadata.meta.dataset,
-            metadata_identifier,
-            value,
-        )
-    except ValidationError as e:
-        show_error = True
-        error_explanation = f"`{e}`"
-        logger.debug("Caught ValidationError:", exc_info=True)
-    else:
-        show_error = False
-        error_explanation = ""
-        logger.debug(f"Successfully updated {metadata_identifier} with {value}")
-
-    return show_error, error_explanation
-
-
-def update_dataset_metadata_language(language: SupportedLanguages) -> List[Any]:
-    """Update the global language setting with the chosen language.
-    Return new values for ALL the dataset metadata inputs to allow
-    editing of strings in the chosen language"""
-
-    state.current_metadata_language = language
-    # The order of this list MUST match the order of display components, as defined
-    # in dataset_variables in DataDocDash.py
-    displayed_dataset_metadata: List[DisplayDatasetMetadata] = (
-        OBLIGATORY_EDITABLE_DATASET_METADATA
-        + OPTIONAL_DATASET_METADATA
-        + NON_EDITABLE_DATASET_METADATA
-    )
-    logger.debug(f"Updated language: {state.current_metadata_language.name}")
-    return [
-        m.value_getter(state.metadata.meta.dataset, m.identifier)
-        for m in displayed_dataset_metadata
-    ]
-
-
-def update_variable_table_language(
-    data: List[Dict],
-    language: SupportedLanguages,
-) -> Tuple[List[Dict], bool, str]:
-    state.current_metadata_language = language
-    new_data = []
-    for row in data:
-        new_data.append(
-            get_display_values(
-                state.metadata.variables_lookup[
-                    row[VariableIdentifiers.SHORT_NAME.value]
-                ],
-                state.current_metadata_language,
-            )
-        )
-    logger.debug(f"Updated variable table language: {language.name}")
-    return new_data, False, ""
-
-
-def register_callbacks(app: Dash) -> None:
-    @app.callback(
-        Output("progress-bar", "value"),
-        Output("progress-bar", "label"),
-        Input({"type": DATASET_METADATA_INPUT, "id": ALL}, "value"),
-        Input("variables-table", "data"),
-    )
-    def callback_update_progress(value, data) -> Tuple[int, str]:
-        completion = state.metadata.percent_complete
-        return completion, f"{completion}%"
-
-    @app.callback(
-        Output("success-message", "is_open"),
-        Input("save-button", "n_clicks"),
-        prevent_initial_call=True,
-    )
-    def callback_save_metadata_file(n_clicks):
-        if n_clicks and n_clicks > 0:
-            # Write the final completion percentage to the model
-            state.metadata.meta.percentage_complete = state.metadata.percent_complete
-            state.metadata.write_metadata_document()
-            return True
-        else:
-            return False
-
-    @app.callback(
-        Output(
-            {"type": DATASET_METADATA_INPUT, "id": ALL},
-            "value",
-        ),
-        Input("language-dropdown", "value"),
-    )
-    def callback_change_language(language: str):
-        return update_dataset_metadata_language(SupportedLanguages(language))
-
-    @app.callback(
-        Output("dataset-validation-error", "is_open"),
-        Output("dataset-validation-explanation", "children"),
-        Input({"type": DATASET_METADATA_INPUT, "id": ALL}, "value"),
-        prevent_initial_call=True,
-    )
-    def callback_accept_dataset_metadata_input(value: Any) -> Tuple[bool, str]:
-        # Get the ID of the input that changed. This MUST match the attribute name defined in DataDocDataSet
-        return accept_dataset_metadata_input(
-            ctx.triggered[0]["value"], ctx.triggered_id["id"]
-        )
-
-    @app.callback(
-        Output("variables-table", "data"),
-        Output("variables-validation-error", "is_open"),
-        Output("variables-validation-explanation", "children"),
-        Input("variables-table", "data"),
-        Input("variables-table", "data_previous"),
-        Input("language-dropdown", "value"),
-        prevent_initial_call=True,
-    )
-    def callback_variable_table(
-        data: List[Dict], data_previous: List[Dict], language: str
-    ) -> Tuple[List[Dict], bool, str]:
-        if ctx.triggered_id == "language-dropdown":
-            return update_variable_table_language(data, SupportedLanguages(language))
-        else:
-            return accept_variable_metadata_input(data, data_previous)
diff --git a/datadoc/frontend/callbacks/dataset.py b/datadoc/frontend/callbacks/dataset.py
new file mode 100644
index 00000000..3445e635
--- /dev/null
+++ b/datadoc/frontend/callbacks/dataset.py
@@ -0,0 +1,70 @@
+import logging
+from typing import Any, List, Tuple
+
+from pydantic import ValidationError
+
+import datadoc.state as state
+from datadoc.frontend.callbacks.utils import (
+    find_existing_language_string,
+    get_options_for_language,
+    update_global_language_state,
+)
+from datadoc.frontend.fields.DisplayDataset import (
+    DISPLAYED_DATASET_METADATA,
+    DISPLAYED_DROPDOWN_DATASET_ENUMS,
+    MULTIPLE_LANGUAGE_DATASET_METADATA,
+)
+
+logger = logging.getLogger(__name__)
+
+
+def accept_dataset_metadata_input(
+    value: Any, metadata_identifier: str
+) -> Tuple[bool, str]:
+    logger.debug(f"Received update {value = } for {metadata_identifier = }")
+    try:
+        if (
+            metadata_identifier in MULTIPLE_LANGUAGE_DATASET_METADATA
+            and type(value) is str
+        ):
+            value = find_existing_language_string(
+                state.metadata.meta.dataset, value, metadata_identifier
+            )
+
+        logger.debug(f"Updating {value = } for {metadata_identifier = }")
+        # Update the value in the model
+        setattr(
+            state.metadata.meta.dataset,
+            metadata_identifier,
+            value,
+        )
+    except ValidationError as e:
+        show_error = True
+        error_explanation = f"`{e}`"
+        logger.debug("Caught ValidationError:", exc_info=True)
+    else:
+        show_error = False
+        error_explanation = ""
+        logger.debug(f"Successfully updated {metadata_identifier} with {value}")
+
+    return show_error, error_explanation
+
+
+def update_dataset_metadata_language() -> List[Any]:
+    """Return new values for ALL the dataset metadata inputs to allow
+    editing of strings in the chosen language"""
+    return [
+        m.value_getter(state.metadata.meta.dataset, m.identifier)
+        for m in DISPLAYED_DATASET_METADATA
+    ]
+
+
+def change_language_dataset_metadata(language):
+    update_global_language_state(language)
+    return (
+        *(
+            get_options_for_language(language, e)
+            for e in DISPLAYED_DROPDOWN_DATASET_ENUMS
+        ),
+        update_dataset_metadata_language(),
+    )
diff --git a/datadoc/frontend/callbacks/register.py b/datadoc/frontend/callbacks/register.py
new file mode 100644
index 00000000..e09013b2
--- /dev/null
+++ b/datadoc/frontend/callbacks/register.py
@@ -0,0 +1,105 @@
+from typing import Any, Dict, List, Tuple
+
+from dash import ALL, Dash, Input, Output, ctx
+from datadoc_model.Enums import SupportedLanguages
+
+import datadoc.state as state
+from datadoc.frontend.callbacks.dataset import (
+    accept_dataset_metadata_input,
+    change_language_dataset_metadata,
+)
+from datadoc.frontend.callbacks.variables import (
+    accept_variable_metadata_input,
+    update_variable_table_dropdown_options_for_language,
+    update_variable_table_language,
+)
+from datadoc.frontend.components.DatasetTab import DATASET_METADATA_INPUT
+from datadoc.frontend.fields.DisplayDataset import DISPLAYED_DROPDOWN_DATASET_METADATA
+
+# Avoid implementing callbacks here.
+#
+# The Dash Inputs and Outputs etc. shall be defined here, but the
+# implementations should be in other functions, to enable unit testing
+
+
+def register_callbacks(app: Dash) -> None:
+    @app.callback(
+        Output("progress-bar", "value"),
+        Output("progress-bar", "label"),
+        Input({"type": DATASET_METADATA_INPUT, "id": ALL}, "value"),
+        Input("variables-table", "data"),
+    )
+    def callback_update_progress(value, data) -> Tuple[int, str]:
+        completion = state.metadata.percent_complete
+        return completion, f"{completion}%"
+
+    @app.callback(
+        Output("success-message", "is_open"),
+        Input("save-button", "n_clicks"),
+        prevent_initial_call=True,
+    )
+    def callback_save_metadata_file(n_clicks):
+        if n_clicks and n_clicks > 0:
+            # Write the final completion percentage to the model
+            state.metadata.meta.percentage_complete = state.metadata.percent_complete
+            state.metadata.write_metadata_document()
+            return True
+        else:
+            return False
+
+    @app.callback(
+        *[
+            Output(
+                {
+                    "type": DATASET_METADATA_INPUT,
+                    "id": m.identifier,
+                },
+                "options",
+            )
+            for m in DISPLAYED_DROPDOWN_DATASET_METADATA
+        ],
+        Output(
+            {"type": DATASET_METADATA_INPUT, "id": ALL},
+            "value",
+        ),
+        Input("language-dropdown", "value"),
+    )
+    def callback_change_language_dataset_metadata(language: str):
+        return change_language_dataset_metadata(SupportedLanguages(language))
+
+    @app.callback(
+        Output("dataset-validation-error", "is_open"),
+        Output("dataset-validation-explanation", "children"),
+        Input({"type": DATASET_METADATA_INPUT, "id": ALL}, "value"),
+        prevent_initial_call=True,
+    )
+    def callback_accept_dataset_metadata_input(value: Any) -> Tuple[bool, str]:
+        # Get the ID of the input that changed. This MUST match the attribute name defined in DataDocDataSet
+        return accept_dataset_metadata_input(
+            ctx.triggered[0]["value"], ctx.triggered_id["id"]
+        )
+
+    @app.callback(
+        Output("variables-table", "data"),
+        Output("variables-validation-error", "is_open"),
+        Output("variables-validation-explanation", "children"),
+        Input("variables-table", "data"),
+        Input("variables-table", "data_previous"),
+        Input("language-dropdown", "value"),
+        prevent_initial_call=True,
+    )
+    def callback_variable_table(
+        data: List[Dict], data_previous: List[Dict], language: str
+    ) -> Tuple[List[Dict], bool, str]:
+        if ctx.triggered_id == "language-dropdown":
+            return update_variable_table_language(data, SupportedLanguages(language))
+        else:
+            return accept_variable_metadata_input(data, data_previous)
+
+    @app.callback(
+        Output("variables-table", "dropdown"),
+        Input("language-dropdown", "value"),
+    )
+    def callback_variable_table_dropdown_options(language: str):
+        language = SupportedLanguages(language)
+        return update_variable_table_dropdown_options_for_language(language)
diff --git a/datadoc/frontend/callbacks/utils.py b/datadoc/frontend/callbacks/utils.py
new file mode 100644
index 00000000..8310bf7b
--- /dev/null
+++ b/datadoc/frontend/callbacks/utils.py
@@ -0,0 +1,48 @@
+import logging
+from typing import Dict, List, Optional
+
+from datadoc_model import Model
+from datadoc_model.Enums import SupportedLanguages
+from datadoc_model.LanguageStringsEnum import LanguageStringsEnum
+
+import datadoc.state as state
+
+logger = logging.getLogger(__name__)
+
+
+def update_global_language_state(language: SupportedLanguages):
+    logger.debug(f"Updating language: {language.name}")
+    state.current_metadata_language = language
+
+
+def get_options_for_language(
+    language: SupportedLanguages, enum: LanguageStringsEnum
+) -> List[Dict[str, str]]:
+    """Generate the list of options based on the currently chosen language"""
+    return [
+        {
+            "label": i.get_value_for_language(language),
+            "value": i.name,
+        }
+        for i in enum
+    ]
+
+
+def find_existing_language_string(
+    metadata_model_object: "Model.DataDocBaseModel",
+    value: str,
+    metadata_identifier: str,
+) -> Optional[Model.LanguageStrings]:
+    # In this case we need to set the string to the correct language code
+    language_strings = getattr(metadata_model_object, metadata_identifier)
+    if language_strings is None:
+        # This means that no strings have been saved yet so we need to construct
+        # a new LanguageStrings object
+        language_strings = Model.LanguageStrings(
+            **{state.current_metadata_language.value: value}
+        )
+    else:
+        # In this case there's an existing object so we save this string
+        # to the current language
+        setattr(language_strings, state.current_metadata_language.value, value)
+    return language_strings
diff --git a/datadoc/frontend/callbacks/variables.py b/datadoc/frontend/callbacks/variables.py
new file mode 100644
index 00000000..95136c4a
--- /dev/null
+++ b/datadoc/frontend/callbacks/variables.py
@@ -0,0 +1,150 @@
+import logging
+from typing import Dict, List, Optional, Tuple
+
+from datadoc_model.Enums import SupportedLanguages
+from pydantic import ValidationError
+
+import datadoc.state as state
+from datadoc.frontend.callbacks.utils import (
+    find_existing_language_string,
+    get_options_for_language,
+)
+from datadoc.frontend.fields.DisplayVariables import (
+    DISPLAYED_DROPDOWN_VARIABLES_METADATA,
+    DISPLAYED_DROPDOWN_VARIABLES_TYPES,
+    MULTIPLE_LANGUAGE_VARIABLES_METADATA,
+    VariableIdentifiers,
+)
+from datadoc.utils import get_display_values
+
+logger = logging.getLogger(__name__)
+
+
+def get_boolean_options_for_language(language: SupportedLanguages):
+    true_labels = {
+        SupportedLanguages.ENGLISH: "Yes",
+        SupportedLanguages.NORSK_NYNORSK: "Ja",
+        SupportedLanguages.NORSK_BOKMÅL: "Ja",
+    }
+    false_labels = {
+        SupportedLanguages.ENGLISH: "No",
+        SupportedLanguages.NORSK_NYNORSK: "Nei",
+        SupportedLanguages.NORSK_BOKMÅL: "Nei",
+    }
+    return [
+        {
+            "label": f"{true_labels[language]}",
+            "value": True,
+        },
+        {
+            "label": f"{false_labels[language]}",
+            "value": False,
+        },
+    ]
+
+
+def accept_variable_metadata_input(
+    data: List[Dict], data_previous: List[Dict]
+) -> Tuple[List[Dict], bool, str]:
+    updated_row_id: str = ""
+    updated_column_id: str = ""
+    new_value: Optional[str] = None
+    show_error = False
+    error_explanation = ""
+    output_data = data
+    update_diff = []
+    # What has changed?
+    for i in range(len(data)):
+        update_diff = list(data[i].items() - data_previous[i].items())
+        if update_diff:
+            updated_row_id = data[i][VariableIdentifiers.SHORT_NAME.value]
+            updated_column_id = update_diff[-1][0]
+            new_value = update_diff[-1][-1]
+            break  # We're only interested in one change so we break here
+
+    if update_diff:
+        try:
+            if updated_column_id in MULTIPLE_LANGUAGE_VARIABLES_METADATA:
+                if type(new_value) is str:
+                    new_value = find_existing_language_string(
+                        state.metadata.variables_lookup[updated_row_id],
+                        new_value,
+                        updated_column_id,
+                    )
+                elif new_value is None:
+                    # This edge case occurs when the user removes the text in an input field
+                    # We want to ensure we only remove the content for the current language,
+                    # not create a new blank object!
+                    new_value = find_existing_language_string(
+                        state.metadata.variables_lookup[updated_row_id],
+                        "",
+                        updated_column_id,
+                    )
+
+            logger.debug(
+                f"Row: {updated_row_id} Column: {updated_column_id} New value: {new_value}"
+            )
+            # Write the value to the variables structure
+            setattr(
+                state.metadata.variables_lookup[updated_row_id],
+                updated_column_id,
+                new_value,
+            )
+        except ValidationError as e:
+            show_error = True
+            error_explanation = f"`{e}`"
+            output_data = data_previous
+            logger.debug("Caught ValidationError:", exc_info=True)
+        else:
+            logger.debug(f"Successfully updated {updated_row_id} with {new_value}")
+
+    return output_data, show_error, error_explanation
+
+
+def update_variable_table_dropdown_options_for_language(
+    language: SupportedLanguages,
+) -> Dict[str, Dict[str, List[Dict[str, str]]]]:
+    """Retrieves enum options for dropdowns in the Datatable. Handles the
+    special case of boolean values which we represent in the Datatable
+    with a Dropdown but they're not backed by an Enum.
+
+    Example return structure:
+        {'data_type': {'options': [{'label': 'TEKST', 'value': 'STRING'},
+                                {'label': 'HELTALL', 'value': 'INTEGER'},
+                                {'label': 'DESIMALTALL', 'value': 'FLOAT'},
+                                {'label': 'DATOTID', 'value': 'DATETIME'},
+                                {'label': 'BOOLSK', 'value': 'BOOLEAN'}]},
+        'direct_person_identifying': {'options': [{'label': 'Ja', 'value': True},
+                                                {'label': 'Nei', 'value': False}]},
+        'temporality_type': {'options': [{'label': 'FAST', 'value': 'FIXED'},
+            ...
+        }
+    """
+    options = []
+    for field_type in DISPLAYED_DROPDOWN_VARIABLES_TYPES:
+        value = (
+            get_boolean_options_for_language(language)
+            if field_type is bool
+            else get_options_for_language(language, field_type)
+        )
+        options.append({"options": value})
+    return dict(zip(DISPLAYED_DROPDOWN_VARIABLES_METADATA, options))
+
+
+def update_variable_table_language(
+    data: List[Dict],
+    language: SupportedLanguages,
+) -> Tuple[List[Dict], bool, str]:
+    state.current_metadata_language = language
+    new_data = []
+    for row in data:
+        new_data.append(
+            get_display_values(
+                state.metadata.variables_lookup[
+                    row[VariableIdentifiers.SHORT_NAME.value]
+                ],
+                state.current_metadata_language,
+            )
+        )
+    logger.debug(f"Updated variable table language: {language.name}")
+    return new_data, False, ""
diff --git a/datadoc/frontend/components/Alerts.py b/datadoc/frontend/components/Alerts.py
index 055ef2a5..d5b86c5a 100644
--- a/datadoc/frontend/components/Alerts.py
+++ b/datadoc/frontend/components/Alerts.py
@@ -1,6 +1,5 @@
 import dash_bootstrap_components as dbc
 from dash import html
-
 from datadoc.frontend.Builders import make_ssb_warning_alert
 
 dataset_validation_error = make_ssb_warning_alert(
diff --git a/datadoc/frontend/components/DatasetTab.py b/datadoc/frontend/components/DatasetTab.py
index cf0423da..9b6a2c78 100644
--- a/datadoc/frontend/components/DatasetTab.py
+++ b/datadoc/frontend/components/DatasetTab.py
@@ -1,18 +1,46 @@
-import dash_bootstrap_components as dbc
-from dash import html, dash_table
+from typing import List
 
-from datadoc.frontend.Builders import (
-    make_dataset_metadata_accordion_item,
-    make_ssb_styled_tab,
-)
+import dash_bootstrap_components as dbc
+from dash import html
+from datadoc.frontend.Builders import make_ssb_styled_tab
 from datadoc.frontend.fields.DisplayDataset import (
     NON_EDITABLE_DATASET_METADATA,
     OBLIGATORY_EDITABLE_DATASET_METADATA,
     OPTIONAL_DATASET_METADATA,
+    DisplayDatasetMetadata,
 )
-from datadoc.frontend.fields.DisplayVariables import DISPLAY_VARIABLES
-from datadoc.utils import get_display_values
-import datadoc.state as state
+
+DATASET_METADATA_INPUT = "dataset-metadata-input"
+
+
+def make_dataset_metadata_accordion_item(
+    title: str,
+    metadata_inputs: List[DisplayDatasetMetadata],
+) -> dbc.AccordionItem:
+    return dbc.AccordionItem(
+        title=title,
+        children=[
+            dbc.Row(
+                [
+                    dbc.Col(html.Label(i.display_name)),
+                    dbc.Col(
+                        i.component(
+                            placeholder=i.description,
+                            disabled=not i.editable,
+                            id={
+                                "type": DATASET_METADATA_INPUT,
+                                "id": i.identifier,
+                            },
+                            **i.extra_kwargs,
+                        ),
+                        width=5,
+                    ),
+                    dbc.Col(width=4),
+                ]
+            )
+            for i in metadata_inputs
+        ],
+    )
 
 
 def get_dataset_tab() -> dbc.Tab:
diff --git a/datadoc/frontend/components/HeaderBars.py b/datadoc/frontend/components/HeaderBars.py
index 3134b4fb..a91151ee 100644
--- a/datadoc/frontend/components/HeaderBars.py
+++ b/datadoc/frontend/components/HeaderBars.py
@@ -1,16 +1,13 @@
 import dash_bootstrap_components as dbc
-from dash import html, dcc
-
-from datadoc.Enums import SupportedLanguages
 import datadoc.state as state
-
+from dash import dcc, html
+from datadoc_model.Enums import SupportedLanguages
 
 COLORS = {"dark_1": "#F0F8F9", "green_1": "#ECFEED", "green_4": "#00824D"}
 
 header = dbc.CardBody(
     dbc.Row(
         children=[
-            # html.Link(rel="stylesheet", href="assets/bundle.css"),
             html.H1("DataDoc", className="ssb-title", style={"color": "white"}),
         ],
     ),
diff --git a/datadoc/frontend/components/VariablesTab.py b/datadoc/frontend/components/VariablesTab.py
index 5db7b7e0..8030bcd5 100644
--- a/datadoc/frontend/components/VariablesTab.py
+++ b/datadoc/frontend/components/VariablesTab.py
@@ -1,10 +1,9 @@
 import dash_bootstrap_components as dbc
-from dash import html, dash_table
-
+import datadoc.state as state
+from dash import dash_table, html
 from datadoc.frontend.Builders import make_ssb_styled_tab
 from datadoc.frontend.fields.DisplayVariables import DISPLAY_VARIABLES
 from datadoc.utils import get_display_values
-import datadoc.state as state
 
 
 def get_variables_tab() -> dbc.Tab:
@@ -47,12 +46,6 @@ def get_variables_tab() -> dbc.Tab:
                         # Enable sorting and pagination
                         sort_action="native",
                         page_action="native",
-                        # Populate the options for all dropdown values
-                        dropdown={
-                            v.identifier: v.options
-                            for v in DISPLAY_VARIABLES.values()
-                            if v.options
-                        },
                     )
                 ),
             ],
diff --git a/datadoc/frontend/fields/DisplayBase.py b/datadoc/frontend/fields/DisplayBase.py
index d41f9db3..93a4b05a 100644
--- a/datadoc/frontend/fields/DisplayBase.py
+++ b/datadoc/frontend/fields/DisplayBase.py
@@ -1,12 +1,14 @@
+import logging
 from dataclasses import dataclass, field
-from typing import Any, Dict, Optional, Type, List, Callable
-from pydantic import BaseModel
+from typing import Any, Callable, Dict, List, Optional, Type
+
 from dash import dcc
 from dash.development.base_component import Component
-
-from datadoc.Enums import SupportedLanguages
 from datadoc import state
+from datadoc_model.LanguageStrings import LanguageStrings
+from pydantic import BaseModel
 
+logger = logging.getLogger(__name__)
 
 INPUT_KWARGS = {
     "debounce": True,
@@ -30,10 +32,8 @@ def get_standard_metadata(metadata: BaseModel, identifier: str) -> Any:
     return metadata.dict()[identifier]
 
 
-def get_multi_language_metadata(
-    metadata: BaseModel, identifier: str
-) -> Optional[SupportedLanguages]:
-    value = getattr(metadata, identifier)
+def get_multi_language_metadata(metadata: BaseModel, identifier: str) -> Optional[str]:
+    value: LanguageStrings = getattr(metadata, identifier)
     if value is None:
         return value
     return getattr(value, state.current_metadata_language)
@@ -59,5 +59,4 @@ class DisplayVariablesMetadata(DisplayMetadata):
 class DisplayDatasetMetadata(DisplayMetadata):
     extra_kwargs: Dict[str, Any] = field(default_factory=kwargs_factory)
     component: Type[Component] = dcc.Input
-    options: Optional[Dict[str, List[Dict[str, str]]]] = None
     value_getter: Callable[[BaseModel, str], Any] = get_standard_metadata
diff --git a/datadoc/frontend/fields/DisplayDataset.py b/datadoc/frontend/fields/DisplayDataset.py
index d70a73ad..0d4746ab 100644
--- a/datadoc/frontend/fields/DisplayDataset.py
+++ b/datadoc/frontend/fields/DisplayDataset.py
@@ -1,19 +1,17 @@
+import logging
 from enum import Enum
+from typing import Dict, List
+
 from dash import dcc
-from datadoc_model.Enums import (
-    Assessment,
-    DatasetState,
-    DatasetStatus,
-    TemporalityType,
-    UnitType,
-)
-from datadoc import state
 from datadoc.frontend.fields.DisplayBase import (
-    DisplayDatasetMetadata,
     DROPDOWN_KWARGS,
     NUMBER_KWARGS,
+    DisplayDatasetMetadata,
     get_multi_language_metadata,
 )
+from datadoc_model import Model
+
+logger = logging.getLogger(__name__)
 
 
 class DatasetIdentifiers(str, Enum):
@@ -41,7 +39,7 @@ class DatasetIdentifiers(str, Enum):
     METADATA_LAST_UPDATED_BY = "last_updated_by"
 
 
-DISPLAY_DATASET = {
+DISPLAY_DATASET: Dict[DatasetIdentifiers, DisplayDatasetMetadata] = {
     DatasetIdentifiers.SHORT_NAME: DisplayDatasetMetadata(
         identifier=DatasetIdentifiers.SHORT_NAME.value,
         display_name="Kortnavn",
@@ -56,7 +54,6 @@ class DatasetIdentifiers(str, Enum):
         description="Verdivurdering (sensitivitetsklassifisering) for datasettet.",
         component=dcc.Dropdown,
         extra_kwargs=DROPDOWN_KWARGS,
-        options={"options": [{"label": i.name, "value": i.name} for i in Assessment]},
     ),
     DatasetIdentifiers.DATASET_STATUS: DisplayDatasetMetadata(
         identifier=DatasetIdentifiers.DATASET_STATUS.value,
@@ -64,9 +61,6 @@ class DatasetIdentifiers(str, Enum):
         description="Livssyklus for datasettet",
         component=dcc.Dropdown,
         extra_kwargs=DROPDOWN_KWARGS,
-        options={
-            "options": [{"label": i.name, "value": i.name} for i in DatasetStatus]
-        },
     ),
     DatasetIdentifiers.DATASET_STATE: DisplayDatasetMetadata(
         identifier=DatasetIdentifiers.DATASET_STATE.value,
@@ -75,7 +69,6 @@ class DatasetIdentifiers(str, Enum):
         obligatory=True,
         component=dcc.Dropdown,
         extra_kwargs=DROPDOWN_KWARGS,
-        options={"options": [{"label": i.name, "value": i.name} for i in DatasetState]},
     ),
     DatasetIdentifiers.NAME: DisplayDatasetMetadata(
         identifier=DatasetIdentifiers.NAME.value,
@@ -111,7 +104,6 @@ class DatasetIdentifiers(str, Enum):
         description="Primær enhetstype for datafil, datatabell eller datasett. Se  Vi jobber med en avklaring av behov for flere enhetstyper her.",
         component=dcc.Dropdown,
         extra_kwargs=DROPDOWN_KWARGS,
-        options={"options": [{"label": i.name, "value": i.name} for i in UnitType]},
     ),
     DatasetIdentifiers.TEMPORALITY_TYPE: DisplayDatasetMetadata(
         identifier=DatasetIdentifiers.TEMPORALITY_TYPE.value,
@@ -119,9 +111,6 @@ class DatasetIdentifiers(str, Enum):
         description="Temporalitetstype. Settes enten for variabelforekomst eller datasett. Se Temporalitet, hendelser og forløp.",
         component=dcc.Dropdown,
         extra_kwargs=DROPDOWN_KWARGS,
-        options={
-            "options": [{"label": i.name, "value": i.name} for i in TemporalityType]
-        },
     ),
     DatasetIdentifiers.DESCRIPTION: DisplayDatasetMetadata(
         identifier=DatasetIdentifiers.DESCRIPTION.value,
@@ -211,3 +200,20 @@ class DatasetIdentifiers(str, Enum):
 ]
 
 NON_EDITABLE_DATASET_METADATA = [m for m in DISPLAY_DATASET.values() if not m.editable]
+
+
+# The order of this list MUST match the order of display components, as defined in DatasetTab.py
+DISPLAYED_DATASET_METADATA: List[DisplayDatasetMetadata] = (
+    OBLIGATORY_EDITABLE_DATASET_METADATA
+    + OPTIONAL_DATASET_METADATA
+    + NON_EDITABLE_DATASET_METADATA
+)
+
+DISPLAYED_DROPDOWN_DATASET_METADATA = [
+    m for m in DISPLAYED_DATASET_METADATA if m.component == dcc.Dropdown
+]
+
+DISPLAYED_DROPDOWN_DATASET_ENUMS = [
+    Model.DataDocDataSet.__fields__[m.identifier].type_
+    for m in DISPLAYED_DROPDOWN_DATASET_METADATA
+]
diff --git a/datadoc/frontend/fields/DisplayVariables.py b/datadoc/frontend/fields/DisplayVariables.py
index 47905783..8a917823 100644
--- a/datadoc/frontend/fields/DisplayVariables.py
+++ b/datadoc/frontend/fields/DisplayVariables.py
@@ -1,7 +1,8 @@
 from enum import Enum
 
-from datadoc_model.Enums import Datatype, TemporalityType, VariableRole
 from datadoc.frontend.fields.DisplayBase import DisplayVariablesMetadata
+from datadoc_model import Model
+from datadoc_model.LanguageStringsEnum import LanguageStringsEnum
 
 
 class VariableIdentifiers(str, Enum):
@@ -48,7 +49,6 @@ class VariableIdentifiers(str, Enum):
         description="Datatype",
         obligatory=True,
         presentation="dropdown",
-        options={"options": [{"label": i.name, "value": i.name} for i in Datatype]},
     ),
     VariableIdentifiers.VARIABLE_ROLE: DisplayVariablesMetadata(
         identifier=VariableIdentifiers.VARIABLE_ROLE.value,
@@ -56,7 +56,6 @@ class VariableIdentifiers(str, Enum):
         description="Variabelens rolle i datasett",
         obligatory=True,
         presentation="dropdown",
-        options={"options": [{"label": i.name, "value": i.name} for i in VariableRole]},
     ),
     VariableIdentifiers.DEFINITION_URI: DisplayVariablesMetadata(
         identifier=VariableIdentifiers.DEFINITION_URI.value,
@@ -70,12 +69,6 @@ class VariableIdentifiers(str, Enum):
         description="Direkte personidentifiserende informasjon (DPI)",
         obligatory=True,
         presentation="dropdown",
-        options={
-            "options": [
-                {"label": "Ja", "value": True},
-                {"label": "Nei", "value": False},
-            ]
-        },
     ),
     VariableIdentifiers.DATA_SOURCE: DisplayVariablesMetadata(
         identifier=VariableIdentifiers.DATA_SOURCE.value,
@@ -100,9 +93,6 @@ class VariableIdentifiers(str, Enum):
         display_name="Temporalitetstype",
         description="Temporalitetstype. Settes enten for variabelforekomst eller datasett. Se Temporalitet, hendelser og forløp.",
         presentation="dropdown",
-        options={
-            "options": [{"label": i.name, "value": i.name} for i in TemporalityType]
-        },
     ),
     VariableIdentifiers.MEASUREMENT_UNIT: DisplayVariablesMetadata(
         identifier=VariableIdentifiers.MEASUREMENT_UNIT.value,
@@ -153,3 +143,15 @@ class VariableIdentifiers(str, Enum):
 MULTIPLE_LANGUAGE_VARIABLES_METADATA = [
     m.identifier for m in DISPLAY_VARIABLES.values() if m.multiple_language_support
 ]
+
+DISPLAYED_DROPDOWN_VARIABLES_METADATA = [
+    m.identifier for m in DISPLAY_VARIABLES.values() if m.presentation == "dropdown"
+]
+
+DISPLAYED_DROPDOWN_VARIABLES_TYPES = []
+
+for m in DISPLAY_VARIABLES.values():
+    if m.presentation == "dropdown":
+        field_type = Model.DataDocVariable.__fields__[m.identifier].type_
+        if issubclass(field_type, LanguageStringsEnum) or field_type is bool:
+            DISPLAYED_DROPDOWN_VARIABLES_TYPES.append(field_type)
diff --git a/datadoc/state.py b/datadoc/state.py
index 405a6af5..2b5ba323 100644
--- a/datadoc/state.py
+++ b/datadoc/state.py
@@ -1,5 +1,6 @@
 from typing import TYPE_CHECKING
-from datadoc.Enums import SupportedLanguages
+
+from datadoc_model.Enums import SupportedLanguages
 
 if TYPE_CHECKING:
     # This is only needed for a type hint so we put the import inside
diff --git a/datadoc/utils.py b/datadoc/utils.py
index ad5de4ab..1bbe9418 100644
--- a/datadoc/utils.py
+++ b/datadoc/utils.py
@@ -1,5 +1,5 @@
 from datadoc_model import Model
-from datadoc.Enums import SupportedLanguages
+from datadoc_model.Enums import SupportedLanguages
 
 
 def running_in_notebook() -> bool:
diff --git a/klargjorte_data/person_data_v1__DOC_example_1.json b/klargjorte_data/person_data_v1__DOC_example_1.json
index d8f2f31d..51c9cf1e 100644
--- a/klargjorte_data/person_data_v1__DOC_example_1.json
+++ b/klargjorte_data/person_data_v1__DOC_example_1.json
@@ -229,4 +229,4 @@
             "contains_data_until": null
         }
     ]
-}
\ No newline at end of file
+}
diff --git a/poetry.lock b/poetry.lock
index 900bc649..498e16da 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -20,7 +20,7 @@ python-versions = "*"
 
 [[package]]
 name = "asttokens"
-version = "2.0.5"
+version = "2.0.8"
 description = "Annotate AST trees with source code positions"
 category = "main"
 optional = false
@@ -30,7 +30,7 @@ python-versions = "*"
 six = "*"
 
 [package.extras]
-test = ["astroid", "pytest"]
+test = ["pytest", "astroid (<=2.5.3)"]
 
 [[package]]
 name = "atomicwrites"
@@ -54,6 +54,17 @@ tests = ["cloudpickle", "zope.interface", "pytest-mypy-plugins", "mypy (>=0.900,
 docs = ["sphinx-notfound-page", "zope.interface", "sphinx", "furo"]
 dev = ["cloudpickle", "pre-commit", "sphinx-notfound-page", "sphinx", "furo", "zope.interface", "pytest-mypy-plugins", "mypy (>=0.900,!=0.940)", "pytest (>=4.3.0)", "pympler", "hypothesis", "coverage[toml] (>=5.0.2)"]
 
+[[package]]
+name = "autoflake"
+version = "1.4"
+description = "Removes unused imports and unused variables"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+pyflakes = ">=1.1.0"
+
 [[package]]
 name = "backcall"
 version = "0.2.0"
@@ -119,6 +130,14 @@ python-versions = "*"
 [package.dependencies]
 pycparser = "*"
 
+[[package]]
+name = "cfgv"
+version = "3.3.1"
+description = "Validate configuration and produce human readable error messages."
+category = "dev"
+optional = false
+python-versions = ">=3.6.1"
+
 [[package]]
 name = "charset-normalizer"
 version = "2.1.0"
@@ -149,6 +168,17 @@ category = "main"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
 
+[[package]]
+name = "commonmark"
+version = "0.9.1"
+description = "Python parser for the CommonMark Markdown spec"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.extras]
+test = ["hypothesis (==3.55.3)", "flake8 (==3.7.8)"]
+
 [[package]]
 name = "coverage"
 version = "6.4.3"
@@ -240,6 +270,14 @@ category = "main"
 optional = false
 python-versions = ">=3.5"
 
+[[package]]
+name = "distlib"
+version = "0.3.5"
+description = "Distribution utilities"
+category = "dev"
+optional = false
+python-versions = "*"
+
 [[package]]
 name = "entrypoints"
 version = "0.4"
@@ -248,9 +286,17 @@ category = "main"
 optional = false
 python-versions = ">=3.6"
 
+[[package]]
+name = "eradicate"
+version = "2.1.0"
+description = "Removes commented-out code."
+category = "dev"
+optional = false
+python-versions = "*"
+
 [[package]]
 name = "executing"
-version = "0.9.1"
+version = "0.10.0"
 description = "Get the currently executing AST node of a frame, and other information"
 category = "main"
 optional = false
@@ -267,22 +313,111 @@ python-versions = "*"
 [package.extras]
 devel = ["colorama", "jsonschema", "json-spec", "pylint", "pytest", "pytest-benchmark", "pytest-cache", "validictory"]
 
+[[package]]
+name = "filelock"
+version = "3.8.0"
+description = "A platform independent file lock."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.extras]
+testing = ["pytest-timeout (>=2.1)", "pytest-cov (>=3)", "pytest (>=7.1.2)", "coverage (>=6.4.2)", "covdefaults (>=2.2)"]
+docs = ["sphinx-autodoc-typehints (>=1.19.1)", "sphinx (>=5.1.1)", "furo (>=2022.6.21)"]
+
 [[package]]
 name = "flake8"
-version = "4.0.1"
+version = "5.0.4"
 description = "the modular source code checker: pep8 pyflakes and co"
 category = "dev"
 optional = false
+python-versions = ">=3.6.1"
+
+[package.dependencies]
+mccabe = ">=0.7.0,<0.8.0"
+pycodestyle = ">=2.9.0,<2.10.0"
+pyflakes = ">=2.5.0,<2.6.0"
+
+[[package]]
+name = "flake8-bugbear"
+version = "22.7.1"
+description = "A plugin for flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pycodestyle."
+category = "dev"
+optional = false
 python-versions = ">=3.6"
 
 [package.dependencies]
-mccabe = ">=0.6.0,<0.7.0"
-pycodestyle = ">=2.8.0,<2.9.0"
-pyflakes = ">=2.4.0,<2.5.0"
+attrs = ">=19.2.0"
+flake8 = ">=3.0.0"
+
+[package.extras]
+dev = ["pre-commit", "hypothesmith (>=0.2)", "hypothesis", "coverage"]
+
+[[package]]
+name = "flake8-builtins"
+version = "1.5.3"
+description = "Check for python builtins being used as variables or parameters."
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+flake8 = "*"
+
+[package.extras]
+test = ["pytest-cov", "pytest", "mock", "coveralls", "coverage"]
+
+[[package]]
+name = "flake8-comprehensions"
+version = "3.10.0"
+description = "A flake8 plugin to help you write better list/set/dict comprehensions."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+flake8 = ">=3.0,<3.2.0 || >3.2.0"
+
+[[package]]
+name = "flake8-debugger"
+version = "4.1.2"
+description = "ipdb/pdb statement checker plugin for flake8"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+flake8 = ">=3.0"
+pycodestyle = "*"
+
+[[package]]
+name = "flake8-eradicate"
+version = "1.3.0"
+description = "Flake8 plugin to find commented out code"
+category = "dev"
+optional = false
+python-versions = ">=3.6,<4.0"
+
+[package.dependencies]
+attrs = "*"
+eradicate = ">=2.0,<3.0"
+flake8 = ">=3.5,<6"
+
+[[package]]
+name = "flake8-logging-format"
+version = "0.7.4"
+description = ""
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.extras]
+lint = ["flake8"]
+test = ["pytest", "pytest-cov", "pyhamcrest"]
 
 [[package]]
 name = "flask"
-version = "2.2.1"
+version = "2.2.2"
 description = "A simple framework for building complex web applications."
 category = "main"
 optional = false
@@ -293,11 +428,11 @@ click = ">=8.0"
 importlib-metadata = {version = ">=3.6.0", markers = "python_version < \"3.10\""}
 itsdangerous = ">=2.0"
 Jinja2 = ">=3.0"
-Werkzeug = ">=2.2.0"
+Werkzeug = ">=2.2.2"
 
 [package.extras]
-dotenv = ["python-dotenv"]
 async = ["asgiref (>=3.2)"]
+dotenv = ["python-dotenv"]
 
 [[package]]
 name = "flask-compress"
@@ -311,6 +446,39 @@ python-versions = "*"
 brotli = "*"
 flask = "*"
 
+[[package]]
+name = "gitdb"
+version = "4.0.9"
+description = "Git Object Database"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+smmap = ">=3.0.1,<6"
+
+[[package]]
+name = "gitpython"
+version = "3.1.27"
+description = "GitPython is a python library used to interact with Git repositories"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+gitdb = ">=4.0.1,<5"
+
+[[package]]
+name = "identify"
+version = "2.5.3"
+description = "File identification library for Python"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.extras]
+license = ["ukkonen"]
+
 [[package]]
 name = "idna"
 version = "3.3"
@@ -417,6 +585,20 @@ doc = ["Sphinx (>=1.3)"]
 black = ["black"]
 all = ["trio", "pandas", "numpy (>=1.19)", "matplotlib (!=3.2.0)", "curio", "testpath", "pytest-asyncio", "pytest (<7.1)", "qtconsole", "ipyparallel", "notebook", "ipywidgets", "nbformat", "nbconvert", "ipykernel", "Sphinx (>=1.3)", "black"]
 
+[[package]]
+name = "isort"
+version = "5.10.1"
+description = "A Python utility / library to sort Python imports."
+category = "dev"
+optional = false
+python-versions = ">=3.6.1,<4.0"
+
+[package.extras]
+pipfile_deprecated_finder = ["pipreqs", "requirementslib"]
+requirements_deprecated_finder = ["pipreqs", "pip-api"]
+colors = ["colorama (>=0.4.3,<0.5.0)"]
+plugins = ["setuptools"]
+
 [[package]]
 name = "itsdangerous"
 version = "2.1.2"
@@ -550,11 +732,11 @@ traitlets = "*"
 
 [[package]]
 name = "mccabe"
-version = "0.6.1"
+version = "0.7.0"
 description = "McCabe checker, plugin for flake8"
 category = "dev"
 optional = false
-python-versions = "*"
+python-versions = ">=3.6"
 
 [[package]]
 name = "mypy"
@@ -618,9 +800,17 @@ category = "main"
 optional = false
 python-versions = ">=3.5"
 
+[[package]]
+name = "nodeenv"
+version = "1.7.0"
+description = "Node.js virtual environment builder"
+category = "dev"
+optional = false
+python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*"
+
 [[package]]
 name = "numpy"
-version = "1.23.1"
+version = "1.23.2"
 description = "NumPy is the fundamental package for array computing with Python."
 category = "main"
 optional = false
@@ -678,6 +868,17 @@ category = "dev"
 optional = false
 python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
 
+[[package]]
+name = "pep8-naming"
+version = "0.13.1"
+description = "Check PEP-8 naming conventions, plugin for flake8"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+flake8 = ">=3.9.1"
+
 [[package]]
 name = "pexpect"
 version = "4.8.0"
@@ -719,7 +920,7 @@ docs = ["sphinx (>=4)", "sphinx-autodoc-typehints (>=1.12)", "proselint (>=0.10.
 
 [[package]]
 name = "plotly"
-version = "5.9.0"
+version = "5.10.0"
 description = "An open-source, interactive data visualization library for Python"
 category = "main"
 optional = false
@@ -740,6 +941,22 @@ python-versions = ">=3.6"
 testing = ["pytest-benchmark", "pytest"]
 dev = ["tox", "pre-commit"]
 
+[[package]]
+name = "pre-commit"
+version = "2.20.0"
+description = "A framework for managing and maintaining multi-language pre-commit hooks."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+cfgv = ">=2.0.0"
+identify = ">=1.0.0"
+nodeenv = ">=0.11.1"
+pyyaml = ">=5.1"
+toml = "*"
+virtualenv = ">=20.0.8"
+
 [[package]]
 name = "prompt-toolkit"
 version = "3.0.30"
@@ -800,13 +1017,24 @@ python-versions = ">=3.7"
 [package.dependencies]
 numpy = ">=1.16.6"
 
+[[package]]
+name = "pychalk"
+version = "2.0.1"
+description = "Color printing in python"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+six = ">=1.11.0"
+
 [[package]]
 name = "pycodestyle"
-version = "2.8.0"
+version = "2.9.1"
 description = "Python style guide checker"
 category = "dev"
 optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+python-versions = ">=3.6"
 
 [[package]]
 name = "pycparser"
@@ -818,7 +1046,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
 
 [[package]]
 name = "pydantic"
-version = "1.9.1"
+version = "1.9.2"
 description = "Data validation and settings management using python type hints"
 category = "main"
 optional = false
@@ -833,11 +1061,11 @@ email = ["email-validator (>=1.0.3)"]
 
 [[package]]
 name = "pyflakes"
-version = "2.4.0"
+version = "2.5.0"
 description = "passive checker of Python programs"
 category = "dev"
 optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+python-versions = ">=3.6"
 
 [[package]]
 name = "pygments"
@@ -902,6 +1130,22 @@ pytest = ">=4.6"
 [package.extras]
 testing = ["virtualenv", "pytest-xdist", "six", "process-tests", "hunter", "fields"]
 
+[[package]]
+name = "python-box"
+version = "6.0.2"
+description = "Advanced Python dictionaries with dot notation access"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.extras]
+yaml = ["ruamel.yaml (>=0.17)"]
+toml = ["toml"]
+"ruamel.yaml" = ["ruamel.yaml (>=0.17)"]
+msgpack = ["msgpack"]
+all = ["msgpack", "toml", "ruamel.yaml (>=0.17)"]
+pyyaml = ["pyyaml"]
+
 [[package]]
 name = "python-dateutil"
 version = "2.8.2"
@@ -913,14 +1157,41 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
 [package.dependencies]
 six = ">=1.5"
 
+[[package]]
+name = "python-kacl"
+version = "0.2.30"
+description = "Python module and CLI tool for validating and modifying Changelogs in \"keep-a-changelog\" format\""
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+click = "*"
+gitpython = "*"
+pychalk = "*"
+python-box = "*"
+pyyaml = "*"
+semver = "*"
+
 [[package]]
 name = "pytz"
-version = "2022.1"
+version = "2022.2.1"
 description = "World timezone definitions, modern and historical"
 category = "main"
 optional = false
 python-versions = "*"
 
+[[package]]
+name = "pyupgrade"
+version = "2.37.3"
+description = "A tool to automatically upgrade syntax for newer versions."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+tokenize-rt = ">=3.2.0"
+
 [[package]]
 name = "pywin32"
 version = "304"
@@ -929,9 +1200,17 @@ category = "main"
 optional = false
 python-versions = "*"
 
+[[package]]
+name = "pyyaml"
+version = "6.0"
+description = "YAML parser and emitter for Python"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
 [[package]]
 name = "pyzmq"
-version = "23.2.0"
+version = "23.2.1"
 description = "Python bindings for 0MQ"
 category = "main"
 optional = false
@@ -970,6 +1249,30 @@ python-versions = "*"
 [package.dependencies]
 six = ">=1.7.0"
 
+[[package]]
+name = "rich"
+version = "12.5.1"
+description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
+category = "dev"
+optional = false
+python-versions = ">=3.6.3,<4.0.0"
+
+[package.dependencies]
+commonmark = ">=0.9.0,<0.10.0"
+pygments = ">=2.6.0,<3.0.0"
+typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9\""}
+
+[package.extras]
+jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"]
+
+[[package]]
+name = "semver"
+version = "2.13.0"
+description = "Python helper for Semantic Versioning (http://semver.org/)"
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
 [[package]]
 name = "six"
 version = "1.16.0"
@@ -978,10 +1281,18 @@ category = "main"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
 
+[[package]]
+name = "smmap"
+version = "5.0.0"
+description = "A pure Python implementation of a sliding window memory map manager"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
 [[package]]
 name = "ssb-datadoc-model"
-version = "0.1.0"
-description = "Data Model for use in SSB's Metadata system"
+version = "0.1.3"
+description = "Data Model for use in Statistics Norway's Metadata system"
 category = "main"
 optional = false
 python-versions = ">=3.8,<4.0"
@@ -991,7 +1302,7 @@ pydantic = ">=1.9.1,<2.0.0"
 
 [[package]]
 name = "stack-data"
-version = "0.3.0"
+version = "0.4.0"
 description = "Extract data from python stack frames and tracebacks for informative displays"
 category = "main"
 optional = false
@@ -1016,6 +1327,22 @@ python-versions = ">=3.6"
 [package.extras]
 doc = ["reno", "sphinx", "tornado (>=4.5)"]
 
+[[package]]
+name = "tokenize-rt"
+version = "4.2.1"
+description = "A wrapper around the stdlib `tokenize` which roundtrips."
+category = "dev"
+optional = false
+python-versions = ">=3.6.1"
+
+[[package]]
+name = "toml"
+version = "0.10.2"
+description = "Python Library for Tom's Obvious, Minimal Language"
+category = "dev"
+optional = false
+python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
+
 [[package]]
 name = "tomli"
 version = "2.0.1"
@@ -1043,6 +1370,19 @@ python-versions = ">=3.7"
 [package.extras]
 test = ["pytest", "pre-commit"]
 
+[[package]]
+name = "tryceratops"
+version = "1.1.0"
+description = "A linter to manage your exception like a PRO!"
+category = "dev"
+optional = false
+python-versions = ">=3.8,<4.0"
+
+[package.dependencies]
+click = ">=7"
+rich = ">=10.14.0"
+toml = ">=0.10.2"
+
 [[package]]
 name = "typing-extensions"
 version = "4.3.0"
@@ -1064,6 +1404,23 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
 secure = ["ipaddress", "certifi", "idna (>=2.0.0)", "cryptography (>=1.3.4)", "pyOpenSSL (>=0.14)"]
 brotli = ["brotlipy (>=0.6.0)", "brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"]
 
+[[package]]
+name = "virtualenv"
+version = "20.16.3"
+description = "Virtual Python Environment builder"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+distlib = ">=0.3.5,<1"
+filelock = ">=3.4.1,<4"
+platformdirs = ">=2.4,<3"
+
+[package.extras]
+testing = ["pytest-timeout (>=2.1)", "pytest-randomly (>=3.10.3)", "pytest-mock (>=3.6.1)", "pytest-freezegun (>=0.4.2)", "pytest-env (>=0.6.2)", "pytest (>=7.0.1)", "packaging (>=21.3)", "flaky (>=3.7)", "coverage-enable-subprocess (>=1)", "coverage (>=6.2)"]
+docs = ["towncrier (>=21.9)", "sphinx-rtd-theme (>=1)", "sphinx-argparse (>=0.3.1)", "sphinx (>=5.1.1)", "proselint (>=0.13)"]
+
 [[package]]
 name = "wcwidth"
 version = "0.2.5"
@@ -1074,7 +1431,7 @@ python-versions = "*"
 
 [[package]]
 name = "werkzeug"
-version = "2.2.1"
+version = "2.2.2"
 description = "The comprehensive WSGI web application library."
 category = "main"
 optional = false
@@ -1101,7 +1458,7 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-
 [metadata]
 lock-version = "1.1"
 python-versions = "^3.8"
-content-hash = "1f55be03c20851a9b664bba0ba07fcde2bffc421c48bba455279b920e1adf4a9"
+content-hash = "2a65190f68d7f15f7e385d993d056600334479d89a8e009f6a7fec38cde18d58"
 
 [metadata.files]
 ansi2html = [
@@ -1113,8 +1470,8 @@ appnope = [
     {file = "appnope-0.1.3.tar.gz", hash = "sha256:02bd91c4de869fbb1e1c50aafc4098827a7a54ab2f39d9dcba6c9547ed920e24"},
 ]
 asttokens = [
-    {file = "asttokens-2.0.5-py2.py3-none-any.whl", hash = "sha256:0844691e88552595a6f4a4281a9f7f79b8dd45ca4ccea82e5e05b4bbdb76705c"},
-    {file = "asttokens-2.0.5.tar.gz", hash = "sha256:9a54c114f02c7a9480d56550932546a3f1fe71d8a02f1bc7ccd0ee3ee35cf4d5"},
+    {file = "asttokens-2.0.8-py2.py3-none-any.whl", hash = "sha256:e3305297c744ae53ffa032c45dc347286165e4ffce6875dc662b205db0623d86"},
+    {file = "asttokens-2.0.8.tar.gz", hash = "sha256:c61e16246ecfb2cde2958406b4c8ebc043c9e6d73aaa83c941673b35e5d3a76b"},
 ]
 atomicwrites = [
     {file = "atomicwrites-1.4.1.tar.gz", hash = "sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11"},
@@ -1123,6 +1480,9 @@ attrs = [
     {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"},
     {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"},
 ]
+autoflake = [
+    {file = "autoflake-1.4.tar.gz", hash = "sha256:61a353012cff6ab94ca062823d1fb2f692c4acda51c76ff83a8d77915fba51ea"},
+]
 backcall = [
     {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"},
     {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"},
@@ -1290,6 +1650,10 @@ cffi = [
     {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"},
     {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"},
 ]
+cfgv = [
+    {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"},
+    {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"},
+]
 charset-normalizer = [
     {file = "charset-normalizer-2.1.0.tar.gz", hash = "sha256:575e708016ff3a5e3681541cb9d79312c416835686d054a23accb873b254f413"},
     {file = "charset_normalizer-2.1.0-py3-none-any.whl", hash = "sha256:5189b6f22b01957427f35b6a08d9a0bc45b46d3788ef5a92e978433c7a35f8a5"},
@@ -1302,6 +1666,10 @@ colorama = [
     {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"},
     {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"},
 ]
+commonmark = [
+    {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"},
+    {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"},
+]
 coverage = [
     {file = "coverage-6.4.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f50d3a822947572496ea922ee7825becd8e3ae6fbd2400cd8236b7d64b17f285"},
     {file = "coverage-6.4.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d5191d53afbe5b6059895fa7f58223d3751c42b8101fb3ce767e1a0b1a1d8f87"},
@@ -1389,30 +1757,77 @@ decorator = [
     {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"},
     {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"},
 ]
+distlib = [
+    {file = "distlib-0.3.5-py2.py3-none-any.whl", hash = "sha256:b710088c59f06338ca514800ad795a132da19fda270e3ce4affc74abf955a26c"},
+    {file = "distlib-0.3.5.tar.gz", hash = "sha256:a7f75737c70be3b25e2bee06288cec4e4c221de18455b2dd037fe2a795cab2fe"},
+]
 entrypoints = [
     {file = "entrypoints-0.4-py3-none-any.whl", hash = "sha256:f174b5ff827504fd3cd97cc3f8649f3693f51538c7e4bdf3ef002c8429d42f9f"},
     {file = "entrypoints-0.4.tar.gz", hash = "sha256:b706eddaa9218a19ebcd67b56818f05bb27589b1ca9e8d797b74affad4ccacd4"},
 ]
+eradicate = [
+    {file = "eradicate-2.1.0-py3-none-any.whl", hash = "sha256:8bfaca181db9227dc88bdbce4d051a9627604c2243e7d85324f6d6ce0fd08bb2"},
+    {file = "eradicate-2.1.0.tar.gz", hash = "sha256:aac7384ab25b1bf21c4c012de9b4bf8398945a14c98c911545b2ea50ab558014"},
+]
 executing = [
-    {file = "executing-0.9.1-py2.py3-none-any.whl", hash = "sha256:4ce4d6082d99361c0231fc31ac1a0f56979363cc6819de0b1410784f99e49105"},
-    {file = "executing-0.9.1.tar.gz", hash = "sha256:ea278e2cf90cbbacd24f1080dd1f0ac25b71b2e21f50ab439b7ba45dd3195587"},
+    {file = "executing-0.10.0-py2.py3-none-any.whl", hash = "sha256:9c745f80cda11eb22b62cbecf21156491a794eb56ab06f9d286a44e62822b24e"},
+    {file = "executing-0.10.0.tar.gz", hash = "sha256:d1cd87c2e371e9966261410c5b3769d6df2f9e4a79a83eebd2662dd3388f9833"},
 ]
 fastjsonschema = [
     {file = "fastjsonschema-2.16.1-py3-none-any.whl", hash = "sha256:2f7158c4de792555753d6c2277d6a2af2d406dfd97aeca21d17173561ede4fe6"},
     {file = "fastjsonschema-2.16.1.tar.gz", hash = "sha256:d6fa3ffbe719768d70e298b9fb847484e2bdfdb7241ed052b8d57a9294a8c334"},
 ]
+filelock = [
+    {file = "filelock-3.8.0-py3-none-any.whl", hash = "sha256:617eb4e5eedc82fc5f47b6d61e4d11cb837c56cb4544e39081099fa17ad109d4"},
+    {file = "filelock-3.8.0.tar.gz", hash = "sha256:55447caa666f2198c5b6b13a26d2084d26fa5b115c00d065664b2124680c4edc"},
+]
 flake8 = [
-    {file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"},
-    {file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"},
+    {file = "flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248"},
+    {file = "flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db"},
+]
+flake8-bugbear = [
+    {file = "flake8-bugbear-22.7.1.tar.gz", hash = "sha256:e450976a07e4f9d6c043d4f72b17ec1baf717fe37f7997009c8ae58064f88305"},
+    {file = "flake8_bugbear-22.7.1-py3-none-any.whl", hash = "sha256:db5d7a831ef4412a224b26c708967ff816818cabae415e76b8c58df156c4b8e5"},
+]
+flake8-builtins = [
+    {file = "flake8-builtins-1.5.3.tar.gz", hash = "sha256:09998853b2405e98e61d2ff3027c47033adbdc17f9fe44ca58443d876eb00f3b"},
+    {file = "flake8_builtins-1.5.3-py2.py3-none-any.whl", hash = "sha256:7706babee43879320376861897e5d1468e396a40b8918ed7bccf70e5f90b8687"},
+]
+flake8-comprehensions = [
+    {file = "flake8-comprehensions-3.10.0.tar.gz", hash = "sha256:181158f7e7aa26a63a0a38e6017cef28c6adee71278ce56ce11f6ec9c4905058"},
+    {file = "flake8_comprehensions-3.10.0-py3-none-any.whl", hash = "sha256:dad454fd3d525039121e98fa1dd90c46bc138708196a4ebbc949ad3c859adedb"},
+]
+flake8-debugger = [
+    {file = "flake8-debugger-4.1.2.tar.gz", hash = "sha256:52b002560941e36d9bf806fca2523dc7fb8560a295d5f1a6e15ac2ded7a73840"},
+    {file = "flake8_debugger-4.1.2-py3-none-any.whl", hash = "sha256:0a5e55aeddcc81da631ad9c8c366e7318998f83ff00985a49e6b3ecf61e571bf"},
+]
+flake8-eradicate = [
+    {file = "flake8-eradicate-1.3.0.tar.gz", hash = "sha256:e4c98f00d17dc8653e3388cac2624cd81e9735de2fd4a8dcf99029633ebd7a63"},
+    {file = "flake8_eradicate-1.3.0-py3-none-any.whl", hash = "sha256:85a71e0c5f4e07f7c6c5fec520483561fd6bd295417d622855bdeade99242e3d"},
+]
+flake8-logging-format = [
+    {file = "flake8-logging-format-0.7.4.tar.gz", hash = "sha256:d867b46a8a2da5d6b70aa115022a104cf6dae6a671637aae2ef3eb6eac4235dd"},
 ]
 flask = [
-    {file = "Flask-2.2.1-py3-none-any.whl", hash = "sha256:3c604c48c3d5b4c63e72134044c0b4fe90ff01ef65280b9fe2d38c8860d99fe5"},
-    {file = "Flask-2.2.1.tar.gz", hash = "sha256:9c2b81b9b1edcc835af72d600f1955e713a065e7cb41d7e51ee762b449d9c65d"},
+    {file = "Flask-2.2.2-py3-none-any.whl", hash = "sha256:b9c46cc36662a7949f34b52d8ec7bb59c0d74ba08ba6cb9ce9adc1d8676d9526"},
+    {file = "Flask-2.2.2.tar.gz", hash = "sha256:642c450d19c4ad482f96729bd2a8f6d32554aa1e231f4f6b4e7e5264b16cca2b"},
 ]
 flask-compress = [
     {file = "Flask-Compress-1.12.tar.gz", hash = "sha256:e2159499f39d618a4d56ba0484e7b58b57956b9a2c6d3510f095f5bb14b7afc5"},
     {file = "Flask_Compress-1.12-py3-none-any.whl", hash = "sha256:9f4e40211755e86f85e5eb7d414856ef1e8751912caa78d62853169400335f0c"},
 ]
+gitdb = [
+    {file = "gitdb-4.0.9-py3-none-any.whl", hash = "sha256:8033ad4e853066ba6ca92050b9df2f89301b8fc8bf7e9324d412a63f8bf1a8fd"},
+    {file = "gitdb-4.0.9.tar.gz", hash = "sha256:bac2fd45c0a1c9cf619e63a90d62bdc63892ef92387424b855792a6cabe789aa"},
+]
+gitpython = [
+    {file = "GitPython-3.1.27-py3-none-any.whl", hash = "sha256:5b68b000463593e05ff2b261acff0ff0972df8ab1b70d3cdbd41b546c8b8fc3d"},
+    {file = "GitPython-3.1.27.tar.gz", hash = "sha256:1c885ce809e8ba2d88a29befeb385fcea06338d3640712b59ca623c220bb5704"},
+]
+identify = [
+    {file = "identify-2.5.3-py2.py3-none-any.whl", hash = "sha256:25851c8c1370effb22aaa3c987b30449e9ff0cece408f810ae6ce408fdd20893"},
+    {file = "identify-2.5.3.tar.gz", hash = "sha256:887e7b91a1be152b0d46bbf072130235a8117392b9f1828446079a816a05ef44"},
+]
 idna = [
     {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"},
     {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"},
@@ -1437,6 +1852,10 @@ ipython = [
     {file = "ipython-8.4.0-py3-none-any.whl", hash = "sha256:7ca74052a38fa25fe9bedf52da0be7d3fdd2fb027c3b778ea78dfe8c212937d1"},
     {file = "ipython-8.4.0.tar.gz", hash = "sha256:f2db3a10254241d9b447232cec8b424847f338d9d36f9a577a6192c332a46abd"},
 ]
+isort = [
+    {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"},
+    {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"},
+]
 itsdangerous = [
     {file = "itsdangerous-2.1.2-py3-none-any.whl", hash = "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44"},
     {file = "itsdangerous-2.1.2.tar.gz", hash = "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a"},
@@ -1512,8 +1931,8 @@ matplotlib-inline = [
     {file = "matplotlib_inline-0.1.3-py3-none-any.whl", hash = "sha256:aed605ba3b72462d64d475a21a9296f400a19c4f74a31b59103d2a99ffd5aa5c"},
 ]
 mccabe = [
-    {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"},
-    {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"},
+    {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"},
+    {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"},
 ]
 mypy = [
     {file = "mypy-0.950-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cf9c261958a769a3bd38c3e133801ebcd284ffb734ea12d01457cb09eacf7d7b"},
@@ -1556,29 +1975,39 @@ nest-asyncio = [
     {file = "nest_asyncio-1.5.5-py3-none-any.whl", hash = "sha256:b98e3ec1b246135e4642eceffa5a6c23a3ab12c82ff816a92c612d68205813b2"},
     {file = "nest_asyncio-1.5.5.tar.gz", hash = "sha256:e442291cd942698be619823a17a86a5759eabe1f8613084790de189fe9e16d65"},
 ]
+nodeenv = [
+    {file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"},
+    {file = "nodeenv-1.7.0.tar.gz", hash = "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"},
+]
 numpy = [
-    {file = "numpy-1.23.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b15c3f1ed08df4980e02cc79ee058b788a3d0bef2fb3c9ca90bb8cbd5b8a3a04"},
-    {file = "numpy-1.23.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9ce242162015b7e88092dccd0e854548c0926b75c7924a3495e02c6067aba1f5"},
-    {file = "numpy-1.23.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0d7447679ae9a7124385ccf0ea990bb85bb869cef217e2ea6c844b6a6855073"},
-    {file = "numpy-1.23.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3119daed207e9410eaf57dcf9591fdc68045f60483d94956bee0bfdcba790953"},
-    {file = "numpy-1.23.1-cp310-cp310-win32.whl", hash = "sha256:3ab67966c8d45d55a2bdf40701536af6443763907086c0a6d1232688e27e5447"},
-    {file = "numpy-1.23.1-cp310-cp310-win_amd64.whl", hash = "sha256:1865fdf51446839ca3fffaab172461f2b781163f6f395f1aed256b1ddc253622"},
-    {file = "numpy-1.23.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:aeba539285dcf0a1ba755945865ec61240ede5432df41d6e29fab305f4384db2"},
-    {file = "numpy-1.23.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7e8229f3687cdadba2c4faef39204feb51ef7c1a9b669247d49a24f3e2e1617c"},
-    {file = "numpy-1.23.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68b69f52e6545af010b76516f5daaef6173e73353e3295c5cb9f96c35d755641"},
-    {file = "numpy-1.23.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1408c3527a74a0209c781ac82bde2182b0f0bf54dea6e6a363fe0cc4488a7ce7"},
-    {file = "numpy-1.23.1-cp38-cp38-win32.whl", hash = "sha256:47f10ab202fe4d8495ff484b5561c65dd59177949ca07975663f4494f7269e3e"},
-    {file = "numpy-1.23.1-cp38-cp38-win_amd64.whl", hash = "sha256:37e5ebebb0eb54c5b4a9b04e6f3018e16b8ef257d26c8945925ba8105008e645"},
-    {file = "numpy-1.23.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:173f28921b15d341afadf6c3898a34f20a0569e4ad5435297ba262ee8941e77b"},
-    {file = "numpy-1.23.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:876f60de09734fbcb4e27a97c9a286b51284df1326b1ac5f1bf0ad3678236b22"},
-    {file = "numpy-1.23.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35590b9c33c0f1c9732b3231bb6a72d1e4f77872390c47d50a615686ae7ed3fd"},
-    {file = "numpy-1.23.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a35c4e64dfca659fe4d0f1421fc0f05b8ed1ca8c46fb73d9e5a7f175f85696bb"},
-    {file = "numpy-1.23.1-cp39-cp39-win32.whl", hash = "sha256:c2f91f88230042a130ceb1b496932aa717dcbd665350beb821534c5c7e15881c"},
-    {file = "numpy-1.23.1-cp39-cp39-win_amd64.whl", hash = "sha256:37ece2bd095e9781a7156852e43d18044fd0d742934833335599c583618181b9"},
-    {file = "numpy-1.23.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:8002574a6b46ac3b5739a003b5233376aeac5163e5dcd43dd7ad062f3e186129"},
-    {file = "numpy-1.23.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d732d17b8a9061540a10fda5bfeabca5785700ab5469a5e9b93aca5e2d3a5fb"},
-    {file = "numpy-1.23.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:55df0f7483b822855af67e38fb3a526e787adf189383b4934305565d71c4b148"},
-    {file = "numpy-1.23.1.tar.gz", hash = "sha256:d748ef349bfef2e1194b59da37ed5a29c19ea8d7e6342019921ba2ba4fd8b624"},
+    {file = "numpy-1.23.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e603ca1fb47b913942f3e660a15e55a9ebca906857edfea476ae5f0fe9b457d5"},
+    {file = "numpy-1.23.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:633679a472934b1c20a12ed0c9a6c9eb167fbb4cb89031939bfd03dd9dbc62b8"},
+    {file = "numpy-1.23.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:17e5226674f6ea79e14e3b91bfbc153fdf3ac13f5cc54ee7bc8fdbe820a32da0"},
+    {file = "numpy-1.23.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdc02c0235b261925102b1bd586579b7158e9d0d07ecb61148a1799214a4afd5"},
+    {file = "numpy-1.23.2-cp310-cp310-win32.whl", hash = "sha256:df28dda02c9328e122661f399f7655cdcbcf22ea42daa3650a26bce08a187450"},
+    {file = "numpy-1.23.2-cp310-cp310-win_amd64.whl", hash = "sha256:8ebf7e194b89bc66b78475bd3624d92980fca4e5bb86dda08d677d786fefc414"},
+    {file = "numpy-1.23.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dc76bca1ca98f4b122114435f83f1fcf3c0fe48e4e6f660e07996abf2f53903c"},
+    {file = "numpy-1.23.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ecfdd68d334a6b97472ed032b5b37a30d8217c097acfff15e8452c710e775524"},
+    {file = "numpy-1.23.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5593f67e66dea4e237f5af998d31a43e447786b2154ba1ad833676c788f37cde"},
+    {file = "numpy-1.23.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac987b35df8c2a2eab495ee206658117e9ce867acf3ccb376a19e83070e69418"},
+    {file = "numpy-1.23.2-cp311-cp311-win32.whl", hash = "sha256:d98addfd3c8728ee8b2c49126f3c44c703e2b005d4a95998e2167af176a9e722"},
+    {file = "numpy-1.23.2-cp311-cp311-win_amd64.whl", hash = "sha256:8ecb818231afe5f0f568c81f12ce50f2b828ff2b27487520d85eb44c71313b9e"},
+    {file = "numpy-1.23.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:909c56c4d4341ec8315291a105169d8aae732cfb4c250fbc375a1efb7a844f8f"},
+    {file = "numpy-1.23.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8247f01c4721479e482cc2f9f7d973f3f47810cbc8c65e38fd1bbd3141cc9842"},
+    {file = "numpy-1.23.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8b97a8a87cadcd3f94659b4ef6ec056261fa1e1c3317f4193ac231d4df70215"},
+    {file = "numpy-1.23.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd5b7ccae24e3d8501ee5563e82febc1771e73bd268eef82a1e8d2b4d556ae66"},
+    {file = "numpy-1.23.2-cp38-cp38-win32.whl", hash = "sha256:9b83d48e464f393d46e8dd8171687394d39bc5abfe2978896b77dc2604e8635d"},
+    {file = "numpy-1.23.2-cp38-cp38-win_amd64.whl", hash = "sha256:dec198619b7dbd6db58603cd256e092bcadef22a796f778bf87f8592b468441d"},
+    {file = "numpy-1.23.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4f41f5bf20d9a521f8cab3a34557cd77b6f205ab2116651f12959714494268b0"},
+    {file = "numpy-1.23.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:806cc25d5c43e240db709875e947076b2826f47c2c340a5a2f36da5bb10c58d6"},
+    {file = "numpy-1.23.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f9d84a24889ebb4c641a9b99e54adb8cab50972f0166a3abc14c3b93163f074"},
+    {file = "numpy-1.23.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c403c81bb8ffb1c993d0165a11493fd4bf1353d258f6997b3ee288b0a48fce77"},
+    {file = "numpy-1.23.2-cp39-cp39-win32.whl", hash = "sha256:cf8c6aed12a935abf2e290860af8e77b26a042eb7f2582ff83dc7ed5f963340c"},
+    {file = "numpy-1.23.2-cp39-cp39-win_amd64.whl", hash = "sha256:5e28cd64624dc2354a349152599e55308eb6ca95a13ce6a7d5679ebff2962913"},
+    {file = "numpy-1.23.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:806970e69106556d1dd200e26647e9bee5e2b3f1814f9da104a943e8d548ca38"},
+    {file = "numpy-1.23.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bd879d3ca4b6f39b7770829f73278b7c5e248c91d538aab1e506c628353e47f"},
+    {file = "numpy-1.23.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:be6b350dfbc7f708d9d853663772a9310783ea58f6035eec649fb9c4371b5389"},
+    {file = "numpy-1.23.2.tar.gz", hash = "sha256:b78d00e48261fbbd04aa0d7427cf78d18401ee0abd89c7559bbf422e5b1c7d01"},
 ]
 packaging = [
     {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
@@ -1615,6 +2044,10 @@ pathspec = [
     {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"},
     {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"},
 ]
+pep8-naming = [
+    {file = "pep8-naming-0.13.1.tar.gz", hash = "sha256:3af77cdaa9c7965f7c85a56cd579354553c9bbd3fdf3078a776f12db54dd6944"},
+    {file = "pep8_naming-0.13.1-py3-none-any.whl", hash = "sha256:f7867c1a464fe769be4f972ef7b79d6df1d9aff1b1f04ecf738d471963d3ab9c"},
+]
 pexpect = [
     {file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"},
     {file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"},
@@ -1632,13 +2065,17 @@ platformdirs = [
     {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"},
 ]
 plotly = [
-    {file = "plotly-5.9.0-py2.py3-none-any.whl", hash = "sha256:9acf168915101caac82ed6da3390840c622d9d4e4a78cbe1260f52501efc3117"},
-    {file = "plotly-5.9.0.tar.gz", hash = "sha256:b0536e72bbc0b3cf169ac1fd00759d77aae7bb12ae378cdc75c5dc362f5de576"},
+    {file = "plotly-5.10.0-py2.py3-none-any.whl", hash = "sha256:989b13825cc974390aa0169479485d9257d37848a47bc63957251f8e1a7046ba"},
+    {file = "plotly-5.10.0.tar.gz", hash = "sha256:4d36d9859b7a153b273562deeed8c292587a472eb1fd57cd4158ec89d9defadb"},
 ]
 pluggy = [
     {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
     {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
 ]
+pre-commit = [
+    {file = "pre_commit-2.20.0-py2.py3-none-any.whl", hash = "sha256:51a5ba7c480ae8072ecdb6933df22d2f812dc897d5fe848778116129a681aac7"},
+    {file = "pre_commit-2.20.0.tar.gz", hash = "sha256:a978dac7bc9ec0bcee55c18a277d553b0f419d259dadb4b9418ff2d00eb43959"},
+]
 prompt-toolkit = [
     {file = "prompt_toolkit-3.0.30-py3-none-any.whl", hash = "sha256:d8916d3f62a7b67ab353a952ce4ced6a1d2587dfe9ef8ebc30dd7c386751f289"},
     {file = "prompt_toolkit-3.0.30.tar.gz", hash = "sha256:859b283c50bde45f5f97829f77a4674d1c1fcd88539364f1b28a37805cfd89c0"},
@@ -1721,54 +2158,57 @@ pyarrow = [
     {file = "pyarrow-8.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:cb06cacc19f3b426681f2f6803cc06ff481e7fe5b3a533b406bc5b2138843d4f"},
     {file = "pyarrow-8.0.0.tar.gz", hash = "sha256:4a18a211ed888f1ac0b0ebcb99e2d9a3e913a481120ee9b1fe33d3fedb945d4e"},
 ]
+pychalk = [
+    {file = "pychalk-2.0.1.tar.gz", hash = "sha256:f763275f6fa68835a30d22c2449f73724d569f33532a031d26e32edc604e7e39"},
+]
 pycodestyle = [
-    {file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"},
-    {file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"},
+    {file = "pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"},
+    {file = "pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785"},
 ]
 pycparser = [
     {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"},
     {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"},
 ]
 pydantic = [
-    {file = "pydantic-1.9.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c8098a724c2784bf03e8070993f6d46aa2eeca031f8d8a048dff277703e6e193"},
-    {file = "pydantic-1.9.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c320c64dd876e45254bdd350f0179da737463eea41c43bacbee9d8c9d1021f11"},
-    {file = "pydantic-1.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18f3e912f9ad1bdec27fb06b8198a2ccc32f201e24174cec1b3424dda605a310"},
-    {file = "pydantic-1.9.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c11951b404e08b01b151222a1cb1a9f0a860a8153ce8334149ab9199cd198131"},
-    {file = "pydantic-1.9.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8bc541a405423ce0e51c19f637050acdbdf8feca34150e0d17f675e72d119580"},
-    {file = "pydantic-1.9.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e565a785233c2d03724c4dc55464559639b1ba9ecf091288dd47ad9c629433bd"},
-    {file = "pydantic-1.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:a4a88dcd6ff8fd47c18b3a3709a89adb39a6373f4482e04c1b765045c7e282fd"},
-    {file = "pydantic-1.9.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:447d5521575f18e18240906beadc58551e97ec98142266e521c34968c76c8761"},
-    {file = "pydantic-1.9.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:985ceb5d0a86fcaa61e45781e567a59baa0da292d5ed2e490d612d0de5796918"},
-    {file = "pydantic-1.9.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059b6c1795170809103a1538255883e1983e5b831faea6558ef873d4955b4a74"},
-    {file = "pydantic-1.9.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:d12f96b5b64bec3f43c8e82b4aab7599d0157f11c798c9f9c528a72b9e0b339a"},
-    {file = "pydantic-1.9.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:ae72f8098acb368d877b210ebe02ba12585e77bd0db78ac04a1ee9b9f5dd2166"},
-    {file = "pydantic-1.9.1-cp36-cp36m-win_amd64.whl", hash = "sha256:79b485767c13788ee314669008d01f9ef3bc05db9ea3298f6a50d3ef596a154b"},
-    {file = "pydantic-1.9.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:494f7c8537f0c02b740c229af4cb47c0d39840b829ecdcfc93d91dcbb0779892"},
-    {file = "pydantic-1.9.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0f047e11febe5c3198ed346b507e1d010330d56ad615a7e0a89fae604065a0e"},
-    {file = "pydantic-1.9.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:969dd06110cb780da01336b281f53e2e7eb3a482831df441fb65dd30403f4608"},
-    {file = "pydantic-1.9.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:177071dfc0df6248fd22b43036f936cfe2508077a72af0933d0c1fa269b18537"},
-    {file = "pydantic-1.9.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:9bcf8b6e011be08fb729d110f3e22e654a50f8a826b0575c7196616780683380"},
-    {file = "pydantic-1.9.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a955260d47f03df08acf45689bd163ed9df82c0e0124beb4251b1290fa7ae728"},
-    {file = "pydantic-1.9.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9ce157d979f742a915b75f792dbd6aa63b8eccaf46a1005ba03aa8a986bde34a"},
-    {file = "pydantic-1.9.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0bf07cab5b279859c253d26a9194a8906e6f4a210063b84b433cf90a569de0c1"},
-    {file = "pydantic-1.9.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d93d4e95eacd313d2c765ebe40d49ca9dd2ed90e5b37d0d421c597af830c195"},
-    {file = "pydantic-1.9.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1542636a39c4892c4f4fa6270696902acb186a9aaeac6f6cf92ce6ae2e88564b"},
-    {file = "pydantic-1.9.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a9af62e9b5b9bc67b2a195ebc2c2662fdf498a822d62f902bf27cccb52dbbf49"},
-    {file = "pydantic-1.9.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fe4670cb32ea98ffbf5a1262f14c3e102cccd92b1869df3bb09538158ba90fe6"},
-    {file = "pydantic-1.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:9f659a5ee95c8baa2436d392267988fd0f43eb774e5eb8739252e5a7e9cf07e0"},
-    {file = "pydantic-1.9.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b83ba3825bc91dfa989d4eed76865e71aea3a6ca1388b59fc801ee04c4d8d0d6"},
-    {file = "pydantic-1.9.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1dd8fecbad028cd89d04a46688d2fcc14423e8a196d5b0a5c65105664901f810"},
-    {file = "pydantic-1.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02eefd7087268b711a3ff4db528e9916ac9aa18616da7bca69c1871d0b7a091f"},
-    {file = "pydantic-1.9.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7eb57ba90929bac0b6cc2af2373893d80ac559adda6933e562dcfb375029acee"},
-    {file = "pydantic-1.9.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:4ce9ae9e91f46c344bec3b03d6ee9612802682c1551aaf627ad24045ce090761"},
-    {file = "pydantic-1.9.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:72ccb318bf0c9ab97fc04c10c37683d9eea952ed526707fabf9ac5ae59b701fd"},
-    {file = "pydantic-1.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:61b6760b08b7c395975d893e0b814a11cf011ebb24f7d869e7118f5a339a82e1"},
-    {file = "pydantic-1.9.1-py3-none-any.whl", hash = "sha256:4988c0f13c42bfa9ddd2fe2f569c9d54646ce84adc5de84228cfe83396f3bd58"},
-    {file = "pydantic-1.9.1.tar.gz", hash = "sha256:1ed987c3ff29fff7fd8c3ea3a3ea877ad310aae2ef9889a119e22d3f2db0691a"},
+    {file = "pydantic-1.9.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9c9e04a6cdb7a363d7cb3ccf0efea51e0abb48e180c0d31dca8d247967d85c6e"},
+    {file = "pydantic-1.9.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fafe841be1103f340a24977f61dee76172e4ae5f647ab9e7fd1e1fca51524f08"},
+    {file = "pydantic-1.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afacf6d2a41ed91fc631bade88b1d319c51ab5418870802cedb590b709c5ae3c"},
+    {file = "pydantic-1.9.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ee0d69b2a5b341fc7927e92cae7ddcfd95e624dfc4870b32a85568bd65e6131"},
+    {file = "pydantic-1.9.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ff68fc85355532ea77559ede81f35fff79a6a5543477e168ab3a381887caea76"},
+    {file = "pydantic-1.9.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c0f5e142ef8217019e3eef6ae1b6b55f09a7a15972958d44fbd228214cede567"},
+    {file = "pydantic-1.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:615661bfc37e82ac677543704437ff737418e4ea04bef9cf11c6d27346606044"},
+    {file = "pydantic-1.9.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:328558c9f2eed77bd8fffad3cef39dbbe3edc7044517f4625a769d45d4cf7555"},
+    {file = "pydantic-1.9.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bd446bdb7755c3a94e56d7bdfd3ee92396070efa8ef3a34fab9579fe6aa1d84"},
+    {file = "pydantic-1.9.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e0b214e57623a535936005797567231a12d0da0c29711eb3514bc2b3cd008d0f"},
+    {file = "pydantic-1.9.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:d8ce3fb0841763a89322ea0432f1f59a2d3feae07a63ea2c958b2315e1ae8adb"},
+    {file = "pydantic-1.9.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b34ba24f3e2d0b39b43f0ca62008f7ba962cff51efa56e64ee25c4af6eed987b"},
+    {file = "pydantic-1.9.2-cp36-cp36m-win_amd64.whl", hash = "sha256:84d76ecc908d917f4684b354a39fd885d69dd0491be175f3465fe4b59811c001"},
+    {file = "pydantic-1.9.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4de71c718c9756d679420c69f216776c2e977459f77e8f679a4a961dc7304a56"},
+    {file = "pydantic-1.9.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5803ad846cdd1ed0d97eb00292b870c29c1f03732a010e66908ff48a762f20e4"},
+    {file = "pydantic-1.9.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a8c5360a0297a713b4123608a7909e6869e1b56d0e96eb0d792c27585d40757f"},
+    {file = "pydantic-1.9.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:cdb4272678db803ddf94caa4f94f8672e9a46bae4a44f167095e4d06fec12979"},
+    {file = "pydantic-1.9.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:19b5686387ea0d1ea52ecc4cffb71abb21702c5e5b2ac626fd4dbaa0834aa49d"},
+    {file = "pydantic-1.9.2-cp37-cp37m-win_amd64.whl", hash = "sha256:32e0b4fb13ad4db4058a7c3c80e2569adbd810c25e6ca3bbd8b2a9cc2cc871d7"},
+    {file = "pydantic-1.9.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:91089b2e281713f3893cd01d8e576771cd5bfdfbff5d0ed95969f47ef6d676c3"},
+    {file = "pydantic-1.9.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e631c70c9280e3129f071635b81207cad85e6c08e253539467e4ead0e5b219aa"},
+    {file = "pydantic-1.9.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b3946f87e5cef3ba2e7bd3a4eb5a20385fe36521d6cc1ebf3c08a6697c6cfb3"},
+    {file = "pydantic-1.9.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5565a49effe38d51882cb7bac18bda013cdb34d80ac336428e8908f0b72499b0"},
+    {file = "pydantic-1.9.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:bd67cb2c2d9602ad159389c29e4ca964b86fa2f35c2faef54c3eb28b4efd36c8"},
+    {file = "pydantic-1.9.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4aafd4e55e8ad5bd1b19572ea2df546ccace7945853832bb99422a79c70ce9b8"},
+    {file = "pydantic-1.9.2-cp38-cp38-win_amd64.whl", hash = "sha256:d70916235d478404a3fa8c997b003b5f33aeac4686ac1baa767234a0f8ac2326"},
+    {file = "pydantic-1.9.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f0ca86b525264daa5f6b192f216a0d1e860b7383e3da1c65a1908f9c02f42801"},
+    {file = "pydantic-1.9.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1061c6ee6204f4f5a27133126854948e3b3d51fcc16ead2e5d04378c199b2f44"},
+    {file = "pydantic-1.9.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e78578f0c7481c850d1c969aca9a65405887003484d24f6110458fb02cca7747"},
+    {file = "pydantic-1.9.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5da164119602212a3fe7e3bc08911a89db4710ae51444b4224c2382fd09ad453"},
+    {file = "pydantic-1.9.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ead3cd020d526f75b4188e0a8d71c0dbbe1b4b6b5dc0ea775a93aca16256aeb"},
+    {file = "pydantic-1.9.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7d0f183b305629765910eaad707800d2f47c6ac5bcfb8c6397abdc30b69eeb15"},
+    {file = "pydantic-1.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:f1a68f4f65a9ee64b6ccccb5bf7e17db07caebd2730109cb8a95863cfa9c4e55"},
+    {file = "pydantic-1.9.2-py3-none-any.whl", hash = "sha256:78a4d6bdfd116a559aeec9a4cfe77dda62acc6233f8b56a716edad2651023e5e"},
+    {file = "pydantic-1.9.2.tar.gz", hash = "sha256:8cb0bc509bfb71305d7a59d00163d5f9fc4530f0881ea32c74ff4f74c85f3d3d"},
 ]
 pyflakes = [
-    {file = "pyflakes-2.4.0-py2.py3-none-any.whl", hash = "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"},
-    {file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"},
+    {file = "pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2"},
+    {file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"},
 ]
 pygments = [
     {file = "Pygments-2.12.0-py3-none-any.whl", hash = "sha256:dc9c10fb40944260f6ed4c688ece0cd2048414940f1cea51b8b226318411c519"},
@@ -1809,13 +2249,36 @@ pytest-cov = [
     {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"},
     {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"},
 ]
+python-box = [
+    {file = "python-box-6.0.2.tar.gz", hash = "sha256:c15f09fa2a8730702115396b490ddc17dde3d4276a4f24eb754e35f102c41347"},
+    {file = "python_box-6.0.2-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:41ae4ef7d8336b377b9caa1c1ef5b1efff3aaebe72f580eeb641c01a15288953"},
+    {file = "python_box-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:113f4fb6aca9df22689e39f411b4b7cc74d3b284f2fa927666404a58d399f438"},
+    {file = "python_box-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:8b76359be83ea5d82c3d5ad20614acba522455f4f8c2aecf99847b72acd2db95"},
+    {file = "python_box-6.0.2-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:dbc85880cd01b1272509d660517d0fac94f104f902cc31647d1a27be070d37b3"},
+    {file = "python_box-6.0.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a43baf2fdee399c7b3df425e2df163e9551f1d8b40ceb213f9cc0a952effa77"},
+    {file = "python_box-6.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:449b190d5d8dae6b5ed67a71a0eff57bf9d9654bee2c3e21e54f0489ec0f8fd9"},
+    {file = "python_box-6.0.2-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:6cbf14b96afabaeaf6cd8c1da7c05704b0ab84cbd39188826d3fe6b2eed9bc1d"},
+    {file = "python_box-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce824e4625bb121ef265f7eac54b29ff1c038b4c523bfea606582d8c6185737d"},
+    {file = "python_box-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:b81a5398fdb97de5def918b6c206e3185c173e638d8ded9a3f0e5ef8f3e9da91"},
+    {file = "python_box-6.0.2-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:a8f2c20c838d565b92e8733aaa24ea12c0d82f1e415b996e0ef664ac7d02938e"},
+    {file = "python_box-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8595d6916eb2ee6c097066886faa5a3fc9392abba8835faa63fd189d1b6ec11b"},
+    {file = "python_box-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:99e9f88069855e76cbc4846527d35bc9d1858f2272ab741f29a71c92b72b7b5b"},
+    {file = "python_box-6.0.2-py3-none-any.whl", hash = "sha256:2565dd56d5e9107e1499ab864f7a4dcd60f63dd5d68215a019308e36c0c98dbc"},
+]
 python-dateutil = [
     {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"},
     {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"},
 ]
+python-kacl = [
+    {file = "python-kacl-0.2.30.tar.gz", hash = "sha256:b1caa9ec93984120e13fc412cc891b1d22c7abc06a3feef97b93a2f7ad9cbec3"},
+]
 pytz = [
-    {file = "pytz-2022.1-py2.py3-none-any.whl", hash = "sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c"},
-    {file = "pytz-2022.1.tar.gz", hash = "sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7"},
+    {file = "pytz-2022.2.1-py2.py3-none-any.whl", hash = "sha256:220f481bdafa09c3955dfbdddb7b57780e9a94f5127e35456a48589b9e0c0197"},
+    {file = "pytz-2022.2.1.tar.gz", hash = "sha256:cea221417204f2d1a2aa03ddae3e867921971d0d76f14d87abb4414415bbdcf5"},
+]
+pyupgrade = [
+    {file = "pyupgrade-2.37.3-py2.py3-none-any.whl", hash = "sha256:9746efd064dbf53d7f86d6f88a1d48120f58dbfc378f517768634740ea2225e2"},
+    {file = "pyupgrade-2.37.3.tar.gz", hash = "sha256:1414c7a7c558004cf610e6180716b876814b639b5a5789c3da023c5cdaebcd49"},
 ]
 pywin32 = [
     {file = "pywin32-304-cp310-cp310-win32.whl", hash = "sha256:3c7bacf5e24298c86314f03fa20e16558a4e4138fc34615d7de4070c23e65af3"},
@@ -1833,64 +2296,115 @@ pywin32 = [
     {file = "pywin32-304-cp39-cp39-win32.whl", hash = "sha256:25746d841201fd9f96b648a248f731c1dec851c9a08b8e33da8b56148e4c65cc"},
     {file = "pywin32-304-cp39-cp39-win_amd64.whl", hash = "sha256:d24a3382f013b21aa24a5cfbfad5a2cd9926610c0affde3e8ab5b3d7dbcf4ac9"},
 ]
+pyyaml = [
+    {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"},
+    {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"},
+    {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"},
+    {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"},
+    {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"},
+    {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"},
+    {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"},
+    {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"},
+    {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"},
+    {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"},
+    {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"},
+    {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"},
+    {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"},
+    {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"},
+    {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"},
+    {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"},
+    {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"},
+    {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"},
+    {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"},
+    {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"},
+    {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"},
+    {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"},
+    {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"},
+    {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"},
+    {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"},
+    {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"},
+    {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"},
+    {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"},
+    {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"},
+    {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"},
+    {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"},
+    {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"},
+    {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"},
+]
 pyzmq = [
-    {file = "pyzmq-23.2.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:22ac0243a41798e3eb5d5714b28c2f28e3d10792dffbc8a5fca092f975fdeceb"},
-    {file = "pyzmq-23.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f685003d836ad0e5d4f08d1e024ee3ac7816eb2f873b2266306eef858f058133"},
-    {file = "pyzmq-23.2.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d4651de7316ec8560afe430fb042c0782ed8ac54c0be43a515944d7c78fddac8"},
-    {file = "pyzmq-23.2.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:bcc6953e47bcfc9028ddf9ab2a321a3c51d7cc969db65edec092019bb837959f"},
-    {file = "pyzmq-23.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e08671dc202a1880fa522f921f35ca5925ba30da8bc96228d74a8f0643ead9c"},
-    {file = "pyzmq-23.2.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de727ea906033b30527b4a99498f19aca3f4d1073230a958679a5b726e2784e0"},
-    {file = "pyzmq-23.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5aa9da520e4bb8cee8189f2f541701405e7690745094ded7a37b425d60527ea"},
-    {file = "pyzmq-23.2.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:f3ff6abde52e702397949054cb5b06c1c75b5d6542f6a2ce029e46f71ffbbbf2"},
-    {file = "pyzmq-23.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e2e2db5c6ef376e97c912733dfc24406f5949474d03e800d5f07b6aca4d870af"},
-    {file = "pyzmq-23.2.0-cp310-cp310-win32.whl", hash = "sha256:e669913cb2179507628419ec4f0e453e48ce6f924de5884d396f18c31836089c"},
-    {file = "pyzmq-23.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:a3dc339f7bc185d5fd0fd976242a5baf35de404d467e056484def8a4dd95868b"},
-    {file = "pyzmq-23.2.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:30c365e60c39c53f8eea042b37ea28304ffa6558fb7241cf278745095a5757da"},
-    {file = "pyzmq-23.2.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c2d8b69a2bf239ae3d987537bf3fbc2b044a405394cf4c258fc684971dd48b2"},
-    {file = "pyzmq-23.2.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:602835e5672ca9ca1d78e6c148fb28c4f91b748ebc41fbd2f479d8763d58bc9b"},
-    {file = "pyzmq-23.2.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:831da96ba3f36cc892f0afbb4fb89b28b61b387261676e55d55a682addbd29f7"},
-    {file = "pyzmq-23.2.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:c8dec8a2f3f0bb462e6439df436cd8c7ec37968e90b4209ac621e7fbc0ed3b00"},
-    {file = "pyzmq-23.2.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:814e5aaf0c3be9991a59066eafb2d6e117aed6b413e3e7e9be45d4e55f5e2748"},
-    {file = "pyzmq-23.2.0-cp36-cp36m-win32.whl", hash = "sha256:8496a2a5efd055c61ac2c6a18116c768a25c644b6747dcfde43e91620ab3453c"},
-    {file = "pyzmq-23.2.0-cp36-cp36m-win_amd64.whl", hash = "sha256:60746a7e8558655420a69441c0a1d47ed225ed3ac355920b96a96d0554ef7e6b"},
-    {file = "pyzmq-23.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5cb642e94337b0c76c9c8cb9bfb0f8a78654575847d080d3e1504f312d691fc3"},
-    {file = "pyzmq-23.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:444f7d615d5f686d0ef508b9edfa8a286e6d89f449a1ba37b60ef69d869220a3"},
-    {file = "pyzmq-23.2.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c9638e0057e3f1a8b7c5ce33c7575349d9183a033a19b5676ad55096ae36820b"},
-    {file = "pyzmq-23.2.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:004a431dfa0459123e6f4660d7e3c4ac19217d134ca38bacfffb2e78716fe944"},
-    {file = "pyzmq-23.2.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:5592fb4316f895922b1cacb91b04a0fa09d6f6f19bbab4442b4d0a0825177b93"},
-    {file = "pyzmq-23.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c0a5f987d73fd9b46c3d180891f829afda714ab6bab30a1218724d4a0a63afd8"},
-    {file = "pyzmq-23.2.0-cp37-cp37m-win32.whl", hash = "sha256:d11628212fd731b8986f1561d9bb3f8c38d9c15b330c3d8a88963519fbcd553b"},
-    {file = "pyzmq-23.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:558f5f636e3e65f261b64925e8b190e8689e334911595394572cc7523879006d"},
-    {file = "pyzmq-23.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:61b97f624da42813f74977425a3a6144d604ea21cf065616d36ea3a866d92c1c"},
-    {file = "pyzmq-23.2.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:693c96ae4d975eb8efa1639670e9b1fac0c3f98b7845b65c0f369141fb4bb21f"},
-    {file = "pyzmq-23.2.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2b054525c9f7e240562185bf21671ca16d56bde92e9bd0f822c07dec7626b704"},
-    {file = "pyzmq-23.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:859059caf564f0c9398c9005278055ed3d37af4d73de6b1597821193b04ca09b"},
-    {file = "pyzmq-23.2.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8355744fdbdeac5cfadfa4f38b82029b5f2b8cab7472a33453a217a7f3a9dce2"},
-    {file = "pyzmq-23.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:420b9abd1a7330687a095373b8280a20cdee04342fbc8ccb3b56d9ec8efd4e62"},
-    {file = "pyzmq-23.2.0-cp38-cp38-win32.whl", hash = "sha256:59928dfebe93cf1e203e3cb0fd5d5dd384da56b99c8305f2e1b0a933751710f6"},
-    {file = "pyzmq-23.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:c882f1d4f96fbd807e92c334251d8ebd159a1ef89059ccd386ddea83fdb91bd8"},
-    {file = "pyzmq-23.2.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:ced12075cdf3c7332ecc1960f77f7439d5ebb8ea20bbd3c34c8299e694f1b0a1"},
-    {file = "pyzmq-23.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3a4d87342c2737fbb9eee5c33c792db27b36b04957b4e6b7edd73a5b239a2a13"},
-    {file = "pyzmq-23.2.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:99cedf38eaddf263cf7e2a50e405f12c02cedf6d9df00a0d9c5d7b9417b57f76"},
-    {file = "pyzmq-23.2.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d1610260cc672975723fcf7705c69a95f3b88802a594c9867781bedd9b13422c"},
-    {file = "pyzmq-23.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c223a13555444707a0a7ebc6f9ee63053147c8c082bd1a31fd1207a03e8b0500"},
-    {file = "pyzmq-23.2.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f5fdb00d65ec44b10cc6b9b6318ef1363b81647a4aa3270ca39565eadb2d1201"},
-    {file = "pyzmq-23.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:984b232802eddf9f0be264a4d57a10b3a1fd7319df14ee6fc7b41c6d155a3e6c"},
-    {file = "pyzmq-23.2.0-cp39-cp39-win32.whl", hash = "sha256:f146648941cadaaaf01254a75651a23c08159d009d36c5af42a7cc200a5e53ec"},
-    {file = "pyzmq-23.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:83005d8928f8a5cebcfb33af3bfb84b1ad65d882b899141a331cc5d07d89f093"},
-    {file = "pyzmq-23.2.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:fee86542dc4ee8229e023003e3939b4d58cc2453922cf127778b69505fc9064b"},
-    {file = "pyzmq-23.2.0-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5d57542429df6acff02ff022067aa75b677603cee70e3abb9742787545eec966"},
-    {file = "pyzmq-23.2.0-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:057b154471e096e2dda147f7b057041acc303bb7ca4aa24c3b88c6cecdd78717"},
-    {file = "pyzmq-23.2.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:5d92e7cbeab7f70b08cc0f27255b0bb2500afc30f31075bca0b1cb87735d186c"},
-    {file = "pyzmq-23.2.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:eb4a573a8499685d62545e806d8fd143c84ac8b3439f925cd92c8763f0ed9bd7"},
-    {file = "pyzmq-23.2.0-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:da338e2728410d74ddeb1479ec67cfba73311607037455a40f92b6f5c62bf11d"},
-    {file = "pyzmq-23.2.0-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1b2a21f595f8cc549abd6c8de1fcd34c83441e35fb24b8a59bf161889c62a486"},
-    {file = "pyzmq-23.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:8c0f4d6f8c985bab83792be26ff3233940ba42e22237610ac50cbcfc10a5c235"},
-    {file = "pyzmq-23.2.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:bbabd1df23bf63ae829e81200034c0e433499275a6ed29ca1a912ea7629426d9"},
-    {file = "pyzmq-23.2.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:21552624ce69e69f7924f413b802b1fb554f4c0497f837810e429faa1cd4f163"},
-    {file = "pyzmq-23.2.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c616893a577e9d6773a3836732fd7e2a729157a108b8fccd31c87512fa01671a"},
-    {file = "pyzmq-23.2.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ce4f71e17fa849de41a06109030d3f6815fcc33338bf98dd0dde6d456d33c929"},
-    {file = "pyzmq-23.2.0.tar.gz", hash = "sha256:a51f12a8719aad9dcfb55d456022f16b90abc8dde7d3ca93ce3120b40e3fa169"},
+    {file = "pyzmq-23.2.1-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:a3fd44b5046d247e7f0f1660bcafe7b5fb0db55d0934c05dd57dda9e1f823ce7"},
+    {file = "pyzmq-23.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2141e6798d5981be04c08996d27962086a1aa3ea536fe9cf7e89817fd4523f86"},
+    {file = "pyzmq-23.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a39ddb0431a68954bd318b923230fa5b649c9c62b0e8340388820c5f1b15bd2"},
+    {file = "pyzmq-23.2.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e06747014a5ad1b28cebf5bc1ddcdaccfb44e9b441d35e6feb1286c8a72e54be"},
+    {file = "pyzmq-23.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e0113d70b095339e99bb522fe7294f5ae6a7f3b2b8f52f659469a74b5cc7661"},
+    {file = "pyzmq-23.2.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:71b32a1e827bdcbf73750e60370d3b07685816ff3d8695f450f0f8c3226503f8"},
+    {file = "pyzmq-23.2.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:55568a020ad2cae9ae36da6058e7ca332a56df968f601cbdb7cf6efb2a77579a"},
+    {file = "pyzmq-23.2.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8c02a0cd39dc01659b3d6cb70bb3a41aebd9885fd78239acdd8d9c91351c4568"},
+    {file = "pyzmq-23.2.1-cp310-cp310-win32.whl", hash = "sha256:e1fe30bcd5aea5948c42685fad910cd285eacb2518ea4dc6c170d6b535bee95d"},
+    {file = "pyzmq-23.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:650389bbfca73955b262b2230423d89992f38ec48033307ae80e700eaa2fbb63"},
+    {file = "pyzmq-23.2.1-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:e753eee6d3b93c5354e8ba0a1d62956ee49355f0a36e00570823ef64e66183f5"},
+    {file = "pyzmq-23.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f07016e3cf088dbfc6e7c5a7b3f540db5c23b0190d539e4fd3e2b5e6beffa4b5"},
+    {file = "pyzmq-23.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4805af9614b0b41b7e57d17673459facf85604dac502a5a9244f6e8c9a4de658"},
+    {file = "pyzmq-23.2.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:39dd252b683816935702825e5bf775df16090619ced9bb4ba68c2d0b6f0c9b18"},
+    {file = "pyzmq-23.2.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:84678153432241bcdca2210cf4ff83560b200556867aea913ffbb960f5d5f340"},
+    {file = "pyzmq-23.2.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:90d88f9d9a2ae6cfb1dc4ea2d1710cdf6456bc1b9a06dd1bb485c5d298f2517e"},
+    {file = "pyzmq-23.2.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:794871988c34727c7f79bdfe2546e6854ae1fa2e1feb382784f23a9c6c63ecb3"},
+    {file = "pyzmq-23.2.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c56b1a62a1fb87565343c57b6743fd5da6e138b8c6562361d7d9b5ce4acf399a"},
+    {file = "pyzmq-23.2.1-cp311-cp311-win32.whl", hash = "sha256:c3ebf1668664d20c8f7d468955f18379b7d1f7bc8946b13243d050fa3888c7ff"},
+    {file = "pyzmq-23.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:ec9803aca9491fd6f0d853d2a6147f19f8deaaa23b1b713d05c5d09e56ea7142"},
+    {file = "pyzmq-23.2.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:385609812eafd9970c3752c51f2f6c4f224807e3e441bcfd8c8273877d00c8a8"},
+    {file = "pyzmq-23.2.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b861db65f6b8906c8d6db51dde2448f266f0c66bf28db2c37aea50f58a849859"},
+    {file = "pyzmq-23.2.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6b1e79bba24f6df1712e3188d5c32c480d8eda03e8ecff44dc8ecb0805fa62f3"},
+    {file = "pyzmq-23.2.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:8dc66f109a245653b19df0f44a5af7a3f14cb8ad6c780ead506158a057bd36ce"},
+    {file = "pyzmq-23.2.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:b815991c7d024bf461f358ad871f2be1135576274caed5749c4828859e40354e"},
+    {file = "pyzmq-23.2.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:29b74774a0bfd3c4d98ac853f0bdca55bd9ec89d5b0def5486407cca54472ef8"},
+    {file = "pyzmq-23.2.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:4bb798bef181648827019001f6be43e1c48b34b477763b37a8d27d8c06d197b8"},
+    {file = "pyzmq-23.2.1-cp36-cp36m-win32.whl", hash = "sha256:565bd5ab81f6964fc4067ccf2e00877ad0fa917308975694bbb54378389215f8"},
+    {file = "pyzmq-23.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:1f368a82b29f80071781b20663c0fc0c8f6b13273f9f5abe1526af939534f90f"},
+    {file = "pyzmq-23.2.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c9cfaf530e6a7ff65f0afe275e99f983f68b54dfb23ea401f0bc297a632766b6"},
+    {file = "pyzmq-23.2.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c558b50402fca1acc94329c5d8f12aa429738904a5cfb32b9ed3c61235221bb"},
+    {file = "pyzmq-23.2.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:20bafc4095eab00f41a510579363a3f5e1f5c69d7ee10f1d88895c4df0259183"},
+    {file = "pyzmq-23.2.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:f619fd38fc2641abfb53cca719c165182500600b82c695cc548a0f05f764be05"},
+    {file = "pyzmq-23.2.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:044447ae4b2016a6b8697571fd633f799f860b19b76c4a2fd9b1140d52ee6745"},
+    {file = "pyzmq-23.2.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:49d30ba7074f469e8167917abf9eb854c6503ae10153034a6d4df33618f1db5f"},
+    {file = "pyzmq-23.2.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:48400b96788cdaca647021bf19a9cd668384f46e4d9c55cf045bdd17f65299c8"},
+    {file = "pyzmq-23.2.1-cp37-cp37m-win32.whl", hash = "sha256:8a68f57b7a3f7b6b52ada79876be1efb97c8c0952423436e84d70cc139f16f0d"},
+    {file = "pyzmq-23.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9e5bf6e7239fc9687239de7a283aa8b801ab85371116045b33ae20132a1325d6"},
+    {file = "pyzmq-23.2.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ffc6b1623d0f9affb351db4ca61f432dca3628a5ee015f9bf2bfbe9c6836881c"},
+    {file = "pyzmq-23.2.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4d6f110c56f7d5b4d64dde3a382ae61b6d48174e30742859d8e971b18b6c9e5c"},
+    {file = "pyzmq-23.2.1-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9269fbfe3a4eb2009199120861c4571ef1655fdf6951c3e7f233567c94e8c602"},
+    {file = "pyzmq-23.2.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12e62ff0d5223ec09b597ab6d73858b9f64a51221399f3cb08aa495e1dff7935"},
+    {file = "pyzmq-23.2.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6fd5d0d50cbcf4bc376861529a907bed026a4cbe8c22a500ff8243231ef02433"},
+    {file = "pyzmq-23.2.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9d0ab2936085c85a1fc6f9fd8f89d5235ae99b051e90ec5baa5e73ad44346e1f"},
+    {file = "pyzmq-23.2.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:022cf5ea7bcaa8a06a03c2706e0ae66904b6138b2155577cd34c64bc7cc637ab"},
+    {file = "pyzmq-23.2.1-cp38-cp38-win32.whl", hash = "sha256:28dbdb90b2f6b131f8f10e6081012e4e25234213433420e67e0c1162de537113"},
+    {file = "pyzmq-23.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:10d1910ec381b851aeb024a042a13db178cb1edf125e76a4e9d2548ad103aadb"},
+    {file = "pyzmq-23.2.1-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:99a5a77a10863493a1ee8dece02578c6b32025fb3afff91b40476bc489e81648"},
+    {file = "pyzmq-23.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:aecd6ceaccc4b594e0092d6513ef3f1c0fa678dd89f86bb8ff1a47014b8fca35"},
+    {file = "pyzmq-23.2.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:415ff62ac525d9add1e3550430a09b9928d2d24a20cc4ce809e67caac41219ab"},
+    {file = "pyzmq-23.2.1-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:67975a9e1237b9ccc78f457bef17691bbdd2055a9d26e81ee914ba376846d0ce"},
+    {file = "pyzmq-23.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38e106b64bad744fe469dc3dd864f2764d66399178c1bf39d45294cc7980f14f"},
+    {file = "pyzmq-23.2.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:8c842109d31a9281d678f668629241c405928afbebd913c48a5a8e7aee61f63d"},
+    {file = "pyzmq-23.2.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:fefdf9b685fda4141b95ebec975946076a5e0723ff70b037032b2085c5317684"},
+    {file = "pyzmq-23.2.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:79a87831b47a9f6161ad23fa5e89d5469dc585abc49f90b9b07fea8905ae1234"},
+    {file = "pyzmq-23.2.1-cp39-cp39-win32.whl", hash = "sha256:342ca3077f47ec2ee41b9825142b614e03e026347167cbc72a59b618c4f6106c"},
+    {file = "pyzmq-23.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:5e05492be125dce279721d6b54fd1b956546ecc4bcdfcf8e7b4c413bc0874c10"},
+    {file = "pyzmq-23.2.1-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:07ed8aaf7ffe150af873269690cc654ffeca7491f62aae0f3821baa181f8d5fe"},
+    {file = "pyzmq-23.2.1-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ad28ddb40db8e450d7d4bf8a1d765d3f87b63b10e7e9a825a3c130c6371a8c03"},
+    {file = "pyzmq-23.2.1-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2f67b63f53c6994d601404fd1a329e6d940ac3dd1d92946a93b2b9c70df67b9f"},
+    {file = "pyzmq-23.2.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c890309296f53f9aa32ffcfc51d805705e1982bffd27c9692a8f1e1b8de279f4"},
+    {file = "pyzmq-23.2.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:624fd38071a817644acdae075b92a23ea0bdd126a58148288e8284d23ec361ce"},
+    {file = "pyzmq-23.2.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a114992a193577cb62233abf8cb2832970f9975805a64740e325d2f895e7f85a"},
+    {file = "pyzmq-23.2.1-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c780acddd2934c6831ff832ecbf78a45a7b62d4eb216480f863854a8b7d54fa7"},
+    {file = "pyzmq-23.2.1-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d904f6595acfaaf99a1a61881fea068500c40374d263e5e073aa4005e5f9c28a"},
+    {file = "pyzmq-23.2.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:929d548b74c0f82f7f95b54e4a43f9e4ce2523cfb8a54d3f7141e45652304b2a"},
+    {file = "pyzmq-23.2.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:f392cbea531b7142d1958c0d4a0c9c8d760dc451e5848d8dd3387804d3e3e62c"},
+    {file = "pyzmq-23.2.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a0f09d85c45f58aa8e715b42f8b26beba68b3b63a8f7049113478aca26efbc30"},
+    {file = "pyzmq-23.2.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23e708fbfdf4ee3107422b69ca65da1b9f056b431fc0888096a8c1d6cd908e8f"},
+    {file = "pyzmq-23.2.1-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:35e635343ff367f697d00fa1484262bb68e36bc74c9b80737eac5a1e04c4e1b1"},
+    {file = "pyzmq-23.2.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efb9e38b2a590282704269585de7eb33bf43dc294cad092e1b172e23d4c217e5"},
+    {file = "pyzmq-23.2.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:407f909c4e8fde62fbdad9ebd448319792258cc0550c2815567a4d9d8d9e6d18"},
+    {file = "pyzmq-23.2.1.tar.gz", hash = "sha256:2b381aa867ece7d0a82f30a0c7f3d4387b7cf2e0697e33efaa5bed6c5784abcd"},
 ]
 requests = [
     {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"},
@@ -1899,22 +2413,42 @@ requests = [
 retrying = [
     {file = "retrying-1.3.3.tar.gz", hash = "sha256:08c039560a6da2fe4f2c426d0766e284d3b736e355f8dd24b37367b0bb41973b"},
 ]
+rich = [
+    {file = "rich-12.5.1-py3-none-any.whl", hash = "sha256:2eb4e6894cde1e017976d2975ac210ef515d7548bc595ba20e195fb9628acdeb"},
+    {file = "rich-12.5.1.tar.gz", hash = "sha256:63a5c5ce3673d3d5fbbf23cd87e11ab84b6b451436f1b7f19ec54b6bc36ed7ca"},
+]
+semver = [
+    {file = "semver-2.13.0-py2.py3-none-any.whl", hash = "sha256:ced8b23dceb22134307c1b8abfa523da14198793d9787ac838e70e29e77458d4"},
+    {file = "semver-2.13.0.tar.gz", hash = "sha256:fa0fe2722ee1c3f57eac478820c3a5ae2f624af8264cbdf9000c980ff7f75e3f"},
+]
 six = [
     {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
     {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
 ]
+smmap = [
+    {file = "smmap-5.0.0-py3-none-any.whl", hash = "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94"},
+    {file = "smmap-5.0.0.tar.gz", hash = "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936"},
+]
 ssb-datadoc-model = [
-    {file = "ssb-datadoc-model-0.1.0.tar.gz", hash = "sha256:0f211642b86e137fa8ce1770a90d939bda26d1fac7ce6a3c72275c28d877bd02"},
-    {file = "ssb_datadoc_model-0.1.0-py3-none-any.whl", hash = "sha256:a4ecc763fc47bd5a32f0b2d276a86071084190f8e2382755400c040b2bd368dc"},
+    {file = "ssb-datadoc-model-0.1.3.tar.gz", hash = "sha256:d18bf155b2289a8438a6b0b242040e0534021cfcc00691357fca0e3ec47742b5"},
+    {file = "ssb_datadoc_model-0.1.3-py3-none-any.whl", hash = "sha256:b52e0ddb2235a7d382b25690efdbb162c65f28c3ce827ae70ea4e47982cd40c6"},
 ]
 stack-data = [
-    {file = "stack_data-0.3.0-py3-none-any.whl", hash = "sha256:aa1d52d14d09c7a9a12bb740e6bdfffe0f5e8f4f9218d85e7c73a8c37f7ae38d"},
-    {file = "stack_data-0.3.0.tar.gz", hash = "sha256:77bec1402dcd0987e9022326473fdbcc767304892a533ed8c29888dacb7dddbc"},
+    {file = "stack_data-0.4.0-py3-none-any.whl", hash = "sha256:b94fed36d725cfabc6d09ed5886913e35eed9009766a1af1d5941b9da3a94aaa"},
+    {file = "stack_data-0.4.0.tar.gz", hash = "sha256:a90ae7e260f7d15aefeceb46f0a028d4ccb9eb8856475c53e341945342d41ea7"},
 ]
 tenacity = [
     {file = "tenacity-8.0.1-py3-none-any.whl", hash = "sha256:f78f4ea81b0fabc06728c11dc2a8c01277bfc5181b321a4770471902e3eb844a"},
     {file = "tenacity-8.0.1.tar.gz", hash = "sha256:43242a20e3e73291a28bcbcacfd6e000b02d3857a9a9fff56b297a27afdc932f"},
 ]
+tokenize-rt = [
+    {file = "tokenize_rt-4.2.1-py2.py3-none-any.whl", hash = "sha256:08a27fa032a81cf45e8858d0ac706004fcd523e8463415ddf1442be38e204ea8"},
+    {file = "tokenize_rt-4.2.1.tar.gz", hash = "sha256:0d4f69026fed520f8a1e0103aa36c406ef4661417f20ca643f913e33531b3b94"},
+]
+toml = [
+    {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
+    {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
+]
 tomli = [
     {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
     {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
@@ -1936,6 +2470,10 @@ traitlets = [
     {file = "traitlets-5.3.0-py3-none-any.whl", hash = "sha256:65fa18961659635933100db8ca120ef6220555286949774b9cfc106f941d1c7a"},
     {file = "traitlets-5.3.0.tar.gz", hash = "sha256:0bb9f1f9f017aa8ec187d8b1b2a7a6626a2a1d877116baba52a129bfa124f8e2"},
 ]
+tryceratops = [
+    {file = "tryceratops-1.1.0-py3-none-any.whl", hash = "sha256:f4770960782a2ae87cad7a7fd1206476468d0f41383c9fd7a49c6d537534015e"},
+    {file = "tryceratops-1.1.0.tar.gz", hash = "sha256:58e56b33eeb7a9cfd643957069bb1872ebe26c1eeac7abb1877c9535b7e473db"},
+]
 typing-extensions = [
     {file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"},
     {file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"},
@@ -1944,13 +2482,17 @@ urllib3 = [
     {file = "urllib3-1.26.11-py2.py3-none-any.whl", hash = "sha256:c33ccba33c819596124764c23a97d25f32b28433ba0dedeb77d873a38722c9bc"},
     {file = "urllib3-1.26.11.tar.gz", hash = "sha256:ea6e8fb210b19d950fab93b60c9009226c63a28808bc8386e05301e25883ac0a"},
 ]
+virtualenv = [
+    {file = "virtualenv-20.16.3-py2.py3-none-any.whl", hash = "sha256:4193b7bc8a6cd23e4eb251ac64f29b4398ab2c233531e66e40b19a6b7b0d30c1"},
+    {file = "virtualenv-20.16.3.tar.gz", hash = "sha256:d86ea0bb50e06252d79e6c241507cb904fcd66090c3271381372d6221a3970f9"},
+]
 wcwidth = [
     {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"},
     {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"},
 ]
 werkzeug = [
-    {file = "Werkzeug-2.2.1-py3-none-any.whl", hash = "sha256:7e1db6a5ba6b9a8be061e47e900456355b8714c0f238b0313f53afce1a55a79a"},
-    {file = "Werkzeug-2.2.1.tar.gz", hash = "sha256:4d7013ef96fd197d1cdeb03e066c6c5a491ccb44758a5b2b91137319383e5a5a"},
+    {file = "Werkzeug-2.2.2-py3-none-any.whl", hash = "sha256:f979ab81f58d7318e064e99c4506445d60135ac5cd2e177a2de0089bfd4c9bd5"},
+    {file = "Werkzeug-2.2.2.tar.gz", hash = "sha256:7ea2d48322cc7c0f8b3a215ed73eabd7b5d75d0b50e31ab006286ccff9e00b8f"},
 ]
 zipp = [
     {file = "zipp-3.8.1-py3-none-any.whl", hash = "sha256:47c40d7fe183a6f21403a199b3e4192cca5774656965b0a4988ad2f8feb5f009"},
diff --git a/pyproject.toml b/pyproject.toml
index 814af209..3090a8e3 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -24,18 +24,39 @@ dash = "^2.4.1"
 pydantic = "^1.9.1"
 dash-bootstrap-components = "^1.1.0"
 pandas = "^1.4.2"
-ssb-datadoc-model = "0.1.0"
+ssb-datadoc-model = "0.1.3"
 
 [tool.poetry.dev-dependencies]
 ipykernel = "^6.13.0"
-flake8 = "^4.0.1"
 mypy = "^0.950"
 black = "^22.3.0"
 pytest = "^7.1.2"
 pytest-cov = "^3.0.0"
 nbstripout = "^0.5.0"
 bump2version = "^1.0.1"
+flake8 = "*"
+flake8-bugbear = "*"
+flake8-builtins = "*"
+flake8-comprehensions = "*"
+flake8-debugger = "*"
+flake8-eradicate = "*"
+flake8-logging-format = "*"
+isort = "*"
+python-kacl = "*"
+pyupgrade = "*"
+tryceratops = "*"
+pep8-naming = "*"
+pre-commit = "^2.20.0"
+autoflake = "^1.4"
 
 [build-system]
 requires = ["poetry-core>=1.0.0"]
 build-backend = "poetry.core.masonry.api"
+
+[tool.isort]
+profile = "black"
+src_paths = ["datadoc"]
+
+[tool.black]
+target-version = ["py37", "py38", "py39"]
+include = '\.pyi?$'
diff --git a/setup.cfg b/setup.cfg
index 9b23f2f8..e17a987c 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -3,3 +3,43 @@ name = PACKAGENAME
 
 [options]
 packages = find:
+
+[flake8]
+ignore =
+    # Line break occurred before a binary operator (W503)
+    # https://github.com/psf/black/issues/52
+    W503,
+    # Line too long (E501)
+    # 1. black does not format comments
+    #    https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html#comments
+    # 2. long links in doc strings are an issue
+    E501
+    # flake8-builtins
+    # the likelihood of running into an issue when shadowing a buildin
+    # with a class attribute is very low
+    A003,
+    # flake8-bugbear
+    # fastapi recommends to use `Depend()` as an argument default.
+    # Unfortuantely, exceptions are hardcoded in bugbear.
+    # https://github.com/PyCQA/flake8-bugbear/issues/62
+    B008,
+    # I like using f-strings for logging
+    G004,
+
+# pep8-naming
+classmethod-decorators =
+  classmethod,  # built-in
+  validator,  # pydantic
+  root_validator,  # pydantic
+
+enable-extensions=
+    G,  # flake8-logging-format
+
+per-file-ignores =
+    # star imports in `__init__.py` files are ok
+    */__init__.py: F401
+
+# Enables maccabe complexity checks
+max-complexity = 10
+
+exclude = .git,__pycache__,old,build,dist,.venv,.eggs,.tox


Test patch:
diff --git a/datadoc/tests/conftest.py b/datadoc/tests/conftest.py
new file mode 100644
index 00000000..b039b5e4
--- /dev/null
+++ b/datadoc/tests/conftest.py
@@ -0,0 +1,10 @@
+import pytest
+from datadoc_model.Enums import SupportedLanguages
+
+from datadoc import state
+
+
+@pytest.fixture()
+def clear_state():
+    state.metadata = None
+    state.current_metadata_language = SupportedLanguages.NORSK_BOKMÅL
diff --git a/datadoc/tests/pytest.ini b/datadoc/tests/pytest.ini
new file mode 100644
index 00000000..b06ffac1
--- /dev/null
+++ b/datadoc/tests/pytest.ini
@@ -0,0 +1,2 @@
+[pytest]
+usefixtures = clear_state
diff --git a/datadoc/tests/resources/person_data.csv b/datadoc/tests/resources/person_data.csv
index e6f12d4d..f2bf99d6 100644
--- a/datadoc/tests/resources/person_data.csv
+++ b/datadoc/tests/resources/person_data.csv
@@ -2,4 +2,4 @@ pers_id;tidspunkt;sivilstand;alm_inntekt;sykepenger;ber_bruttoformue;fullf_utdan
 XYZ010203QP;2022-01-01;2;950000;0;500000;7371;
 ZSY020304RS;2022-01-01;4;350000;250000;100000;6221;04
 QLZ030405PL;2022-01-01;3;300000;0;200000;5611;
-AQC050506LQ;2022-01-01;1;700000;50000;0;7814;15
\ No newline at end of file
+AQC050506LQ;2022-01-01;1;700000;50000;0;7814;15
diff --git a/datadoc/tests/test_callbacks.py b/datadoc/tests/test_callbacks.py
index e1215d22..6ac68296 100644
--- a/datadoc/tests/test_callbacks.py
+++ b/datadoc/tests/test_callbacks.py
@@ -1,15 +1,26 @@
-from copy import deepcopy
 import random
+from copy import deepcopy
+
+from datadoc_model.Enums import DatasetState, Datatype, SupportedLanguages
+from datadoc_model.Model import DataDocDataSet, DataDocVariable, LanguageStrings
 
-from datadoc_model.Enums import DatasetState, VariableRole
-from datadoc_model.Model import LanguageStrings, DataDocDataSet, DataDocVariable
-from datadoc.frontend.fields.DisplayVariables import VariableIdentifiers
-from datadoc.tests.utils import TEST_PARQUET_FILEPATH
 import datadoc.state as state
 from datadoc.backend.DataDocMetadata import DataDocMetadata
-from datadoc.frontend.callbacks import Callbacks
-from datadoc.Enums import SupportedLanguages
-
+from datadoc.frontend.callbacks.dataset import (
+    accept_dataset_metadata_input,
+    change_language_dataset_metadata,
+    update_dataset_metadata_language,
+    update_global_language_state,
+)
+from datadoc.frontend.callbacks.utils import find_existing_language_string
+from datadoc.frontend.callbacks.variables import (
+    accept_variable_metadata_input,
+    update_variable_table_dropdown_options_for_language,
+    update_variable_table_language,
+)
+from datadoc.frontend.fields.DisplayDataset import DISPLAYED_DROPDOWN_DATASET_ENUMS
+from datadoc.frontend.fields.DisplayVariables import VariableIdentifiers
+from datadoc.tests.utils import TEST_PARQUET_FILEPATH
 
 DATA_ORIGINAL = [
     {
@@ -42,7 +53,7 @@
 
 def test_accept_variable_metadata_input_no_change_in_data():
     state.metadata = DataDocMetadata(TEST_PARQUET_FILEPATH)
-    output = Callbacks.accept_variable_metadata_input(DATA_ORIGINAL, DATA_ORIGINAL)
+    output = accept_variable_metadata_input(DATA_ORIGINAL, DATA_ORIGINAL)
     assert output[0] == DATA_ORIGINAL
     assert output[1] is False
     assert output[2] == ""
@@ -50,12 +61,9 @@ def test_accept_variable_metadata_input_no_change_in_data():
 
 def test_accept_variable_metadata_input_new_data():
     state.metadata = DataDocMetadata(TEST_PARQUET_FILEPATH)
-    output = Callbacks.accept_variable_metadata_input(DATA_VALID, DATA_ORIGINAL)
+    output = accept_variable_metadata_input(DATA_VALID, DATA_ORIGINAL)
 
-    assert (
-        state.metadata.variables_lookup["pers_id"].variable_role
-        == VariableRole.IDENTIFIER
-    )
+    assert state.metadata.variables_lookup["pers_id"].variable_role == "IDENTIFIER"
     assert output[0] == DATA_VALID
     assert output[1] is False
     assert output[2] == ""
@@ -64,7 +72,7 @@ def test_accept_variable_metadata_input_new_data():
 def test_accept_variable_metadata_input_incorrect_data_type():
     state.metadata = DataDocMetadata(TEST_PARQUET_FILEPATH)
     previous_metadata = deepcopy(state.metadata.meta.variables)
-    output = Callbacks.accept_variable_metadata_input(DATA_INVALID, DATA_ORIGINAL)
+    output = accept_variable_metadata_input(DATA_INVALID, DATA_ORIGINAL)
 
     assert output[0] == DATA_ORIGINAL
     assert output[1] is True
@@ -74,36 +82,51 @@ def test_accept_variable_metadata_input_incorrect_data_type():
 
 def test_accept_dataset_metadata_input_new_data():
     state.metadata = DataDocMetadata(TEST_PARQUET_FILEPATH)
-    output = Callbacks.accept_dataset_metadata_input(
-        DatasetState.INPUT_DATA, "dataset_state"
-    )
+    output = accept_dataset_metadata_input(DatasetState.INPUT_DATA, "dataset_state")
     assert output[0] is False
     assert output[1] == ""
-    assert state.metadata.meta.dataset.dataset_state == DatasetState.INPUT_DATA
+    assert state.metadata.meta.dataset.dataset_state == "INPUT_DATA"
 
 
 def test_accept_dataset_metadata_input_incorrect_data_type():
     state.metadata = DataDocMetadata(TEST_PARQUET_FILEPATH)
-    output = Callbacks.accept_dataset_metadata_input(3.1415, "dataset_state")
+    output = accept_dataset_metadata_input(3.1415, "dataset_state")
     assert output[0] is True
     assert "validation error for DataDocDataSet" in output[1]
 
 
-def test_change_language():
+def test_update_dataset_metadata_language_strings():
     state.metadata = DataDocMetadata(TEST_PARQUET_FILEPATH)
     state.metadata.meta.dataset.name = LANGUAGE_OBJECT
-    output = Callbacks.update_dataset_metadata_language(SupportedLanguages.NORSK_BOKMÅL)
+    state.current_metadata_language = SupportedLanguages.NORSK_BOKMÅL
+    output = update_dataset_metadata_language()
     assert ENGLISH_NAME not in output
     assert BOKMÅL_NAME in output
-    output = Callbacks.update_dataset_metadata_language(SupportedLanguages.ENGLISH)
+    state.current_metadata_language = SupportedLanguages.ENGLISH
+    output = update_dataset_metadata_language()
     assert ENGLISH_NAME in output
     assert BOKMÅL_NAME not in output
 
 
+def test_update_dataset_metadata_language_enums():
+    state.metadata = DataDocMetadata(TEST_PARQUET_FILEPATH)
+    state.metadata.meta.dataset.dataset_state = DatasetState.PROCESSED_DATA
+    state.current_metadata_language = SupportedLanguages.NORSK_BOKMÅL
+    output = update_dataset_metadata_language()
+    assert DatasetState.PROCESSED_DATA.language_strings.en not in output
+    assert DatasetState.PROCESSED_DATA.language_strings.nb not in output
+    assert DatasetState.PROCESSED_DATA.name in output
+    state.current_metadata_language = SupportedLanguages.ENGLISH
+    output = update_dataset_metadata_language()
+    assert DatasetState.PROCESSED_DATA.language_strings.en not in output
+    assert DatasetState.PROCESSED_DATA.language_strings.nb not in output
+    assert DatasetState.PROCESSED_DATA.name in output
+
+
 def test_find_existing_language_string_no_existing_strings():
     dataset_metadata = DataDocDataSet()
     state.current_metadata_language = SupportedLanguages.NORSK_BOKMÅL
-    language_strings = Callbacks.find_existing_language_string(
+    language_strings = find_existing_language_string(
         dataset_metadata, BOKMÅL_NAME, "name"
     )
     assert language_strings == LanguageStrings(nb=BOKMÅL_NAME)
@@ -113,7 +136,7 @@ def test_find_existing_language_string_pre_existing_strings():
     dataset_metadata = DataDocDataSet()
     dataset_metadata.name = LANGUAGE_OBJECT
     state.current_metadata_language = SupportedLanguages.NORSK_NYNORSK
-    language_strings = Callbacks.find_existing_language_string(
+    language_strings = find_existing_language_string(
         dataset_metadata, NYNORSK_NAME, "name"
     )
     assert language_strings == LanguageStrings(
@@ -125,7 +148,7 @@ def test_update_variable_table_language():
     state.metadata = DataDocMetadata(TEST_PARQUET_FILEPATH)
     test_variable = random.choice([v.short_name for v in state.metadata.meta.variables])
     state.metadata.variables_lookup[test_variable].name = LANGUAGE_OBJECT
-    output = Callbacks.update_variable_table_language(
+    output = update_variable_table_language(
         [v.dict() for v in state.metadata.meta.variables],
         SupportedLanguages.NORSK_BOKMÅL,
     )
@@ -141,6 +164,47 @@ def test_nonetype_value_for_language_string():
     state.metadata = DataDocMetadata(TEST_PARQUET_FILEPATH)
     state.metadata.variables_lookup["pers_id"].name = LANGUAGE_OBJECT
     state.current_metadata_language = SupportedLanguages.NORSK_NYNORSK
-    Callbacks.accept_variable_metadata_input(DATA_NONETYPE, DATA_ORIGINAL)
+    accept_variable_metadata_input(DATA_NONETYPE, DATA_ORIGINAL)
 
     assert state.metadata.variables_lookup["pers_id"].name == LANGUAGE_OBJECT
+
+
+def test_update_variable_table_dropdown_options_for_language():
+    options = update_variable_table_dropdown_options_for_language(
+        SupportedLanguages.NORSK_BOKMÅL
+    )
+    assert all(k in DataDocVariable.__fields__.keys() for k in options.keys())
+    assert all(list(v.keys()) == ["options"] for v in options.values())
+    assert all(
+        list(d.keys()) == ["label", "value"]
+        for v in options.values()
+        for d in list(v.values())[0]
+    )
+    assert [d["label"] for d in options["data_type"]["options"]] == [
+        i.get_value_for_language(SupportedLanguages.NORSK_BOKMÅL) for i in Datatype
+    ]
+
+
+def test_update_global_language_state():
+    language: SupportedLanguages = random.choice(list(SupportedLanguages))
+    update_global_language_state(language)
+    assert state.current_metadata_language == language
+
+
+def test_change_language_dataset_metadata():
+    state.metadata = DataDocMetadata(TEST_PARQUET_FILEPATH)
+    value = change_language_dataset_metadata(SupportedLanguages.NORSK_NYNORSK)
+    test = random.choice(DISPLAYED_DROPDOWN_DATASET_ENUMS)
+    assert isinstance(value, tuple)
+
+    for options in value[0:-1]:
+        assert all(list(d.keys()) == ["label", "value"] for d in options)
+
+        member_names = set(test._member_names_)
+        values = [i for d in options for i in d.values()]
+
+        if member_names.intersection(values):
+            assert {d["label"] for d in options} == {
+                e.get_value_for_language(SupportedLanguages.NORSK_NYNORSK) for e in test
+            }
+            assert {d["value"] for d in options} == {e.name for e in test}
diff --git a/datadoc/tests/test_datadoc_metadata.py b/datadoc/tests/test_datadoc_metadata.py
index f6c61437..b845bd16 100644
--- a/datadoc/tests/test_datadoc_metadata.py
+++ b/datadoc/tests/test_datadoc_metadata.py
@@ -1,13 +1,14 @@
-from copy import copy
 import os
-from pathlib import PurePath
 import shutil
+from copy import copy
+from pathlib import PurePath
+
 import pytest
+from datadoc.backend.DataDocMetadata import DataDocMetadata
 from datadoc_model import Enums
-
 from datadoc_model.Enums import DatasetState
-from datadoc.backend.DataDocMetadata import DataDocMetadata
 from datadoc_model.Model import DataDocDataSet, DataDocVariable, MetadataDocument
+
 from .utils import (
     TEST_EXISTING_METADATA_FILE_NAME,
     TEST_EXISTING_METADATA_FILEPATH,
diff --git a/datadoc/tests/test_dataset_reader.py b/datadoc/tests/test_dataset_reader.py
index 7b150ae2..6d4c7f38 100644
--- a/datadoc/tests/test_dataset_reader.py
+++ b/datadoc/tests/test_dataset_reader.py
@@ -1,10 +1,6 @@
 import random
-from pytest import raises
 
-from datadoc_model.Model import DataDocVariable, LanguageStrings
-from datadoc_model.Enums import Datatype
 from datadoc import state
-from datadoc.Enums import SupportedLanguages
 from datadoc.backend.DatasetReader import (
     KNOWN_BOOLEAN_TYPES,
     KNOWN_DATETIME_TYPES,
@@ -13,6 +9,10 @@
     KNOWN_STRING_TYPES,
     DatasetReader,
 )
+from datadoc_model.Enums import Datatype, SupportedLanguages
+from datadoc_model.Model import DataDocVariable, LanguageStrings
+from pytest import raises
+
 from .utils import TEST_PARQUET_FILEPATH, TEST_SAS7BDAT_FILEPATH
 
 
diff --git a/datadoc/tests/test_model.py b/datadoc/tests/test_model.py
index 7ad6b821..02062989 100644
--- a/datadoc/tests/test_model.py
+++ b/datadoc/tests/test_model.py
@@ -3,16 +3,13 @@
     DISPLAY_VARIABLES,
     VariableIdentifiers,
 )
-from datadoc_model.Model import (
-    DataDocDataSet,
-    DataDocVariable,
-)
+from datadoc_model.Model import DataDocDataSet, DataDocVariable
 
 
 def test_dataset_metadata_definition_parity():
     """The metadata fields are currently defined in multiple places for technical reasons. We want these to always be exactly identical."""
     assert [i.value for i in DatasetIdentifiers] == list(DataDocDataSet().dict().keys())
-    assert [i for i in DatasetIdentifiers] == list(DISPLAY_DATASET.keys())
+    assert list(DatasetIdentifiers) == list(DISPLAY_DATASET.keys())
 
 
 def test_variables_metadata_definition_parity():
@@ -20,4 +17,4 @@ def test_variables_metadata_definition_parity():
     assert [i.value for i in VariableIdentifiers] == list(
         DataDocVariable().dict().keys()
     )
-    assert [i for i in VariableIdentifiers] == list(DISPLAY_VARIABLES.keys())
+    assert list(VariableIdentifiers) == list(DISPLAY_VARIABLES.keys())
diff --git a/datadoc/tests/test_smoke.py b/datadoc/tests/test_smoke.py
index c904994a..327098ca 100644
--- a/datadoc/tests/test_smoke.py
+++ b/datadoc/tests/test_smoke.py
@@ -1,8 +1,8 @@
+import datadoc.state as state
 from dash import Dash
-
 from datadoc.app import build_app
 from datadoc.backend.DataDocMetadata import DataDocMetadata
-import datadoc.state as state
+
 from .utils import TEST_PARQUET_FILEPATH
 
 
diff --git a/datadoc/tests/test_utils.py b/datadoc/tests/test_utils.py
index 8ec675ac..fdaca0f4 100644
--- a/datadoc/tests/test_utils.py
+++ b/datadoc/tests/test_utils.py
@@ -1,8 +1,7 @@
-import sys
-from datadoc_model.Model import DataDocVariable, LanguageStrings
-from datadoc.Enums import SupportedLanguages
+from datadoc.tests.test_callbacks import BOKMÅL_NAME, LANGUAGE_OBJECT
 from datadoc.utils import calculate_percentage, get_display_values, running_in_notebook
-from datadoc.tests.test_callbacks import LANGUAGE_OBJECT, BOKMÅL_NAME
+from datadoc_model.Enums import SupportedLanguages
+from datadoc_model.Model import DataDocVariable
 
 
 def test_not_running_in_notebook():
diff --git a/datadoc/tests/utils.py b/datadoc/tests/utils.py
index 3bb82d20..23665480 100644
--- a/datadoc/tests/utils.py
+++ b/datadoc/tests/utils.py
@@ -1,6 +1,5 @@
 import os
 
-
 TEST_RESOURCES_DIRECTORY = "datadoc/tests/resources/"
 TEST_PARQUET_FILEPATH = os.path.join(TEST_RESOURCES_DIRECTORY, "person_data_v1.parquet")
 TEST_SAS7BDAT_FILEPATH = os.path.join(TEST_RESOURCES_DIRECTORY, "sasdata.sas7bdat")