Переглянути джерело

Merge pull request #437 from rbeeli/plotly_orjson

Plotly extension/improvement + orjson integration in NiceGUI
Falko Schindler 2 роки тому
батько
коміт
a743c5f3d5

+ 2 - 1
nicegui/client.py

@@ -1,5 +1,4 @@
 import asyncio
 import asyncio
-import json
 import time
 import time
 import uuid
 import uuid
 from pathlib import Path
 from pathlib import Path
@@ -9,6 +8,8 @@ from fastapi import Request
 from fastapi.responses import Response
 from fastapi.responses import Response
 from fastapi.templating import Jinja2Templates
 from fastapi.templating import Jinja2Templates
 
 
+from nicegui import json
+
 from . import globals, outbox
 from . import globals, outbox
 from .dependencies import generate_js_imports, generate_vue_content
 from .dependencies import generate_js_imports, generate_vue_content
 from .element import Element
 from .element import Element

+ 2 - 1
nicegui/element.py

@@ -1,6 +1,5 @@
 from __future__ import annotations
 from __future__ import annotations
 
 
-import json
 import re
 import re
 from abc import ABC
 from abc import ABC
 from copy import deepcopy
 from copy import deepcopy
@@ -8,6 +7,8 @@ from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Union
 
 
 from typing_extensions import Self
 from typing_extensions import Self
 
 
+from nicegui import json
+
 from . import binding, events, globals, outbox
 from . import binding, events, globals, outbox
 from .elements.mixins.visibility import Visibility
 from .elements.mixins.visibility import Visibility
 from .event_listener import EventListener
 from .event_listener import EventListener

+ 0 - 19
nicegui/elements/plotly.js

@@ -1,19 +0,0 @@
-export default {
-  template: `<div></div>`,
-  mounted() {
-    setTimeout(() => {
-      import(window.path_prefix + this.lib).then(() => {
-        Plotly.newPlot(this.$el.id, this.options.data, this.options.layout);
-      });
-    }, 0); // NOTE: wait for window.path_prefix to be set in app.mounted()
-  },
-  methods: {
-    update(options) {
-      Plotly.newPlot(this.$el.id, options.data, options.layout);
-    },
-  },
-  props: {
-    options: Object,
-    lib: String,
-  },
-};

+ 31 - 7
nicegui/elements/plotly.py

@@ -1,29 +1,53 @@
-import json
+from typing import Dict, Union
 
 
 import plotly.graph_objects as go
 import plotly.graph_objects as go
 
 
 from ..dependencies import js_dependencies, register_component
 from ..dependencies import js_dependencies, register_component
 from ..element import Element
 from ..element import Element
 
 
-register_component('plotly', __file__, 'plotly.js', [], ['lib/plotly.min.js'])
+register_component('plotly', __file__, 'plotly.vue', [], ['lib/plotly.min.js'])
 
 
 
 
 class Plotly(Element):
 class Plotly(Element):
 
 
-    def __init__(self, figure: go.Figure) -> None:
+    def __init__(self, figure: Union[Dict, go.Figure]) -> None:
         """Plotly Element
         """Plotly Element
 
 
-        Renders a plotly figure onto the page.
+        Renders a Plotly chart.
+        There are two ways to pass a Plotly figure for rendering, see parameter `figure`:
 
 
-        See `plotly documentation <https://plotly.com/python/>`_ for more information.
+        * Pass a `go.Figure` object, see https://plotly.com/python/
 
 
-        :param figure: the plotly figure to be displayed
+        * Pass a Python `dict` object with keys `data`, `layout`, `config` (optional), see https://plotly.com/javascript/
+
+        For best performance, use the declarative `dict` approach for creating a Plotly chart.
+
+        :param figure: Plotly figure to be rendered. Can be either a `go.Figure` instance, or
+                       a `dict` object with keys `data`, `layout`, `config` (optional).
         """
         """
         super().__init__('plotly')
         super().__init__('plotly')
+
         self.figure = figure
         self.figure = figure
         self._props['lib'] = [d.import_path for d in js_dependencies.values() if d.path.name == 'plotly.min.js'][0]
         self._props['lib'] = [d.import_path for d in js_dependencies.values() if d.path.name == 'plotly.min.js'][0]
         self.update()
         self.update()
 
 
