Browse Source

Merge pull request #437 from rbeeli/plotly_orjson

Plotly extension/improvement + orjson integration in NiceGUI
Falko Schindler 2 years ago
parent
commit
a743c5f3d5

+ 2 - 1
nicegui/client.py

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

+ 2 - 1
nicegui/element.py

@@ -1,6 +1,5 @@
 from __future__ import annotations
 
-import json
 import re
 from abc import ABC
 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 nicegui import json
+
 from . import binding, events, globals, outbox
 from .elements.mixins.visibility import Visibility
 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
 
 from ..dependencies import js_dependencies, register_component
 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):
 
-    def __init__(self, figure: go.Figure) -> None:
+    def __init__(self, figure: Union[Dict, go.Figure]) -> None:
         """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')
+
         self.figure = figure
         self._props['lib'] = [d.import_path for d in js_dependencies.values() if d.path.name == 'plotly.min.js'][0]
         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:
-        self._props['options'] = json.loads(self.figure.to_json())
+        self._props['options'] = self._get_figure_json()
         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.responses import FileResponse, Response
 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 .app import App
@@ -19,8 +22,9 @@ from .error import error_content
 from .helpers import safe_invoke
 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.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-win32.whl", hash = "sha256:70ab53918fd907a3ade01909b3ed783287ede362c80c75f41e79596d5ccacd32"},
     {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-win32.whl", hash = "sha256:549ae0cb2d34fc09d1675f9b01942499751d174381b6082279cf19cdb3c47cbe"},
     {file = "debugpy-1.6.6-cp39-cp39-win_amd64.whl", hash = "sha256:de4a045fbf388e120bb6ec66501458d3134f4729faed26ff95de52a754abddb1"},
@@ -946,7 +945,6 @@ packaging = ">=20.0"
 pillow = ">=6.2.0"
 pyparsing = ">=2.2.1"
 python-dateutil = ">=2.7"
-setuptools_scm = ">=4,<7"
 
 [[package]]
 name = "matplotlib"
@@ -1086,6 +1084,60 @@ files = [
     {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]]
 name = "outcome"
 version = "1.2.0"
@@ -1689,44 +1741,6 @@ trio = ">=0.17,<1.0"
 trio-websocket = ">=0.9,<1.0"
 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]]
 name = "six"
 version = "1.16.0"
@@ -1812,18 +1826,6 @@ files = [
     {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]]
 name = "trio"
 version = "0.22.0"
@@ -2126,4 +2128,4 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools"
 [metadata]
 lock-version = "2.0"
 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"
 python-multipart = "^0.0.5"
 plotly = "^5.13.0"
+orjson = "^3.8.6"
 
 [tool.poetry.group.dev.dependencies]
 icecream = "^2.1.0"