Browse Source

Add benchmarking tests (#2143)

Alek Petuskey 1 year ago
parent
commit
dd982c5ace

+ 10 - 4
.github/workflows/lighthouse.yml → .github/workflows/benchmarks.yml

@@ -1,4 +1,4 @@
-name: lighthouse-tests
+name: benchmarking
 
 
 on:
 on:
   push:
   push:
@@ -62,8 +62,14 @@ jobs:
         run: |
         run: |
           # Check that npm is home
           # Check that npm is home
           npm -v
           npm -v
-          poetry run bash scripts/lighthouse.sh ./reflex-web prod
+          poetry run bash scripts/benchmarks.sh ./reflex-web prod
         env:
         env:
             LHCI_GITHUB_APP_TOKEN: $
             LHCI_GITHUB_APP_TOKEN: $
-            POSTHOG: $
-        
+      - name: Run Benchmarks
+        working-directory: ./integration/benchmarks
+        run: 
+          poetry run python benchmarks.py "$GITHUB_SHA" .lighthouseci
+        env:
+            GITHUB_SHA: ${{ github.sha }}
+            DATABASE_URL: ${{ secrets.DATABASE_URL }}
+            PR_TITLE: ${{ github.event.pull_request.title }}

+ 138 - 0
integration/benchmarks/benchmarks.py

@@ -0,0 +1,138 @@
+"""Runs the benchmarks and inserts the results into the database."""
+
+import json
+import os
+import sys
+
+import pytest
+from helpers import insert_benchmarking_data
+
+
+def get_lighthouse_scores(directory_path: str) -> dict:
+    """Extracts the Lighthouse scores from the JSON files in the specified directory.
+
+    Args:
+        directory_path (str): The path to the directory containing the JSON files.
+
+    Returns:
+        dict: The Lighthouse scores.
+    """
+    scores = {}
+
+    try:
+        for filename in os.listdir(directory_path):
+            if filename.endswith(".json") and filename != "manifest.json":
+                file_path = os.path.join(directory_path, filename)
+                with open(file_path, "r") as file:
+                    data = json.load(file)
+                    # Extract scores and add them to the dictionary with the filename as key
+                    scores[data["finalUrl"].replace("http://localhost:3000/", "")] = {
+                        "performance_score": data["categories"]["performance"]["score"],
+                        "accessibility_score": data["categories"]["accessibility"][
+                            "score"
+                        ],
+                        "best_practices_score": data["categories"]["best-practices"][
+                            "score"
+                        ],
+                        "seo_score": data["categories"]["seo"]["score"],
+                        "pwa_score": data["categories"]["pwa"]["score"],
+                    }
+    except Exception as e:
+        print(e)
+        return {"error": "Error parsing JSON files"}
+
+    return scores
+
+
+def run_pytest_and_get_results(test_path=None) -> dict:
+    """Runs pytest and returns the results.
+
+    Args:
+        test_path: The path to the tests to run.
+
+    Returns:
+        dict: The results of the tests.
+    """
+    # Set the default path to the current directory if no path is provided
+    if not test_path:
+        test_path = os.getcwd()
+    # Ensure you have installed the pytest-json plugin before running this
+    pytest_args = ["-v", "--benchmark-json=benchmark_report.json", test_path]
+
+    # Run pytest with the specified arguments
+    pytest.main(pytest_args)
+
+    # Print ls of the current directory
+    print(os.listdir())
+
+    with open("benchmark_report.json", "r") as file:
+        pytest_results = json.load(file)
+
+    return pytest_results
+
+
+def extract_stats_from_json(json_data) -> list[dict]:
+    """Extracts the stats from the JSON data and returns them as a list of dictionaries.
+
+    Args:
+        json_data: The JSON data to extract the stats from.
+
+    Returns:
+        list[dict]: The stats for each test.
+    """
+    # Load the JSON data if it is a string, otherwise assume it's already a dictionary
+    data = json.loads(json_data) if isinstance(json_data, str) else json_data
+
+    # Initialize an empty list to store the stats for each test
+    test_stats = []
+
+    # Iterate over each test in the 'benchmarks' list
+    for test in data.get("benchmarks", []):
+        stats = test.get("stats", {})
+        test_name = test.get("name", "Unknown Test")
+        min_value = stats.get("min", None)
+        max_value = stats.get("max", None)
+        mean_value = stats.get("mean", None)
+        stdev_value = stats.get("stddev", None)
+
+        test_stats.append(
+            {
+                "test_name": test_name,
+                "min": min_value,
+                "max": max_value,
+                "mean": mean_value,
+                "stdev": stdev_value,
+            }
+        )
+
+    return test_stats
+
+
+def main():
+    """Runs the benchmarks and inserts the results into the database."""
+    # Get the commit SHA and JSON directory from the command line arguments
+    commit_sha = sys.argv[1]
+    json_dir = sys.argv[2]
+
+    # Get the PR title and database URL from the environment variables
+    pr_title = os.environ.get("PR_TITLE")
+    db_url = os.environ.get("DATABASE_URL")
+
+    if db_url is None or pr_title is None:
+        sys.exit("Missing environment variables")
+
+    # Run pytest and get the results
+    results = run_pytest_and_get_results()
+    cleaned_results = extract_stats_from_json(results)
+
+    # Get the Lighthouse scores
+    lighthouse_scores = get_lighthouse_scores(json_dir)
+
+    # Insert the data into the database
+    insert_benchmarking_data(
+        db_url, lighthouse_scores, cleaned_results, commit_sha, pr_title
+    )
+
+
+if __name__ == "__main__":
+    main()

+ 49 - 0
integration/benchmarks/helpers.py

@@ -0,0 +1,49 @@
+"""Helper functions for the benchmarking integration."""
+
+import json
+from datetime import datetime
+
+import psycopg2
+
+
+def insert_benchmarking_data(
+    db_connection_url: str,
+    lighthouse_data: dict,
+    performance_data: list[dict],
+    commit_sha: str,
+    pr_title: str,
+):
+    """Insert the benchmarking data into the database.
+
+    Args:
+        db_connection_url: The URL to connect to the database.
+        lighthouse_data: The Lighthouse data to insert.
+        performance_data: The performance data to insert.
+        commit_sha: The commit SHA to insert.
+        pr_title: The PR title to insert.
+    """
+    # Serialize the JSON data
+    lighthouse_json = json.dumps(lighthouse_data)
+    performance_json = json.dumps(performance_data)
+
+    # Get the current timestamp
+    current_timestamp = datetime.now()
+
+    # Connect to the database and insert the data
+    with psycopg2.connect(db_connection_url) as conn, conn.cursor() as cursor:
+        insert_query = """
+            INSERT INTO benchmarks (lighthouse, performance, commit_sha, pr_title, time)
+            VALUES (%s, %s, %s, %s, %s);
+            """
+        cursor.execute(
+            insert_query,
+            (
+                lighthouse_json,
+                performance_json,
+                commit_sha,
+                pr_title,
+                current_timestamp,
+            ),
+        )
+        # Commit the transaction
+        conn.commit()

+ 121 - 0
integration/benchmarks/test_compile_benchmark.py

@@ -0,0 +1,121 @@
+"""Benchmark the time it takes to compile a reflex app."""
+
+import importlib
+
+import reflex
+
+rx = reflex
+
+
+class State(rx.State):
+    """A simple state class with a count variable."""
+
+    count: int = 0
+
+    def increment(self):
+        """Increment the count."""
+        self.count += 1
+
+    def decrement(self):
+        """Decrement the count."""
+        self.count -= 1
+
+
+class SliderVariation(State):
+    """A simple state class with a count variable."""
+
+    value: int = 50
+
+    def set_end(self, value: int):
+        """Increment the count.
+
+        Args:
+            value: The value of the slider.
+        """
+        self.value = value
+
+
+def sample_small_page() -> rx.Component:
+    """A simple page with a button that increments the count.
+
+    Returns:
+        A reflex component.
+    """
+    return rx.vstack(
+        *[rx.button(State.count, font_size="2em") for i in range(100)],
+        spacing="1em",
+    )
+
+
+def sample_large_page() -> rx.Component:
+    """A large page with a slider that increments the count.
+
+    Returns:
+        A reflex component.
+    """
+    return rx.vstack(
+        *[
+            rx.vstack(
+                rx.heading(SliderVariation.value),
+                rx.slider(on_change_end=SliderVariation.set_end),
+                width="100%",
+            )
+            for i in range(100)
+        ],
+        spacing="1em",
+    )
+
+
+def add_small_pages(app: rx.App):
+    """Add 10 small pages to the app.
+
+    Args:
+        app: The reflex app to add the pages to.
+    """
+    for i in range(10):
+        app.add_page(sample_small_page, route=f"/{i}")
+
+
+def add_large_pages(app: rx.App):
+    """Add 10 large pages to the app.
+
+    Args:
+        app: The reflex app to add the pages to.
+    """
+    for i in range(10):
+        app.add_page(sample_large_page, route=f"/{i}")
+
+
+def test_mean_import_time(benchmark):
+    """Test that the mean import time is less than 1 second.
+
+    Args:
+        benchmark: The benchmark fixture.
+    """
+
+    def import_reflex():
+        importlib.reload(reflex)
+
+    # Benchmark the import
+    benchmark(import_reflex)
+
+
+def test_mean_add_small_page_time(benchmark):
+    """Test that the mean add page time is less than 1 second.
+
+    Args:
+        benchmark: The benchmark fixture.
+    """
+    app = rx.App(state=State)
+    benchmark(add_small_pages, app)
+
+
+def test_mean_add_large_page_time(benchmark):
+    """Test that the mean add page time is less than 1 second.
+
+    Args:
+        benchmark: The benchmark fixture.
+    """
+    app = rx.App(state=State)
+    results = benchmark(add_large_pages, app)
+    print(results)

+ 149 - 6
poetry.lock

@@ -1,9 +1,10 @@
-# This file is automatically @generated by Poetry 1.7.0 and should not be changed by hand.
+# This file is automatically @generated by Poetry and should not be changed by hand.
 
 
 [[package]]
 [[package]]
 name = "alembic"
 name = "alembic"
 version = "1.12.1"
 version = "1.12.1"
 description = "A database migration tool for SQLAlchemy."
 description = "A database migration tool for SQLAlchemy."
+category = "main"
 optional = false
 optional = false
 python-versions = ">=3.7"
 python-versions = ">=3.7"
 files = [
 files = [
@@ -25,6 +26,7 @@ tz = ["python-dateutil"]
 name = "anyio"
 name = "anyio"
 version = "4.0.0"
 version = "4.0.0"
 description = "High level compatibility layer for multiple asynchronous event loop implementations"
 description = "High level compatibility layer for multiple asynchronous event loop implementations"
+category = "main"
 optional = false
 optional = false
 python-versions = ">=3.8"
 python-versions = ">=3.8"
 files = [
 files = [
@@ -46,6 +48,7 @@ trio = ["trio (>=0.22)"]
 name = "async-timeout"
 name = "async-timeout"
 version = "4.0.3"
 version = "4.0.3"
 description = "Timeout context manager for asyncio programs"
 description = "Timeout context manager for asyncio programs"
+category = "main"
 optional = false
 optional = false
 python-versions = ">=3.7"
 python-versions = ">=3.7"
 files = [
 files = [
@@ -57,6 +60,7 @@ files = [
 name = "asynctest"
 name = "asynctest"
 version = "0.13.0"
 version = "0.13.0"
 description = "Enhance the standard unittest package with features for testing asyncio libraries"
 description = "Enhance the standard unittest package with features for testing asyncio libraries"
+category = "dev"
 optional = false
 optional = false
 python-versions = ">=3.5"
 python-versions = ">=3.5"
 files = [
 files = [
@@ -68,6 +72,7 @@ files = [
 name = "attrs"
 name = "attrs"
 version = "23.1.0"
 version = "23.1.0"
 description = "Classes Without Boilerplate"
 description = "Classes Without Boilerplate"
+category = "dev"
 optional = false
 optional = false
 python-versions = ">=3.7"
 python-versions = ">=3.7"
 files = [
 files = [
@@ -86,6 +91,7 @@ tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pyte
 name = "bidict"
 name = "bidict"
 version = "0.22.1"
 version = "0.22.1"
 description = "The bidirectional mapping library for Python."
 description = "The bidirectional mapping library for Python."
+category = "main"
 optional = false
 optional = false
 python-versions = ">=3.7"
 python-versions = ">=3.7"
 files = [
 files = [
@@ -102,6 +108,7 @@ test = ["hypothesis", "pytest", "pytest-benchmark[histogram]", "pytest-cov", "py
 name = "black"
 name = "black"
 version = "22.12.0"
 version = "22.12.0"
 description = "The uncompromising code formatter."
 description = "The uncompromising code formatter."
+category = "dev"
 optional = false
 optional = false
 python-versions = ">=3.7"
 python-versions = ">=3.7"
 files = [
 files = [
@@ -137,6 +144,7 @@ uvloop = ["uvloop (>=0.15.2)"]
 name = "certifi"
 name = "certifi"
 version = "2023.7.22"
 version = "2023.7.22"
 description = "Python package for providing Mozilla's CA Bundle."
 description = "Python package for providing Mozilla's CA Bundle."
+category = "main"
 optional = false
 optional = false
 python-versions = ">=3.6"
 python-versions = ">=3.6"
 files = [
 files = [
@@ -148,6 +156,7 @@ files = [
 name = "cffi"
 name = "cffi"
 version = "1.16.0"
 version = "1.16.0"
 description = "Foreign Function Interface for Python calling C code."
 description = "Foreign Function Interface for Python calling C code."
+category = "dev"
 optional = false
 optional = false
 python-versions = ">=3.8"
 python-versions = ">=3.8"
 files = [
 files = [
@@ -212,6 +221,7 @@ pycparser = "*"
 name = "cfgv"
 name = "cfgv"
 version = "3.4.0"
 version = "3.4.0"
 description = "Validate configuration and produce human readable error messages."
 description = "Validate configuration and produce human readable error messages."
+category = "dev"
 optional = false
 optional = false
 python-versions = ">=3.8"
 python-versions = ">=3.8"
 files = [
 files = [
@@ -223,6 +233,7 @@ files = [
 name = "click"
 name = "click"
 version = "8.1.7"
 version = "8.1.7"
 description = "Composable command line interface toolkit"
 description = "Composable command line interface toolkit"
+category = "main"
 optional = false
 optional = false
 python-versions = ">=3.7"
 python-versions = ">=3.7"
 files = [
 files = [
@@ -237,6 +248,7 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""}
 name = "cloudpickle"
 name = "cloudpickle"
 version = "2.2.1"
 version = "2.2.1"
 description = "Extended pickling support for Python objects"
 description = "Extended pickling support for Python objects"
+category = "main"
 optional = false
 optional = false
 python-versions = ">=3.6"
 python-versions = ">=3.6"
 files = [
 files = [
@@ -248,6 +260,7 @@ files = [
 name = "colorama"
 name = "colorama"
 version = "0.4.6"
 version = "0.4.6"
 description = "Cross-platform colored terminal text."
 description = "Cross-platform colored terminal text."
+category = "main"
 optional = false
 optional = false
 python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
 python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
 files = [
 files = [
@@ -259,6 +272,7 @@ files = [
 name = "coverage"
 name = "coverage"
 version = "7.3.2"
 version = "7.3.2"
 description = "Code coverage measurement for Python"
 description = "Code coverage measurement for Python"
+category = "dev"
 optional = false
 optional = false
 python-versions = ">=3.8"
 python-versions = ">=3.8"
 files = [
 files = [
@@ -326,6 +340,7 @@ toml = ["tomli"]
 name = "darglint"
 name = "darglint"
 version = "1.8.1"
 version = "1.8.1"
 description = "A utility for ensuring Google-style docstrings stay up to date with the source code."
 description = "A utility for ensuring Google-style docstrings stay up to date with the source code."
+category = "dev"
 optional = false
 optional = false
 python-versions = ">=3.6,<4.0"
 python-versions = ">=3.6,<4.0"
 files = [
 files = [
@@ -337,6 +352,7 @@ files = [
 name = "distlib"
 name = "distlib"
 version = "0.3.7"
 version = "0.3.7"
 description = "Distribution utilities"
 description = "Distribution utilities"
+category = "dev"
 optional = false
 optional = false
 python-versions = "*"
 python-versions = "*"
 files = [
 files = [
@@ -348,6 +364,7 @@ files = [
 name = "distro"
 name = "distro"
 version = "1.8.0"
 version = "1.8.0"
 description = "Distro - an OS platform information API"
 description = "Distro - an OS platform information API"
+category = "main"
 optional = false
 optional = false
 python-versions = ">=3.6"
 python-versions = ">=3.6"
 files = [
 files = [
@@ -359,6 +376,7 @@ files = [
 name = "exceptiongroup"
 name = "exceptiongroup"
 version = "1.1.3"
 version = "1.1.3"
 description = "Backport of PEP 654 (exception groups)"
 description = "Backport of PEP 654 (exception groups)"
+category = "main"
 optional = false
 optional = false
 python-versions = ">=3.7"
 python-versions = ">=3.7"
 files = [
 files = [
@@ -373,6 +391,7 @@ test = ["pytest (>=6)"]
 name = "fastapi"
 name = "fastapi"
 version = "0.96.1"
 version = "0.96.1"
 description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
 description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
+category = "main"
 optional = false
 optional = false
 python-versions = ">=3.7"
 python-versions = ">=3.7"
 files = [
 files = [
@@ -394,6 +413,7 @@ test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==23.1.0)", "coverage[toml] (>=6
 name = "filelock"
 name = "filelock"
 version = "3.13.1"
 version = "3.13.1"
 description = "A platform independent file lock."
 description = "A platform independent file lock."
+category = "dev"
 optional = false
 optional = false
 python-versions = ">=3.8"
 python-versions = ">=3.8"
 files = [
 files = [
@@ -410,6 +430,7 @@ typing = ["typing-extensions (>=4.8)"]
 name = "greenlet"
 name = "greenlet"
 version = "3.0.1"
 version = "3.0.1"
 description = "Lightweight in-process concurrent programming"
 description = "Lightweight in-process concurrent programming"
+category = "main"
 optional = false
 optional = false
 python-versions = ">=3.7"
 python-versions = ">=3.7"
 files = [
 files = [
@@ -480,6 +501,7 @@ test = ["objgraph", "psutil"]
 name = "gunicorn"
 name = "gunicorn"
 version = "20.1.0"
 version = "20.1.0"
 description = "WSGI HTTP Server for UNIX"
 description = "WSGI HTTP Server for UNIX"
+category = "main"
 optional = false
 optional = false
 python-versions = ">=3.5"
 python-versions = ">=3.5"
 files = [
 files = [
@@ -500,6 +522,7 @@ tornado = ["tornado (>=0.2)"]
 name = "h11"
 name = "h11"
 version = "0.14.0"
 version = "0.14.0"
 description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
 description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
+category = "main"
 optional = false
 optional = false
 python-versions = ">=3.7"
 python-versions = ">=3.7"
 files = [
 files = [
@@ -511,6 +534,7 @@ files = [
 name = "httpcore"
 name = "httpcore"
 version = "0.17.3"
 version = "0.17.3"
 description = "A minimal low-level HTTP client."
 description = "A minimal low-level HTTP client."
+category = "main"
 optional = false
 optional = false
 python-versions = ">=3.7"
 python-versions = ">=3.7"
 files = [
 files = [
@@ -522,16 +546,17 @@ files = [
 anyio = ">=3.0,<5.0"
 anyio = ">=3.0,<5.0"
 certifi = "*"
 certifi = "*"
 h11 = ">=0.13,<0.15"
 h11 = ">=0.13,<0.15"
-sniffio = "==1.*"
+sniffio = ">=1.0.0,<2.0.0"
 
 
 [package.extras]
 [package.extras]
 http2 = ["h2 (>=3,<5)"]
 http2 = ["h2 (>=3,<5)"]
-socks = ["socksio (==1.*)"]
+socks = ["socksio (>=1.0.0,<2.0.0)"]
 
 
 [[package]]
 [[package]]
 name = "httpx"
 name = "httpx"
 version = "0.24.1"
 version = "0.24.1"
 description = "The next generation HTTP client."
 description = "The next generation HTTP client."
+category = "main"
 optional = false
 optional = false
 python-versions = ">=3.7"
 python-versions = ">=3.7"
 files = [
 files = [
@@ -547,14 +572,15 @@ sniffio = "*"
 
 
 [package.extras]
 [package.extras]
 brotli = ["brotli", "brotlicffi"]
 brotli = ["brotli", "brotlicffi"]
-cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"]
+cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<14)"]
 http2 = ["h2 (>=3,<5)"]
 http2 = ["h2 (>=3,<5)"]
-socks = ["socksio (==1.*)"]
+socks = ["socksio (>=1.0.0,<2.0.0)"]
 
 
 [[package]]
 [[package]]
 name = "identify"
 name = "identify"
 version = "2.5.31"
 version = "2.5.31"
 description = "File identification library for Python"
 description = "File identification library for Python"
+category = "dev"
 optional = false
 optional = false
 python-versions = ">=3.8"
 python-versions = ">=3.8"
 files = [
 files = [
@@ -569,6 +595,7 @@ license = ["ukkonen"]
 name = "idna"
 name = "idna"
 version = "3.4"
 version = "3.4"
 description = "Internationalized Domain Names in Applications (IDNA)"
 description = "Internationalized Domain Names in Applications (IDNA)"
+category = "main"
 optional = false
 optional = false
 python-versions = ">=3.5"
 python-versions = ">=3.5"
 files = [
 files = [
@@ -580,6 +607,7 @@ files = [
 name = "importlib-metadata"
 name = "importlib-metadata"
 version = "6.8.0"
 version = "6.8.0"
 description = "Read metadata from Python packages"
 description = "Read metadata from Python packages"
+category = "main"
 optional = false
 optional = false
 python-versions = ">=3.8"
 python-versions = ">=3.8"
 files = [
 files = [
@@ -599,6 +627,7 @@ testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs
 name = "importlib-resources"
 name = "importlib-resources"
 version = "6.1.0"
 version = "6.1.0"
 description = "Read resources from Python packages"
 description = "Read resources from Python packages"
+category = "main"
 optional = false
 optional = false
 python-versions = ">=3.8"
 python-versions = ">=3.8"
 files = [
 files = [
@@ -617,6 +646,7 @@ testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)",
 name = "iniconfig"
 name = "iniconfig"
 version = "2.0.0"
 version = "2.0.0"
 description = "brain-dead simple config-ini parsing"
 description = "brain-dead simple config-ini parsing"
+category = "dev"
 optional = false
 optional = false
 python-versions = ">=3.7"
 python-versions = ">=3.7"
 files = [
 files = [
@@ -628,6 +658,7 @@ files = [
 name = "jinja2"
 name = "jinja2"
 version = "3.1.2"
 version = "3.1.2"
 description = "A very fast and expressive template engine."
 description = "A very fast and expressive template engine."
+category = "main"
 optional = false
 optional = false
 python-versions = ">=3.7"
 python-versions = ">=3.7"
 files = [
 files = [
@@ -645,6 +676,7 @@ i18n = ["Babel (>=2.7)"]
 name = "mako"
 name = "mako"
 version = "1.2.4"
 version = "1.2.4"
 description = "A super-fast templating language that borrows the best ideas from the existing templating languages."
 description = "A super-fast templating language that borrows the best ideas from the existing templating languages."
+category = "main"
 optional = false
 optional = false
 python-versions = ">=3.7"
 python-versions = ">=3.7"
 files = [
 files = [
@@ -664,6 +696,7 @@ testing = ["pytest"]
 name = "markdown-it-py"
 name = "markdown-it-py"
 version = "3.0.0"
 version = "3.0.0"
 description = "Python port of markdown-it. Markdown parsing, done right!"
 description = "Python port of markdown-it. Markdown parsing, done right!"
+category = "main"
 optional = false
 optional = false
 python-versions = ">=3.8"
 python-versions = ">=3.8"
 files = [
 files = [
@@ -688,6 +721,7 @@ testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"]
 name = "markupsafe"
 name = "markupsafe"
 version = "2.1.3"
 version = "2.1.3"
 description = "Safely add untrusted strings to HTML/XML markup."
 description = "Safely add untrusted strings to HTML/XML markup."
+category = "main"
 optional = false
 optional = false
 python-versions = ">=3.7"
 python-versions = ">=3.7"
 files = [
 files = [
@@ -747,6 +781,7 @@ files = [
 name = "mdurl"
 name = "mdurl"
 version = "0.1.2"
 version = "0.1.2"
 description = "Markdown URL utilities"
 description = "Markdown URL utilities"
+category = "main"
 optional = false
 optional = false
 python-versions = ">=3.7"
 python-versions = ">=3.7"
 files = [
 files = [
@@ -758,6 +793,7 @@ files = [
 name = "mypy-extensions"
 name = "mypy-extensions"
 version = "1.0.0"
 version = "1.0.0"
 description = "Type system extensions for programs checked with the mypy type checker."
 description = "Type system extensions for programs checked with the mypy type checker."
+category = "dev"
 optional = false
 optional = false
 python-versions = ">=3.5"
 python-versions = ">=3.5"
 files = [
 files = [
@@ -769,6 +805,7 @@ files = [
 name = "nodeenv"
 name = "nodeenv"
 version = "1.8.0"
 version = "1.8.0"
 description = "Node.js virtual environment builder"
 description = "Node.js virtual environment builder"
+category = "dev"
 optional = false
 optional = false
 python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*"
 python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*"
 files = [
 files = [
@@ -783,6 +820,7 @@ setuptools = "*"
 name = "numpy"
 name = "numpy"
 version = "1.24.4"
 version = "1.24.4"
 description = "Fundamental package for array computing in Python"
 description = "Fundamental package for array computing in Python"
+category = "dev"
 optional = false
 optional = false
 python-versions = ">=3.8"
 python-versions = ">=3.8"
 files = [
 files = [
@@ -820,6 +858,7 @@ files = [
 name = "numpy"
 name = "numpy"
 version = "1.26.1"
 version = "1.26.1"
 description = "Fundamental package for array computing in Python"
 description = "Fundamental package for array computing in Python"
+category = "dev"
 optional = false
 optional = false
 python-versions = "<3.13,>=3.9"
 python-versions = "<3.13,>=3.9"
 files = [
 files = [
@@ -861,6 +900,7 @@ files = [
 name = "outcome"
 name = "outcome"
 version = "1.3.0.post0"
 version = "1.3.0.post0"
 description = "Capture the outcome of Python function calls."
 description = "Capture the outcome of Python function calls."
+category = "dev"
 optional = false
 optional = false
 python-versions = ">=3.7"
 python-versions = ">=3.7"
 files = [
 files = [
@@ -875,6 +915,7 @@ attrs = ">=19.2.0"
 name = "packaging"
 name = "packaging"
 version = "23.2"
 version = "23.2"
 description = "Core utilities for Python packages"
 description = "Core utilities for Python packages"
+category = "main"
 optional = false
 optional = false
 python-versions = ">=3.7"
 python-versions = ">=3.7"
 files = [
 files = [
@@ -886,6 +927,7 @@ files = [
 name = "pandas"
 name = "pandas"
 version = "1.5.3"
 version = "1.5.3"
 description = "Powerful data structures for data analysis, time series, and statistics"
 description = "Powerful data structures for data analysis, time series, and statistics"
+category = "dev"
 optional = false
 optional = false
 python-versions = ">=3.8"
 python-versions = ">=3.8"
 files = [
 files = [
@@ -930,6 +972,7 @@ test = ["hypothesis (>=5.5.3)", "pytest (>=6.0)", "pytest-xdist (>=1.31)"]
 name = "pandas"
 name = "pandas"
 version = "2.1.2"
 version = "2.1.2"
 description = "Powerful data structures for data analysis, time series, and statistics"
 description = "Powerful data structures for data analysis, time series, and statistics"
+category = "dev"
 optional = false
 optional = false
 python-versions = ">=3.9"
 python-versions = ">=3.9"
 files = [
 files = [
@@ -998,6 +1041,7 @@ xml = ["lxml (>=4.8.0)"]
 name = "pathspec"
 name = "pathspec"
 version = "0.11.2"
 version = "0.11.2"
 description = "Utility library for gitignore style pattern matching of file paths."
 description = "Utility library for gitignore style pattern matching of file paths."
+category = "dev"
 optional = false
 optional = false
 python-versions = ">=3.7"
 python-versions = ">=3.7"
 files = [
 files = [
@@ -1009,6 +1053,7 @@ files = [
 name = "pillow"
 name = "pillow"
 version = "10.1.0"
 version = "10.1.0"
 description = "Python Imaging Library (Fork)"
 description = "Python Imaging Library (Fork)"
+category = "dev"
 optional = false
 optional = false
 python-versions = ">=3.8"
 python-versions = ">=3.8"
 files = [
 files = [
@@ -1076,6 +1121,7 @@ tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "pa
 name = "pipdeptree"
 name = "pipdeptree"
 version = "2.13.0"
 version = "2.13.0"
 description = "Command line utility to show dependency tree of packages."
 description = "Command line utility to show dependency tree of packages."
+category = "main"
 optional = false
 optional = false
 python-versions = ">=3.8"
 python-versions = ">=3.8"
 files = [
 files = [
@@ -1091,6 +1137,7 @@ test = ["covdefaults (>=2.3)", "diff-cover (>=7.7)", "pip (>=23.2)", "pytest (>=
 name = "platformdirs"
 name = "platformdirs"
 version = "3.11.0"
 version = "3.11.0"
 description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
 description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
+category = "main"
 optional = false
 optional = false
 python-versions = ">=3.7"
 python-versions = ">=3.7"
 files = [
 files = [
@@ -1106,6 +1153,7 @@ test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-co
 name = "plotly"
 name = "plotly"
 version = "5.18.0"
 version = "5.18.0"
 description = "An open-source, interactive data visualization library for Python"
 description = "An open-source, interactive data visualization library for Python"
+category = "dev"
 optional = false
 optional = false
 python-versions = ">=3.6"
 python-versions = ">=3.6"
 files = [
 files = [
@@ -1121,6 +1169,7 @@ tenacity = ">=6.2.0"
 name = "pluggy"
 name = "pluggy"
 version = "1.3.0"
 version = "1.3.0"
 description = "plugin and hook calling mechanisms for python"
 description = "plugin and hook calling mechanisms for python"
+category = "dev"
 optional = false
 optional = false
 python-versions = ">=3.8"
 python-versions = ">=3.8"
 files = [
 files = [
@@ -1136,6 +1185,7 @@ testing = ["pytest", "pytest-benchmark"]
 name = "pre-commit"
 name = "pre-commit"
 version = "3.5.0"
 version = "3.5.0"
 description = "A framework for managing and maintaining multi-language pre-commit hooks."
 description = "A framework for managing and maintaining multi-language pre-commit hooks."
+category = "dev"
 optional = false
 optional = false
 python-versions = ">=3.8"
 python-versions = ">=3.8"
 files = [
 files = [
@@ -1154,6 +1204,7 @@ virtualenv = ">=20.10.0"
 name = "psutil"
 name = "psutil"
 version = "5.9.6"
 version = "5.9.6"
 description = "Cross-platform lib for process and system monitoring in Python."
 description = "Cross-platform lib for process and system monitoring in Python."
+category = "main"
 optional = false
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
 files = [
 files = [
@@ -1178,10 +1229,23 @@ files = [
 [package.extras]
 [package.extras]
 test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"]
 test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"]
 
 
+[[package]]
+name = "py-cpuinfo"
+version = "9.0.0"
+description = "Get CPU info with pure Python"
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+    {file = "py-cpuinfo-9.0.0.tar.gz", hash = "sha256:3cdbbf3fac90dc6f118bfd64384f309edeadd902d7c8fb17f02ffa1fc3f49690"},
+    {file = "py_cpuinfo-9.0.0-py3-none-any.whl", hash = "sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5"},
+]
+
 [[package]]
 [[package]]
 name = "pycparser"
 name = "pycparser"
 version = "2.21"
 version = "2.21"
 description = "C parser in Python"
 description = "C parser in Python"
+category = "dev"
 optional = false
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
 files = [
 files = [
@@ -1193,6 +1257,7 @@ files = [
 name = "pydantic"
 name = "pydantic"
 version = "1.10.13"
 version = "1.10.13"
 description = "Data validation and settings management using python type hints"
 description = "Data validation and settings management using python type hints"
+category = "main"
 optional = false
 optional = false
 python-versions = ">=3.7"
 python-versions = ">=3.7"
 files = [
 files = [
@@ -1245,6 +1310,7 @@ email = ["email-validator (>=1.0.3)"]
 name = "pygments"
 name = "pygments"
 version = "2.16.1"
 version = "2.16.1"
 description = "Pygments is a syntax highlighting package written in Python."
 description = "Pygments is a syntax highlighting package written in Python."
+category = "main"
 optional = false
 optional = false
 python-versions = ">=3.7"
 python-versions = ">=3.7"
 files = [
 files = [
@@ -1259,6 +1325,7 @@ plugins = ["importlib-metadata"]
 name = "pyright"
 name = "pyright"
 version = "1.1.334"
 version = "1.1.334"
 description = "Command line wrapper for pyright"
 description = "Command line wrapper for pyright"
+category = "dev"
 optional = false
 optional = false
 python-versions = ">=3.7"
 python-versions = ">=3.7"
 files = [
 files = [
@@ -1277,6 +1344,7 @@ dev = ["twine (>=3.4.1)"]
 name = "pysocks"
 name = "pysocks"
 version = "1.7.1"
 version = "1.7.1"
 description = "A Python SOCKS client module. See https://github.com/Anorov/PySocks for more information."
 description = "A Python SOCKS client module. See https://github.com/Anorov/PySocks for more information."
+category = "dev"
 optional = false
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
 files = [
 files = [
@@ -1289,6 +1357,7 @@ files = [
 name = "pytest"
 name = "pytest"
 version = "7.4.3"
 version = "7.4.3"
 description = "pytest: simple powerful testing with Python"
 description = "pytest: simple powerful testing with Python"
+category = "dev"
 optional = false
 optional = false
 python-versions = ">=3.7"
 python-versions = ">=3.7"
 files = [
 files = [
@@ -1311,6 +1380,7 @@ testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "no
 name = "pytest-asyncio"
 name = "pytest-asyncio"
 version = "0.20.3"
 version = "0.20.3"
 description = "Pytest support for asyncio"
 description = "Pytest support for asyncio"
+category = "dev"
 optional = false
 optional = false
 python-versions = ">=3.7"
 python-versions = ">=3.7"
 files = [
 files = [
@@ -1325,10 +1395,32 @@ pytest = ">=6.1.0"
 docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"]
 docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"]
 testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"]
 testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"]
 
 
+[[package]]
+name = "pytest-benchmark"
+version = "4.0.0"
+description = "A ``pytest`` fixture for benchmarking code. It will group the tests into rounds that are calibrated to the chosen timer."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "pytest-benchmark-4.0.0.tar.gz", hash = "sha256:fb0785b83efe599a6a956361c0691ae1dbb5318018561af10f3e915caa0048d1"},
+    {file = "pytest_benchmark-4.0.0-py3-none-any.whl", hash = "sha256:fdb7db64e31c8b277dff9850d2a2556d8b60bcb0ea6524e36e28ffd7c87f71d6"},
+]
+
+[package.dependencies]
+py-cpuinfo = "*"
+pytest = ">=3.8"
+
+[package.extras]
+aspect = ["aspectlib"]
+elasticsearch = ["elasticsearch"]
+histogram = ["pygal", "pygaljs"]
+
 [[package]]
 [[package]]
 name = "pytest-cov"
 name = "pytest-cov"
 version = "4.1.0"
 version = "4.1.0"
 description = "Pytest plugin for measuring coverage."
 description = "Pytest plugin for measuring coverage."
+category = "dev"
 optional = false
 optional = false
 python-versions = ">=3.7"
 python-versions = ">=3.7"
 files = [
 files = [
@@ -1347,6 +1439,7 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtuale
 name = "pytest-mock"
 name = "pytest-mock"
 version = "3.12.0"
 version = "3.12.0"
 description = "Thin-wrapper around the mock package for easier use with pytest"
 description = "Thin-wrapper around the mock package for easier use with pytest"
+category = "dev"
 optional = false
 optional = false
 python-versions = ">=3.8"
 python-versions = ">=3.8"
 files = [
 files = [
@@ -1364,6 +1457,7 @@ dev = ["pre-commit", "pytest-asyncio", "tox"]
 name = "python-dateutil"
 name = "python-dateutil"
 version = "2.8.2"
 version = "2.8.2"
 description = "Extensions to the standard Python datetime module"
 description = "Extensions to the standard Python datetime module"
+category = "dev"
 optional = false
 optional = false
 python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
 python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
 files = [
 files = [
@@ -1378,6 +1472,7 @@ six = ">=1.5"
 name = "python-engineio"
 name = "python-engineio"
 version = "4.8.0"
 version = "4.8.0"
 description = "Engine.IO server and client for Python"
 description = "Engine.IO server and client for Python"
+category = "main"
 optional = false
 optional = false
 python-versions = ">=3.6"
 python-versions = ">=3.6"
 files = [
 files = [
@@ -1397,6 +1492,7 @@ docs = ["sphinx"]
 name = "python-multipart"
 name = "python-multipart"
 version = "0.0.5"
 version = "0.0.5"
 description = "A streaming multipart parser for Python"
 description = "A streaming multipart parser for Python"
+category = "main"
 optional = false
 optional = false
 python-versions = "*"
 python-versions = "*"
 files = [
 files = [
@@ -1410,6 +1506,7 @@ six = ">=1.4.0"
 name = "python-socketio"
 name = "python-socketio"
 version = "5.10.0"
 version = "5.10.0"
 description = "Socket.IO server and client for Python"
 description = "Socket.IO server and client for Python"
+category = "main"
 optional = false
 optional = false
 python-versions = ">=3.6"
 python-versions = ">=3.6"
 files = [
 files = [
@@ -1430,6 +1527,7 @@ docs = ["sphinx"]
 name = "pytz"
 name = "pytz"
 version = "2023.3.post1"
 version = "2023.3.post1"
 description = "World timezone definitions, modern and historical"
 description = "World timezone definitions, modern and historical"
+category = "dev"
 optional = false
 optional = false
 python-versions = "*"
 python-versions = "*"
 files = [
 files = [
@@ -1441,6 +1539,7 @@ files = [
 name = "pyyaml"
 name = "pyyaml"
 version = "6.0.1"
 version = "6.0.1"
 description = "YAML parser and emitter for Python"
 description = "YAML parser and emitter for Python"
+category = "dev"
 optional = false
 optional = false
 python-versions = ">=3.6"
 python-versions = ">=3.6"
 files = [
 files = [
@@ -1449,6 +1548,7 @@ files = [
     {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
     {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
     {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"},
     {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"},
     {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"},
     {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"},
+    {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"},
     {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
     {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
     {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
     {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
     {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
     {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
@@ -1456,8 +1556,15 @@ files = [
     {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"},
     {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"},
     {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"},
     {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"},
     {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"},
     {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"},
+    {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"},
     {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
     {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
     {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
     {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
+    {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
+    {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
+    {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
+    {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
+    {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
+    {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"},
     {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"},
     {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"},
     {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"},
     {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"},
     {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"},
     {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"},
@@ -1474,6 +1581,7 @@ files = [
     {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"},
     {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"},
     {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"},
     {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"},
     {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"},
     {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"},
+    {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"},
     {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"},
     {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"},
     {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"},
     {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"},
     {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"},
     {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"},
@@ -1481,6 +1589,7 @@ files = [
     {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"},
     {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"},
     {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"},
     {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"},
     {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"},
     {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"},
+    {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"},
     {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"},
     {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"},
     {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
     {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
     {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
     {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
@@ -1490,6 +1599,7 @@ files = [
 name = "redis"
 name = "redis"
 version = "4.6.0"
 version = "4.6.0"
 description = "Python client for Redis database and key-value store"
 description = "Python client for Redis database and key-value store"
+category = "main"
 optional = false
 optional = false
 python-versions = ">=3.7"
 python-versions = ">=3.7"
 files = [
 files = [
@@ -1508,6 +1618,7 @@ ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)"
 name = "rich"
 name = "rich"
 version = "13.6.0"
 version = "13.6.0"
 description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
 description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
+category = "main"
 optional = false
 optional = false
 python-versions = ">=3.7.0"
 python-versions = ">=3.7.0"
 files = [
 files = [
@@ -1527,6 +1638,7 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"]
 name = "ruff"
 name = "ruff"
 version = "0.0.244"
 version = "0.0.244"
 description = "An extremely fast Python linter, written in Rust."
 description = "An extremely fast Python linter, written in Rust."
+category = "dev"
 optional = false
 optional = false
 python-versions = ">=3.7"
 python-versions = ">=3.7"
 files = [
 files = [
@@ -1552,6 +1664,7 @@ files = [
 name = "selenium"
 name = "selenium"
 version = "4.15.2"
 version = "4.15.2"
 description = ""
 description = ""
+category = "dev"
 optional = false
 optional = false
 python-versions = ">=3.8"
 python-versions = ">=3.8"
 files = [
 files = [
@@ -1569,6 +1682,7 @@ urllib3 = {version = ">=1.26,<3", extras = ["socks"]}
 name = "setuptools"
 name = "setuptools"
 version = "68.2.2"
 version = "68.2.2"
 description = "Easily download, build, install, upgrade, and uninstall Python packages"
 description = "Easily download, build, install, upgrade, and uninstall Python packages"
+category = "main"
 optional = false
 optional = false
 python-versions = ">=3.8"
 python-versions = ">=3.8"
 files = [
 files = [
@@ -1585,6 +1699,7 @@ testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jar
 name = "simple-websocket"
 name = "simple-websocket"
 version = "1.0.0"
 version = "1.0.0"
 description = "Simple WebSocket server and client for Python"
 description = "Simple WebSocket server and client for Python"
+category = "main"
 optional = false
 optional = false
 python-versions = ">=3.6"
 python-versions = ">=3.6"
 files = [
 files = [
@@ -1602,6 +1717,7 @@ docs = ["sphinx"]
 name = "six"
 name = "six"
 version = "1.16.0"
 version = "1.16.0"
 description = "Python 2 and 3 compatibility utilities"
 description = "Python 2 and 3 compatibility utilities"
+category = "main"
 optional = false
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
 files = [
 files = [
@@ -1613,6 +1729,7 @@ files = [
 name = "sniffio"
 name = "sniffio"
 version = "1.3.0"
 version = "1.3.0"
 description = "Sniff out which async library your code is running under"
 description = "Sniff out which async library your code is running under"
+category = "main"
 optional = false
 optional = false
 python-versions = ">=3.7"
 python-versions = ">=3.7"
 files = [
 files = [
@@ -1624,6 +1741,7 @@ files = [
 name = "sortedcontainers"
 name = "sortedcontainers"
 version = "2.4.0"
 version = "2.4.0"
 description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set"
 description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set"
+category = "dev"
 optional = false
 optional = false
 python-versions = "*"
 python-versions = "*"
 files = [
 files = [
@@ -1635,6 +1753,7 @@ files = [
 name = "sqlalchemy"
 name = "sqlalchemy"
 version = "1.4.41"
 version = "1.4.41"
 description = "Database Abstraction Library"
 description = "Database Abstraction Library"
+category = "main"
 optional = false
 optional = false
 python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7"
 python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7"
 files = [
 files = [
@@ -1709,6 +1828,7 @@ sqlcipher = ["sqlcipher3-binary"]
 name = "sqlalchemy2-stubs"
 name = "sqlalchemy2-stubs"
 version = "0.0.2a36"
 version = "0.0.2a36"
 description = "Typing Stubs for SQLAlchemy 1.4"
 description = "Typing Stubs for SQLAlchemy 1.4"
+category = "main"
 optional = false
 optional = false
 python-versions = ">=3.6"
 python-versions = ">=3.6"
 files = [
 files = [
@@ -1723,6 +1843,7 @@ typing-extensions = ">=3.7.4"
 name = "sqlmodel"
 name = "sqlmodel"
 version = "0.0.8"
 version = "0.0.8"
 description = "SQLModel, SQL databases in Python, designed for simplicity, compatibility, and robustness."
 description = "SQLModel, SQL databases in Python, designed for simplicity, compatibility, and robustness."
+category = "main"
 optional = false
 optional = false
 python-versions = ">=3.6.1,<4.0.0"
 python-versions = ">=3.6.1,<4.0.0"
 files = [
 files = [
@@ -1739,6 +1860,7 @@ sqlalchemy2-stubs = "*"
 name = "starlette"
 name = "starlette"
 version = "0.27.0"
 version = "0.27.0"
 description = "The little ASGI library that shines."
 description = "The little ASGI library that shines."
+category = "main"
 optional = false
 optional = false
 python-versions = ">=3.7"
 python-versions = ">=3.7"
 files = [
 files = [
@@ -1757,6 +1879,7 @@ full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyam
 name = "starlette-admin"
 name = "starlette-admin"
 version = "0.9.0"
 version = "0.9.0"
 description = "Fast, beautiful and extensible administrative interface framework for Starlette/FastApi applications"
 description = "Fast, beautiful and extensible administrative interface framework for Starlette/FastApi applications"
+category = "main"
 optional = false
 optional = false
 python-versions = ">=3.7"
 python-versions = ">=3.7"
 files = [
 files = [
@@ -1779,6 +1902,7 @@ test = ["aiomysql (>=0.1.1,<0.2.0)", "aiosqlite (>=0.17.0,<0.20.0)", "arrow (>=1
 name = "tabulate"
 name = "tabulate"
 version = "0.9.0"
 version = "0.9.0"
 description = "Pretty-print tabular data"
 description = "Pretty-print tabular data"
+category = "main"
 optional = false
 optional = false
 python-versions = ">=3.7"
 python-versions = ">=3.7"
 files = [
 files = [
@@ -1793,6 +1917,7 @@ widechars = ["wcwidth"]
 name = "tenacity"
 name = "tenacity"
 version = "8.2.3"
 version = "8.2.3"
 description = "Retry code until it succeeds"
 description = "Retry code until it succeeds"
+category = "dev"
 optional = false
 optional = false
 python-versions = ">=3.7"
 python-versions = ">=3.7"
 files = [
 files = [
@@ -1807,6 +1932,7 @@ doc = ["reno", "sphinx", "tornado (>=4.5)"]
 name = "toml"
 name = "toml"
 version = "0.10.2"
 version = "0.10.2"
 description = "Python Library for Tom's Obvious, Minimal Language"
 description = "Python Library for Tom's Obvious, Minimal Language"
+category = "dev"
 optional = false
 optional = false
 python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
 python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
 files = [
 files = [
@@ -1818,6 +1944,7 @@ files = [
 name = "tomli"
 name = "tomli"
 version = "2.0.1"
 version = "2.0.1"
 description = "A lil' TOML parser"
 description = "A lil' TOML parser"
+category = "dev"
 optional = false
 optional = false
 python-versions = ">=3.7"
 python-versions = ">=3.7"
 files = [
 files = [
@@ -1829,6 +1956,7 @@ files = [
 name = "trio"
 name = "trio"
 version = "0.23.1"
 version = "0.23.1"
 description = "A friendly Python library for async concurrency and I/O"
 description = "A friendly Python library for async concurrency and I/O"
+category = "dev"
 optional = false
 optional = false
 python-versions = ">=3.8"
 python-versions = ">=3.8"
 files = [
 files = [
@@ -1849,6 +1977,7 @@ sortedcontainers = "*"
 name = "trio-websocket"
 name = "trio-websocket"
 version = "0.11.1"
 version = "0.11.1"
 description = "WebSocket library for Trio"
 description = "WebSocket library for Trio"
+category = "dev"
 optional = false
 optional = false
 python-versions = ">=3.7"
 python-versions = ">=3.7"
 files = [
 files = [
@@ -1865,6 +1994,7 @@ wsproto = ">=0.14"
 name = "typer"
 name = "typer"
 version = "0.9.0"
 version = "0.9.0"
 description = "Typer, build great CLIs. Easy to code. Based on Python type hints."
 description = "Typer, build great CLIs. Easy to code. Based on Python type hints."
+category = "main"
 optional = false
 optional = false
 python-versions = ">=3.6"
 python-versions = ">=3.6"
 files = [
 files = [
@@ -1886,6 +2016,7 @@ test = ["black (>=22.3.0,<23.0.0)", "coverage (>=6.2,<7.0)", "isort (>=5.0.6,<6.
 name = "types-tabulate"
 name = "types-tabulate"
 version = "0.9.0.3"
 version = "0.9.0.3"
 description = "Typing stubs for tabulate"
 description = "Typing stubs for tabulate"
+category = "dev"
 optional = false
 optional = false
 python-versions = "*"
 python-versions = "*"
 files = [
 files = [
@@ -1897,6 +2028,7 @@ files = [
 name = "typing-extensions"
 name = "typing-extensions"
 version = "4.8.0"
 version = "4.8.0"
 description = "Backported and Experimental Type Hints for Python 3.8+"
 description = "Backported and Experimental Type Hints for Python 3.8+"
+category = "main"
 optional = false
 optional = false
 python-versions = ">=3.8"
 python-versions = ">=3.8"
 files = [
 files = [
@@ -1908,6 +2040,7 @@ files = [
 name = "tzdata"
 name = "tzdata"
 version = "2023.3"
 version = "2023.3"
 description = "Provider of IANA time zone data"
 description = "Provider of IANA time zone data"
+category = "dev"
 optional = false
 optional = false
 python-versions = ">=2"
 python-versions = ">=2"
 files = [
 files = [
@@ -1919,6 +2052,7 @@ files = [
 name = "urllib3"
 name = "urllib3"
 version = "2.0.7"
 version = "2.0.7"
 description = "HTTP library with thread-safe connection pooling, file post, and more."
 description = "HTTP library with thread-safe connection pooling, file post, and more."
+category = "dev"
 optional = false
 optional = false
 python-versions = ">=3.7"
 python-versions = ">=3.7"
 files = [
 files = [
@@ -1939,6 +2073,7 @@ zstd = ["zstandard (>=0.18.0)"]
 name = "uvicorn"
 name = "uvicorn"
 version = "0.20.0"
 version = "0.20.0"
 description = "The lightning-fast ASGI server."
 description = "The lightning-fast ASGI server."
+category = "main"
 optional = false
 optional = false
 python-versions = ">=3.7"
 python-versions = ">=3.7"
 files = [
 files = [
@@ -1957,6 +2092,7 @@ standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)",
 name = "uvicorn"
 name = "uvicorn"
 version = "0.24.0"
 version = "0.24.0"
 description = "The lightning-fast ASGI server."
 description = "The lightning-fast ASGI server."
+category = "main"
 optional = false
 optional = false
 python-versions = ">=3.8"
 python-versions = ">=3.8"
 files = [
 files = [
@@ -1975,6 +2111,7 @@ standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)",
 name = "virtualenv"
 name = "virtualenv"
 version = "20.24.6"
 version = "20.24.6"
 description = "Virtual Python Environment builder"
 description = "Virtual Python Environment builder"
+category = "dev"
 optional = false
 optional = false
 python-versions = ">=3.7"
 python-versions = ">=3.7"
 files = [
 files = [
@@ -1995,6 +2132,7 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess
 name = "watchdog"
 name = "watchdog"
 version = "2.3.1"
 version = "2.3.1"
 description = "Filesystem events monitoring"
 description = "Filesystem events monitoring"
+category = "main"
 optional = false
 optional = false
 python-versions = ">=3.6"
 python-versions = ">=3.6"
 files = [
 files = [
@@ -2035,6 +2173,7 @@ watchmedo = ["PyYAML (>=3.10)"]
 name = "watchfiles"
 name = "watchfiles"
 version = "0.19.0"
 version = "0.19.0"
 description = "Simple, modern and high performance file watching and code reload in python."
 description = "Simple, modern and high performance file watching and code reload in python."
+category = "main"
 optional = false
 optional = false
 python-versions = ">=3.7"
 python-versions = ">=3.7"
 files = [
 files = [
@@ -2069,6 +2208,7 @@ anyio = ">=3.0.0"
 name = "websockets"
 name = "websockets"
 version = "10.4"
 version = "10.4"
 description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)"
 description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)"
+category = "main"
 optional = false
 optional = false
 python-versions = ">=3.7"
 python-versions = ">=3.7"
 files = [
 files = [
@@ -2147,6 +2287,7 @@ files = [
 name = "wrapt"
 name = "wrapt"
 version = "1.15.0"
 version = "1.15.0"
 description = "Module for decorators, wrappers and monkey patching."
 description = "Module for decorators, wrappers and monkey patching."
+category = "main"
 optional = false
 optional = false
 python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
 python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
 files = [
 files = [
@@ -2231,6 +2372,7 @@ files = [
 name = "wsproto"
 name = "wsproto"
 version = "1.2.0"
 version = "1.2.0"
 description = "WebSockets state-machine based protocol implementation"
 description = "WebSockets state-machine based protocol implementation"
+category = "main"
 optional = false
 optional = false
 python-versions = ">=3.7.0"
 python-versions = ">=3.7.0"
 files = [
 files = [
@@ -2245,6 +2387,7 @@ h11 = ">=0.9.0,<1"
 name = "zipp"
 name = "zipp"
 version = "3.17.0"
 version = "3.17.0"
 description = "Backport of pathlib-compatible object wrapper for zip files"
 description = "Backport of pathlib-compatible object wrapper for zip files"
+category = "main"
 optional = false
 optional = false
 python-versions = ">=3.8"
 python-versions = ">=3.8"
 files = [
 files = [
@@ -2259,4 +2402,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p
 [metadata]
 [metadata]
 lock-version = "2.0"
 lock-version = "2.0"
 python-versions = "^3.8"
 python-versions = "^3.8"
-content-hash = "5b1490dc4da1ddebdf65472535949c8d51fb81f10e26861ea35bbddb1afca004"
+content-hash = "d5765fcff519e4d18744355420f7ec03a9821c99ed96421c016276ac0bb938e0"

+ 1 - 0
pyproject.toml

@@ -82,6 +82,7 @@ asynctest = "^0.13.0"
 pre-commit = {version = "^3.2.1", python = ">=3.8,<4.0"}
 pre-commit = {version = "^3.2.1", python = ">=3.8,<4.0"}
 selenium = "^4.11.0"
 selenium = "^4.11.0"
 types-tabulate = "^0.9.0.3"
 types-tabulate = "^0.9.0.3"
+pytest-benchmark = "^4.0.0"
 
 
 [tool.poetry.scripts]
 [tool.poetry.scripts]
 reflex = "reflex.reflex:cli"
 reflex = "reflex.reflex:cli"

+ 77 - 0
scripts/benchmarks.sh

@@ -0,0 +1,77 @@
+#!/bin/bash
+
+# Change directory to the first argument passed to the script
+project_dir=$1
+shift
+pushd "$project_dir" || exit 1
+echo "Changed directory to $project_dir"
+
+
+# So we get stdout / stderr from Python ASAP. Without this, delays can be very long (e.g. on Windows, Github Actions)
+export PYTHONUNBUFFERED=1
+
+env_mode=$1
+shift
+check_ports=${1:-3000 8000}
+shift
+
+# Start the server in the background
+export TELEMETRY_ENABLED=false
+reflex run --env "$env_mode" "$@" & pid=$!
+
+# Within the context of this bash, $pid_in_bash is what we need to pass to "kill" on exit
+# This is true on all platforms.
+pid_in_bash=$pid
+trap "kill -INT $pid_in_bash ||:" EXIT
+
+echo "Started server with PID $pid"
+
+# Assume we run from the root of the repo
+popd
+
+# In Windows, our Python script below needs to work with the WINPID
+if [ -f /proc/$pid/winpid ]; then
+  pid=$(cat /proc/$pid/winpid)
+  echo "Windows detected, passing winpid $pid to port waiter"
+fi
+
+python scripts/wait_for_listening_port.py $check_ports --timeout=600 --server-pid "$pid"
+
+
+# Check if something is running on port 3000
+if curl --output /dev/null --silent --head --fail "http://localhost:3000"; then
+  echo "URL exists: http://localhost:3000"
+else
+  echo "URL does not exist: https://localhost:3000"
+fi
+
+mkdir -p ./tests/benchmarks/.lighthouseci
+
+# Create a lighthouserc.js file
+cat << EOF > lighthouserc.js
+module.exports = {
+  ci: {
+    collect: {
+     isSinglePageApplication: true,
+     numberOfRuns: 1,
+     url: ['http://localhost:3000', "http://localhost:3000/docs/getting-started/introduction/", "http://localhost:3000/blog/2023-08-02-seed-annoucement/"]
+    },
+    upload: {
+      target: 'filesystem',
+      "outputDir": "./integration/benchmarks/.lighthouseci"
+    },
+  },
+};
+EOF
+
+# Install and Run LHCI
+npm install -g @lhci/cli
+lhci autorun
+
+# Check to see if the LHCI report is generated
+if [ -d "./integration/benchmarks/.lighthouseci" ] && [ "$(ls -A ./integration/benchmarks/.lighthouseci)" ]; then
+  echo "LHCI report generated"
+else
+  echo "LHCI report not generated"
+  exit 1 # Exits the script with a status of 1, which will cause the GitHub Action to stop
+fi

+ 0 - 123
scripts/lighthouse.sh

@@ -1,123 +0,0 @@
-#!/bin/bash
-
-# Change directory to the first argument passed to the script
-project_dir=$1
-shift
-pushd "$project_dir" || exit 1
-echo "Changed directory to $project_dir"
-
-
-# So we get stdout / stderr from Python ASAP. Without this, delays can be very long (e.g. on Windows, Github Actions)
-export PYTHONUNBUFFERED=1
-
-env_mode=$1
-shift
-check_ports=${1:-3000 8000}
-shift
-
-# Start the server in the background
-export TELEMETRY_ENABLED=false
-reflex run --loglevel debug --env "$env_mode" "$@" & pid=$!
-
-# Within the context of this bash, $pid_in_bash is what we need to pass to "kill" on exit
-# This is true on all platforms.
-pid_in_bash=$pid
-trap "kill -INT $pid_in_bash ||:" EXIT
-
-echo "Started server with PID $pid"
-
-# Assume we run from the root of the repo
-popd
-
-# In Windows, our Python script below needs to work with the WINPID
-if [ -f /proc/$pid/winpid ]; then
-  pid=$(cat /proc/$pid/winpid)
-  echo "Windows detected, passing winpid $pid to port waiter"
-fi
-
-python scripts/wait_for_listening_port.py $check_ports --timeout=600 --server-pid "$pid"
-
-
-# Check if something is running on port 3000
-if curl --output /dev/null --silent --head --fail "http://localhost:3000"; then
-  echo "URL exists: http://localhost:3000"
-else
-  echo "URL does not exist: https://localhost:3000"
-fi
-
-# Change to .web directory
-project_dir=$1
-shift
-pushd "$project_dir" || exit 1
-echo "Changed directory to $project_dir"
-cd .web
-
-# Create a lighthouserc.js file
-cat << EOF > lighthouserc.js
-module.exports = {
-  ci: {
-    collect: {
-     isSinglePageApplication: true,
-     numberOfRuns: 1,
-     url: ['http://localhost:3000', "http://localhost:3000/docs/getting-started/introduction/", "http://localhost:3000/blog/2023-08-02-seed-annoucement/"]
-    },
-    upload: {
-      target: 'temporary-public-storage',
-    },
-  },
-};
-EOF
-
-# Install and Run LHCI
-npm install -g @lhci/cli
-lhci autorun || echo "LHCI failed!"
-
-#!/bin/bash
-
-# Define the base URL where you want to send the POST requests
-base_url="https://app.posthog.com/capture/"
-
-# Directory containing JSON files
-json_dir=".lighthouseci"
-
-# API Key
-api_key="$POSTHOG"
-
-# Get the current timestamp
-timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
-
-# Loop through each JSON file in the directory
-for json_file in "$json_dir"/*.json; do
-    if [ -f "$json_file" ]; then
-        # Extract the file name without the extension
-        file_name=$(basename "$json_file" .json)
-
-        # Generate a random distinct_id (a random number)
-        distinct_id=$((RANDOM))
-
-        # Read the contents of the JSON file
-        json_data=$(cat "$json_file")
-
-        # Construct the event name with the JSON file name
-        event="Lighthouse CI - $file_name"
-
-        # Construct the JSON payload with the random distinct_id
-        payload="{\"api_key\": \"$api_key\", \"event\": \"$event\", \"timestamp\": \"$timestamp\", \"distinct_id\": $distinct_id, \"properties\": $json_data}"
-
-        # Create a temporary file for the payload
-        tmpfile=$(mktemp)
-
-        # Write the payload to the temporary file
-        echo "$payload" > "$tmpfile"
-
-        # Send the POST request with the constructed payload using curl
-        response=$(curl -X POST -H "Content-Type: application/json" --data @"$tmpfile" "$base_url")
-
-        # Clean up the temporary file
-        rm "$tmpfile"
-
-        # Print the response for each file
-        echo "Response for $json_file:"
-        echo "$response"
-    fi
-done