+    def update_figure(self, figure: Union[Dict, go.Figure]):
+        """Overrides figure instance of this Plotly chart and updates chart on client side."""
+        self.figure = figure
+        self.update()
+
     def update(self) -> None:
     def update(self) -> None:
-        self._props['options'] = json.loads(self.figure.to_json())
+        self._props['options'] = self._get_figure_json()
         self.run_method('update', self._props['options'])
         self.run_method('update', self._props['options'])
+
+    def _get_figure_json(self) -> Dict:
+        if isinstance(self.figure, go.Figure):
+            # convert go.Figure to dict object which is directly JSON serializable
+            # orjson supports numpy array serialization
+            return self.figure.to_plotly_json()
+
+        if isinstance(self.figure, dict):
+            # already a dict object with keys: data, layout, config (optional)
+            return self.figure
+
+        raise ValueError(f'Plotly figure is of unknown type "{self.figure.__class__.__name__}".')

+ 91 - 0
nicegui/elements/plotly.vue

@@ -0,0 +1,91 @@
+<template>
+  <div></div>
+</template>
+
+<script>
+export default {
+  mounted() {
+    setTimeout(() => {
+      this.ensureLibLoaded().then(() => {
+        // initial rendering of chart
+        Plotly.newPlot(this.$el.id, this.options.data, this.options.layout, this.options.config);
+
+        // register resize observer on parent div to auto-resize Plotly chart
+        const doResize = () => {
+          // only call resize if actually visible, otherwise error in Plotly.js internals
+          if (this.isHidden(this.$el)) return;
+          Plotly.Plots.resize(this.$el);
+        };
+
+        // throttle Plotly resize calls for better performance
+        // using HTML5 ResizeObserver on parent div
+        this.resizeObserver = new ResizeObserver((entries) => {
+          if (this.timeoutHandle) {
+            clearTimeout(this.timeoutHandle);
+          }
+          this.timeoutHandle = setTimeout(doResize, this.throttleResizeMs);
+        });
+        this.resizeObserver.observe(this.$el);
+      });
+    }, 0); // NOTE: wait for window.path_prefix to be set in app.mounted()
+  },
+  unmounted() {
+    this.resizeObserver.disconnect();
+    clearTimeout(this.timeoutHandle);
+  },
+
+  methods: {
+    isHidden(gd) {
+      // matches plotly's implementation, as it needs to in order
+      // to only resize the plot when plotly is rendering it.
+      // https://github.com/plotly/plotly.js/blob/e1d94b7afad94152db004b3bd5e6060010fbcc28/src/lib/index.js#L1278
+      var display = window.getComputedStyle(gd).display;
+      return !display || display === "none";
+    },
+
+    ensureLibLoaded() {
+      // ensure Plotly imported (lazy-load)
+      return import(window.path_prefix + this.lib);
+    },
+
+    update(options) {
+      // ensure Plotly imported, otherwise first plot will fail in update call
+      // because library not loaded yet
+      this.ensureLibLoaded().then(() => {
+        Plotly.newPlot(this.$el.id, options.data, options.layout, options.config);
+      });
+    },
+  },
+
+  data: function () {
+    return {
+      resizeObserver: undefined,
+      timeoutHandle: undefined,
+      throttleResizeMs: 100, // resize at most every 100 ms
+    };
+  },
+
+  props: {
+    options: Object,
+    lib: String,
+  },
+};
+</script>
+
+<style>
+/*
+  fix styles to correctly render modebar, otherwise large
+  buttons with unwanted line breaks are shown, possibly
+  due to other CSS libraries overriding default styles
+  affecting plotly styling.
+*/
+.js-plotly-plot .plotly .modebar-group {
+  display: flex;
+}
+.js-plotly-plot .plotly .modebar-btn {
+  display: flex;
+}
+.js-plotly-plot .plotly .modebar-btn svg {
+  position: static;
+}
+</style>

+ 15 - 0
nicegui/json/__init__.py

@@ -0,0 +1,15 @@
+"""
+Custom json module. Provides dumps and loads implementations
+wrapping the `orjson` package.
+
+Custom module required in order to override json-module used
+in socketio.AsyncServer, which expects a module as parameter
+to override Python's default json module.
+"""
+
+from nicegui.json.orjson_wrapper import dumps, loads
+
+__all__ = [
+    'dumps',
+    'loads'
+]

+ 13 - 0
nicegui/json/fastapi.py

@@ -0,0 +1,13 @@
+from typing import Any
+
+import orjson
+from fastapi import Response
+
+from nicegui.json.orjson_wrapper import ORJSON_OPTS
+
+
+class NiceGUIJSONResponse(Response):
+    media_type = 'application/json'
+
+    def render(self, content: Any) -> bytes:
+        return orjson.dumps(content, option=ORJSON_OPTS)

+ 36 - 0
nicegui/json/orjson_wrapper.py

@@ -0,0 +1,36 @@
+from typing import Any, Optional, Tuple
+
+import orjson
+
+ORJSON_OPTS = orjson.OPT_SERIALIZE_NUMPY | orjson.OPT_NON_STR_KEYS
+
+
+def dumps(obj: Any, sort_keys: bool = False, separators: Optional[Tuple[str, str]] = None):
+    """Serializes a Python object to a JSON-encoded string.
+
+    By default, this function supports serializing numpy arrays,
+    which Python's json module does not.
+
+    Uses package `orjson` internally.
+    """
+    # note that parameters `sort_keys` and `separators` are required by AsyncServer's
+    # internal calls, which match Python's default `json.dumps` API.
+    assert separators is None or separators == (',', ':'), \
+        f'NiceGUI JSON serializer only supports Python''s default ' +\
+        f'JSON separators "," and ":", but got {separators} instead.'
+
+    opts = ORJSON_OPTS
+
+    # flag for sorting by object keys
+    if sort_keys:
+        opts |= orjson.OPT_SORT_KEYS
+
+    return orjson.dumps(obj, option=opts).decode('utf-8')
+
+
+def loads(value: str) -> Any:
+    """Deserialize a JSON-encoded string to a corresponding Python object/value.
+
+    Uses package `orjson` internally.
+    """
+    return orjson.loads(value)

+ 7 - 3
nicegui/nicegui.py

@@ -8,7 +8,10 @@ from fastapi import HTTPException, Request
 from fastapi.middleware.gzip import GZipMiddleware
 from fastapi.middleware.gzip import GZipMiddleware
 from fastapi.responses import FileResponse, Response
 from fastapi.responses import FileResponse, Response
 from fastapi.staticfiles import StaticFiles
 from fastapi.staticfiles import StaticFiles
-from fastapi_socketio import SocketManager
+
+from nicegui import json
+from nicegui.json.fastapi import NiceGUIJSONResponse
+from nicegui.socket_manager import SocketManager
 
 
 from . import background_tasks, binding, globals, outbox
 from . import background_tasks, binding, globals, outbox
 from .app import App
 from .app import App
@@ -19,8 +22,9 @@ from .error import error_content
 from .helpers import safe_invoke
 from .helpers import safe_invoke
 from .page import page
 from .page import page
 
 
-globals.app = app = App()
-globals.sio = sio = SocketManager(app=app)._sio
+globals.app = app = App(default_response_class=NiceGUIJSONResponse)
+socket_manager = SocketManager(app=app, json=json)  # custom json module (wraps orjson)
+globals.sio = sio = app.sio
 
 
 app.add_middleware(GZipMiddleware)
 app.add_middleware(GZipMiddleware)
 app.mount('/_nicegui/static', StaticFiles(directory=Path(__file__).parent / 'static'), name='static')
 app.mount('/_nicegui/static', StaticFiles(directory=Path(__file__).parent / 'static'), name='static')

+ 105 - 0
nicegui/socket_manager.py

@@ -0,0 +1,105 @@
+from typing import Union
+
+import socketio
+from fastapi import FastAPI
+
+# TODO:
+# based on: https://github.com/pyropy/fastapi-socketio/blob/b37d58f0dc234b517f9ddf5c4c19ccd690f8fe07/fastapi_socketio/socket_manager.py
+# once kwargs is available in `fastapi_socketio` package, the package's implementation can be used
+# and this copy can be removed. We need kwargs in order to pass the json parameter to AsyncServer.
+
+
+class SocketManager:
+    """
+    Integrates SocketIO with FastAPI app.
+    Adds `sio` property to FastAPI object (app).
+
+    Default mount location for SocketIO app is at `/ws`
+    and default SocketIO path is `socket.io`.
+    (e.g. full path: `ws://www.example.com/ws/socket.io/)
+
+    SocketManager exposes basic underlying SocketIO functionality.
+
+    e.g. emit, on, send, call, etc.
+    """
+
+    def __init__(
+        self,
+        app: FastAPI,
+        mount_location: str = "/ws",
+        socketio_path: str = "socket.io",
+        cors_allowed_origins: Union[str, list] = '*',
+        async_mode: str = "asgi",
+        **kwargs
+    ) -> None:
+        # TODO: Change Cors policy based on fastapi cors Middleware
+        self._sio = socketio.AsyncServer(async_mode=async_mode, cors_allowed_origins=cors_allowed_origins, **kwargs)
+        self._app = socketio.ASGIApp(
+            socketio_server=self._sio, socketio_path=socketio_path
+        )
+
+        app.mount(mount_location, self._app)
+        app.sio = self._sio
+
+    def is_asyncio_based(self) -> bool:
+        return True
+
+    @property
+    def on(self):
+        return self._sio.on
+
+    @property
+    def attach(self):
+        return self._sio.attach
+
+    @property
+    def emit(self):
+        return self._sio.emit
+
+    @property
+    def send(self):
+        return self._sio.send
+
+    @property
+    def call(self):
+        return self._sio.call
+
+    @property
+    def close_room(self):
+        return self._sio.close_room
+
+    @property
+    def get_session(self):
+        return self._sio.get_session
+
+    @property
+    def save_session(self):
+        return self._sio.save_session
+
+    @property
+    def session(self):
+        return self._sio.session
+
+    @property
+    def disconnect(self):
+        return self._sio.disconnect
+
+    @property
+    def handle_request(self):
+        return self._sio.handle_request
+
+    @property
+    def start_background_task(self):
+        return self._sio.start_background_task
+
+    @property
+    def sleep(self):
+        return self._sio.sleep
+
+    @property
+    def enter_room(self):
+        return self._sio.enter_room
+
+    @property
+    def leave_room(self):
+        return self._sio.leave_room

+ 55 - 53
poetry.lock

@@ -437,7 +437,6 @@ files = [
     {file = "debugpy-1.6.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b5d1b13d7c7bf5d7cf700e33c0b8ddb7baf030fcf502f76fc061ddd9405d16c"},
     {file = "debugpy-1.6.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b5d1b13d7c7bf5d7cf700e33c0b8ddb7baf030fcf502f76fc061ddd9405d16c"},
     {file = "debugpy-1.6.6-cp38-cp38-win32.whl", hash = "sha256:70ab53918fd907a3ade01909b3ed783287ede362c80c75f41e79596d5ccacd32"},
     {file = "debugpy-1.6.6-cp38-cp38-win32.whl", hash = "sha256:70ab53918fd907a3ade01909b3ed783287ede362c80c75f41e79596d5ccacd32"},
     {file = "debugpy-1.6.6-cp38-cp38-win_amd64.whl", hash = "sha256:c05349890804d846eca32ce0623ab66c06f8800db881af7a876dc073ac1c2225"},
     {file = "debugpy-1.6.6-cp38-cp38-win_amd64.whl", hash = "sha256:c05349890804d846eca32ce0623ab66c06f8800db881af7a876dc073ac1c2225"},
-    {file = "debugpy-1.6.6-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:11a0f3a106f69901e4a9a5683ce943a7a5605696024134b522aa1bfda25b5fec"},
     {file = "debugpy-1.6.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a771739902b1ae22a120dbbb6bd91b2cae6696c0e318b5007c5348519a4211c6"},
     {file = "debugpy-1.6.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a771739902b1ae22a120dbbb6bd91b2cae6696c0e318b5007c5348519a4211c6"},
     {file = "debugpy-1.6.6-cp39-cp39-win32.whl", hash = "sha256:549ae0cb2d34fc09d1675f9b01942499751d174381b6082279cf19cdb3c47cbe"},
     {file = "debugpy-1.6.6-cp39-cp39-win32.whl", hash = "sha256:549ae0cb2d34fc09d1675f9b01942499751d174381b6082279cf19cdb3c47cbe"},
     {file = "debugpy-1.6.6-cp39-cp39-win_amd64.whl", hash = "sha256:de4a045fbf388e120bb6ec66501458d3134f4729faed26ff95de52a754abddb1"},
     {file = "debugpy-1.6.6-cp39-cp39-win_amd64.whl", hash = "sha256:de4a045fbf388e120bb6ec66501458d3134f4729faed26ff95de52a754abddb1"},
@@ -946,7 +945,6 @@ packaging = ">=20.0"
 pillow = ">=6.2.0"
 pillow = ">=6.2.0"
 pyparsing = ">=2.2.1"
 pyparsing = ">=2.2.1"
 python-dateutil = ">=2.7"
 python-dateutil = ">=2.7"
-setuptools_scm = ">=4,<7"
 
 
 [[package]]
 [[package]]
 name = "matplotlib"
 name = "matplotlib"
@@ -1086,6 +1084,60 @@ files = [
     {file = "numpy-1.24.1.tar.gz", hash = "sha256:2386da9a471cc00a1f47845e27d916d5ec5346ae9696e01a8a34760858fe9dd2"},
     {file = "numpy-1.24.1.tar.gz", hash = "sha256:2386da9a471cc00a1f47845e27d916d5ec5346ae9696e01a8a34760858fe9dd2"},
 ]
 ]
 
 
+[[package]]
+name = "orjson"
+version = "3.8.6"
+description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "orjson-3.8.6-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:062a9a74c10c439acc35cf67f31ac88d9464a11025700bab421e6cdf54a54a35"},
+    {file = "orjson-3.8.6-cp310-cp310-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:692c255109867cc8211267f4416d2915845273bf4f403bbca5419f5b15ac9175"},
+    {file = "orjson-3.8.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a20905c7a5ebc280343704c4dd19343ef966c9dea5a38ade6e0461a6deb8eda"},
+    {file = "orjson-3.8.6-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:34ce4a8b8f0fea483bce6985c015953f475540b7d756efd48a571b1803c318ee"},
+    {file = "orjson-3.8.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e57ecad7616ec842d8c382ed42a778cdcdadc67cfb46b804b43079f937b63b31"},
+    {file = "orjson-3.8.6-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:323065cf14fdd4096dbf93ea1634e7e030044af8c1000803bcdc132fbfd395f5"},
+    {file = "orjson-3.8.6-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4cb4f37fca8cf8309de421634447437f229bc03b240cec8ad4ac241fd4b1bcf4"},
+    {file = "orjson-3.8.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:32353b14c5e0b55b6a8759e993482a2d8c44d492489840718b74658de67671e2"},
+    {file = "orjson-3.8.6-cp310-none-win_amd64.whl", hash = "sha256:3e44f78db3a15902b5e8386119979691ed3dd61d1ded10bad2c7106fd50641ef"},
+    {file = "orjson-3.8.6-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:c59ec129d523abd4f2d65c0733d0e57af7dd09c69142f1aa564b04358f04ace3"},
+    {file = "orjson-3.8.6-cp311-cp311-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:d44d89314a66e98e690ce64c8771d963eb64ae6cea662d0a1d077ed024627228"},
+    {file = "orjson-3.8.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:865ef341c4d310ac2689bf811dbc0930b2f13272f8eade1511dc40b186f6d562"},
+    {file = "orjson-3.8.6-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:52809a37a0daa6992835ee0625aca22b4c0693dba3cb465948e6c9796de927b0"},
+    {file = "orjson-3.8.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d7402121d06d11fafcaed7d06f9d68b11bbe39868e0e1bc19239ee5b6b98b2b"},
+    {file = "orjson-3.8.6-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:583338b7dabb509ca4c3b4f160f58a5228bf6c6e0f8a2981663f683791f39d45"},
+    {file = "orjson-3.8.6-cp311-none-win_amd64.whl", hash = "sha256:4a6c0a0ef2f535ba7a5d01f014b53d05eeb372d43556edb25c75a4d52690a123"},
+    {file = "orjson-3.8.6-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:9d35573e7f5817a26d8ce1134c3463d31bc3b39aad3ad7ae06bb67d6078fa9c0"},
+    {file = "orjson-3.8.6-cp37-cp37m-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:94d8fdc12adc0450994931d722cb38be5e4caa273219881abb96c15a9e9f151f"},
+    {file = "orjson-3.8.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8fc43bfb73d394b9bf12062cd6dab72abf728ac7869f972e4bb7327fd3330b8"},
+    {file = "orjson-3.8.6-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a38387387139695a7e52b9f568e39c1632b22eb34939afc5efed265fa8277b84"},
+    {file = "orjson-3.8.6-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e048c6df7453c3da4de10fa5c44f6c655b157b712628888ce880cd5bbf30013"},
+    {file = "orjson-3.8.6-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:d3b0950d792b25c0aa52505faf09237fd98136d09616a0837f7cdb0fde9e2730"},
+    {file = "orjson-3.8.6-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:38bc8a388080d8fd297388bcff4939e350ffafe4a006567e0dd81cdb8c7b86fa"},
+    {file = "orjson-3.8.6-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:5b3251ab7113f2400d76f2b4a2d6592e7d5a5cf45fa948c894340553671ef8f1"},
+    {file = "orjson-3.8.6-cp37-none-win_amd64.whl", hash = "sha256:2c83a33cf389fd286bd9ef0befc406307444b9553d2e9ba14b90b9332524cfa6"},
+    {file = "orjson-3.8.6-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:53f51c23398cfe818d9bb09079d31a60c6cd77e7eee1d555cfcc735460db4190"},
+    {file = "orjson-3.8.6-cp38-cp38-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:6190e23a2fb9fc78228b289b3ec295094671ca0299319c8c72727aa9e7dbe06f"},
+    {file = "orjson-3.8.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61fff8a8b4cd4e489b291fe5105b6138b1831490f1a0dc726d5e17ebe811d595"},
+    {file = "orjson-3.8.6-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c192813f527f886bd85abc5a9e8d9dde16ffa06d7305de526a7c4657730dbf4e"},
+    {file = "orjson-3.8.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aae1487fba9d955b2679f0a697665ed8fc32563b3252acc240e097184c184e29"},
+    {file = "orjson-3.8.6-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:cd2bd48e9a14f2130790a3c2dcb897bd93c2e5c244919799430a6d9b8212cb50"},
+    {file = "orjson-3.8.6-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:006178fd654a0a4f14f5912b8320ba9a26ab9c0ae7ce1c7eeb4b5249d6cada29"},
+    {file = "orjson-3.8.6-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:9d5ad2fddccc89ab64b6333823b250ce8430fc51f014954e5a2d4c933f5deb9f"},
+    {file = "orjson-3.8.6-cp38-none-win_amd64.whl", hash = "sha256:aef3d558f5bd809733ebf2cbce7e1338ce62812db317478427236b97036aba0f"},
+    {file = "orjson-3.8.6-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:7d216a5f3d23eac2c7c654e7bd30280c27cdf5edc32325e6ad8e880d36c265b7"},
+    {file = "orjson-3.8.6-cp39-cp39-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:004122c95e08db7201b80224de3a8f2ad79b9717040e6884c6015f27b010127d"},
+    {file = "orjson-3.8.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:006c492577ad046cb7e50237a8d8935131a35f7e7f8320fbc3514da6fbc0b436"},
+    {file = "orjson-3.8.6-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:67554103b415349b6ee2db82d2422da1c8f4c2d280d20772217f6d1d227410b6"},
+    {file = "orjson-3.8.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:caa5053f19584816f063c887d94385db481fc01d995d6a717ce4fbb929653ec2"},
+    {file = "orjson-3.8.6-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:2bdd64566870a8a0bdcf8c7df2f4452391dd55070f5cd98cc581914e8c263d85"},
+    {file = "orjson-3.8.6-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:550a4dec128d1adfd0262ef9ad7878d62d1cc0bddaaa05e41d8ca28414dc86bc"},
+    {file = "orjson-3.8.6-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d3f5ad9442e8a99fb436279a8614a00aca272ea8dabb692cadee70a4874d6e03"},
+    {file = "orjson-3.8.6-cp39-none-win_amd64.whl", hash = "sha256:aa7b112e3273d1744f7bc983ffd3dd0d004062c69dfa68e119515a7e115c46c8"},
+    {file = "orjson-3.8.6.tar.gz", hash = "sha256:91ef8a554d33fbc5bb61c3972f3e8baa994f72c4967671e64e7dac1cc06f50e1"},
+]
+
 [[package]]
 [[package]]
 name = "outcome"
 name = "outcome"
 version = "1.2.0"
 version = "1.2.0"
@@ -1689,44 +1741,6 @@ trio = ">=0.17,<1.0"
 trio-websocket = ">=0.9,<1.0"
 trio-websocket = ">=0.9,<1.0"
 urllib3 = {version = ">=1.26,<2.0", extras = ["socks"]}
 urllib3 = {version = ">=1.26,<2.0", extras = ["socks"]}
 
 
-[[package]]
-name = "setuptools"
-version = "67.3.1"
-description = "Easily download, build, install, upgrade, and uninstall Python packages"
-category = "main"
-optional = false
-python-versions = ">=3.7"
-files = [
-    {file = "setuptools-67.3.1-py3-none-any.whl", hash = "sha256:23c86b4e44432bfd8899384afc08872ec166a24f48a3f99f293b0a557e6a6b5d"},
-    {file = "setuptools-67.3.1.tar.gz", hash = "sha256:daec07fd848d80676694d6bf69c009d28910aeece68a38dbe88b7e1bb6dba12e"},
-]
-
-[package.extras]
-docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
-testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
-testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
-
-[[package]]
-name = "setuptools-scm"
-version = "6.4.2"
-description = "the blessed package to manage your versions by scm tags"
-category = "main"
-optional = false
-python-versions = ">=3.6"
-files = [
-    {file = "setuptools_scm-6.4.2-py3-none-any.whl", hash = "sha256:acea13255093849de7ccb11af9e1fb8bde7067783450cee9ef7a93139bddf6d4"},
-    {file = "setuptools_scm-6.4.2.tar.gz", hash = "sha256:6833ac65c6ed9711a4d5d2266f8024cfa07c533a0e55f4c12f6eff280a5a9e30"},
-]
-
-[package.dependencies]
-packaging = ">=20.0"
-setuptools = "*"
-tomli = ">=1.0.0"
-
-[package.extras]
-test = ["pytest (>=6.2)", "virtualenv (>20)"]
-toml = ["setuptools (>=42)"]
-
 [[package]]
 [[package]]
 name = "six"
 name = "six"
 version = "1.16.0"
 version = "1.16.0"
@@ -1812,18 +1826,6 @@ files = [
     {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
     {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
 ]
 ]
 
 
-[[package]]
-name = "tomli"
-version = "2.0.1"
-description = "A lil' TOML parser"
-category = "main"
-optional = false
-python-versions = ">=3.7"
-files = [
-    {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
-    {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
-]
-
 [[package]]
 [[package]]
 name = "trio"
 name = "trio"
 version = "0.22.0"
 version = "0.22.0"
@@ -2126,4 +2128,4 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools"
 [metadata]
 [metadata]
 lock-version = "2.0"
 lock-version = "2.0"
 python-versions = "^3.7"
 python-versions = "^3.7"
-content-hash = "666b63ad439505ee4c09524a54824ee59b437d5145a98e012e7c87366ed2934f"
+content-hash = "60c458abf9ad315b8764d25db636ccf444468aa09d2ea2425f0d4ee36918ed89"

+ 1 - 0
pyproject.toml

@@ -25,6 +25,7 @@ watchfiles = "^0.18.1"
 jinja2 = "^3.1.2"
 jinja2 = "^3.1.2"
 python-multipart = "^0.0.5"
 python-multipart = "^0.0.5"
 plotly = "^5.13.0"
 plotly = "^5.13.0"
+orjson = "^3.8.6"
 
 
 [tool.poetry.group.dev.dependencies]
 [tool.poetry.group.dev.dependencies]
 icecream = "^2.1.0"
 icecream = "^2.1.0"