From 2ac6ac005a9c8716a1924015a57c4fe9b87fe5f9 Mon Sep 17 00:00:00 2001 From: hansp Date: Thu, 8 Oct 2026 13:33:41 -0400 Subject: [PATCH] working FastAPI in docker --- Dockerfile | 22 ++ README.md | 15 +- museum_analytics/.env.example | 2 +- museum_analytics/constants.py | 9 +- museum_analytics/museum_api.py | 44 ++-- museum_analytics/museum_regression.ipynb | 313 +++++++++++++++++++++++ poetry.lock | 238 ++++++++--------- pyproject.toml | 11 +- 8 files changed, 496 insertions(+), 158 deletions(-) create mode 100644 Dockerfile create mode 100644 museum_analytics/museum_regression.ipynb diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f46f842 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +FROM python:3.12-slim + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + POETRY_VIRTUALENVS_CREATE=false \ + POETRY_NO_INTERACTION=1 \ + POETRY_REQUESTS_TIMEOUT=120 \ + POETRY_INSTALLER_MAX_WORKERS=4 + +WORKDIR /app + +RUN pip install --no-cache-dir poetry + +# Dependencies first, for layer caching +COPY pyproject.toml poetry.lock ./ +RUN --mount=type=cache,target=/root/.cache/pypoetry \ + poetry install --only main --no-root --no-ansi + +COPY . . + +EXPOSE 8000 +CMD ["uvicorn", "museum_analytics.museum_api:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/README.md b/README.md index 74125ac..3dcae2e 100644 --- a/README.md +++ b/README.md @@ -14,4 +14,17 @@ This project uses environment variables to secure sensitive information. Follow ```text WIKIPEDIA_USER_AGENT="MuseumAnalytics/0.1 (your_email_address)" ``` -⚠️ **Important:** Never commit your `.env` file to Git. It is already added to `.gitignore` to protect your secrets. \ No newline at end of file +⚠️ **Important:** Never commit your `.env` file to Git. It is already added to `.gitignore` to protect your secrets. + +## Installation + +1. **Make sure docker is installed: see https://docs.docker.com/get-started/get-docker/ for instructions.** +2. **Clone the repo** + ```Bash + git clone https://git.hanspayer.com/hpayer/museum_analytics.git + ``` +3. Build and start the museum analytics server with docker: + ```Bash + cd ./museum_analytics + docker build --network=host --no-cache -t museum-analytics-server . + ``` \ No newline at end of file diff --git a/museum_analytics/.env.example b/museum_analytics/.env.example index 5160f5b..3ba8039 100644 --- a/museum_analytics/.env.example +++ b/museum_analytics/.env.example @@ -1 +1 @@ -WIKIPEDIA_USER_AGENT="MuseumAnalytics/0.1 (your_email_address)" # johndoe@gmail.com \ No newline at end of file +WIKIPEDIA_USER_AGENT="MuseumAnalytics/0.1 (your_email_address@domain)" # johndoe@gmail.com \ No newline at end of file diff --git a/museum_analytics/constants.py b/museum_analytics/constants.py index 084b718..b8a49e7 100644 --- a/museum_analytics/constants.py +++ b/museum_analytics/constants.py @@ -1,6 +1,11 @@ +from pathlib import Path + +# Resolve data files relative to this package, independent of the working directory +DATA_DIR = Path(__file__).resolve().parent / "data" + DEFAULT_MUSEUM_DATA_SOURCE_URL = "https://en.wikipedia.org/wiki/List_of_most_visited_museums" WIKIPEDIA_REST_API_URL_PREFIX = "https://en.wikipedia.org/api/rest_v1/page/html" # csv file downloaded from https://simplemaps.com/data/world-cities -RAW_POPULATION_DATA_FILE = "data/worldcities.csv" -MUSEUM_DATA_FILE = "data/updated_museum_data.csv" +RAW_POPULATION_DATA_FILE = DATA_DIR / "worldcities.csv" +MUSEUM_DATA_FILE = DATA_DIR / "updated_museum_data.csv" diff --git a/museum_analytics/museum_api.py b/museum_analytics/museum_api.py index 485f807..ca8d81b 100644 --- a/museum_analytics/museum_api.py +++ b/museum_analytics/museum_api.py @@ -1,27 +1,37 @@ """ Museum visitors vs. city population: regression model + FastAPI service. -Usage, with a DataFrame you already have in memory: +Run locally (from the repo root): - from museum_api import create_app + uvicorn museum_analytics.museum_api:app --reload + +or run this file directly (e.g. from the IDE): + + python museum_analytics/museum_api.py + +Or with a DataFrame you already have in memory: + + from museum_analytics.museum_api import create_app import uvicorn uvicorn.run(create_app(df), port=8000) -Or from a file: - - MUSEUM_DATA=museums.csv uvicorn museum_api:app --reload - Expected columns: Name, Visitors, City, Country, Population """ from __future__ import annotations import os +import sys +from pathlib import Path + +if __package__ in (None, ""): + # Started as a script (python museum_api.py): put the repo root on the + # path so the `museum_analytics` package imports below resolve. + sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from dataclasses import dataclass, field from typing import Literal import numpy as np import pandas as pd -import data_setup -import constants +from museum_analytics import constants, data_setup from fastapi import FastAPI, HTTPException, Query from pydantic import BaseModel, Field from sklearn.linear_model import LinearRegression @@ -268,21 +278,19 @@ def create_app( def _load_data() -> pd.DataFrame | None: - relative_path = constants.MUSEUM_DATA_FILE + path = constants.MUSEUM_DATA_FILE - absolute_path = os.path.abspath(relative_path) - - if not os.path.exists(absolute_path): + if not path.exists(): print("Fetching and conforming museum data and city population...") museum_data = data_setup.get_museum_data() - museum_data.to_csv(absolute_path) + museum_data.to_csv(path) else: - museum_data = pd.read_csv(absolute_path) + museum_data = pd.read_csv(path) return museum_data -# Module-level app so `uvicorn museum_api:app` works. +# Module-level app so `uvicorn museum_analytics.museum_api:app` works. app = create_app( _load_data(), scale=os.environ.get("MUSEUM_SCALE", "log"), # type: ignore[arg-type] @@ -292,4 +300,8 @@ app = create_app( if __name__ == "__main__": import uvicorn - uvicorn.run(app, host="0.0.0.0", port=8000) \ No newline at end of file + uvicorn.run( + app, + host=os.environ.get("HOST", "127.0.0.1"), + port=int(os.environ.get("PORT", "8000")), + ) \ No newline at end of file diff --git a/museum_analytics/museum_regression.ipynb b/museum_analytics/museum_regression.ipynb new file mode 100644 index 0000000..94a36ea --- /dev/null +++ b/museum_analytics/museum_regression.ipynb @@ -0,0 +1,313 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0af7e8f5", + "metadata": {}, + "source": [ + "# Museum visitors vs. city population\n", + "\n", + "This notebook talks to the `museum_api` FastAPI server over HTTP: it starts the server (or uses one you already run), sends it the data, and plots the regression it returns.\n", + "\n", + "**Setup:** put `museum_api.py` next to this notebook, then `pip install scikit-learn pandas fastapi uvicorn requests matplotlib`." + ] + }, + { + "cell_type": "code", + "id": "4c50f329", + "metadata": { + "ExecuteTime": { + "end_time": "2026-10-07T21:11:58.303386100Z", + "start_time": "2026-10-07T21:11:57.570566Z" + } + }, + "source": [ + "import threading, time\n", + "import requests\n", + "import numpy as np\n", + "import pandas as pd\n", + "import matplotlib.pyplot as plt\n", + "from matplotlib.ticker import FuncFormatter\n", + "\n", + "API = \"http://127.0.0.1:8000\" # change if your server runs elsewhere" + ], + "outputs": [ + { + "ename": "ModuleNotFoundError", + "evalue": "No module named 'matplotlib'", + "output_type": "error", + "traceback": [ + "\u001B[31m---------------------------------------------------------------------------\u001B[39m", + "\u001B[31mModuleNotFoundError\u001B[39m Traceback (most recent call last)", + "\u001B[36mCell\u001B[39m\u001B[36m \u001B[39m\u001B[32mIn[1]\u001B[39m\u001B[32m, line 5\u001B[39m\n\u001B[32m 1\u001B[39m \u001B[38;5;28;01mimport\u001B[39;00m threading, time\n\u001B[32m 2\u001B[39m \u001B[38;5;28;01mimport\u001B[39;00m requests\n\u001B[32m 3\u001B[39m \u001B[38;5;28;01mimport\u001B[39;00m numpy \u001B[38;5;28;01mas\u001B[39;00m np\n\u001B[32m 4\u001B[39m \u001B[38;5;28;01mimport\u001B[39;00m pandas \u001B[38;5;28;01mas\u001B[39;00m pd\n\u001B[32m----> \u001B[39m\u001B[32m5\u001B[39m \u001B[38;5;28;01mimport\u001B[39;00m matplotlib.pyplot \u001B[38;5;28;01mas\u001B[39;00m plt\n\u001B[32m 6\u001B[39m \u001B[38;5;28;01mfrom\u001B[39;00m matplotlib.ticker \u001B[38;5;28;01mimport\u001B[39;00m FuncFormatter\n\u001B[32m 7\u001B[39m \n\u001B[32m 8\u001B[39m API = \u001B[33m\"http://127.0.0.1:8000\"\u001B[39m \u001B[38;5;66;03m# change if your server runs elsewhere\u001B[39;00m\n", + "\u001B[31mModuleNotFoundError\u001B[39m: No module named 'matplotlib'" + ] + } + ], + "execution_count": 1 + }, + { + "cell_type": "markdown", + "id": "69524967", + "metadata": {}, + "source": [ + "## 1. Start the server\n", + "\n", + "If a server is already answering at `API` (e.g. you started it with `uvicorn museum_api:app` in a terminal), this cell just uses it. Otherwise it launches one in a background thread inside this kernel. Restart the kernel to stop it." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "b9d41502", + "metadata": {}, + "outputs": [ + { + "ename": "NameError", + "evalue": "name 'API' is not defined", + "output_type": "error", + "traceback": [ + "\u001B[31m---------------------------------------------------------------------------\u001B[39m", + "\u001B[31mNameError\u001B[39m Traceback (most recent call last)", + "\u001B[36mCell\u001B[39m\u001B[36m \u001B[39m\u001B[32mIn[2]\u001B[39m\u001B[32m, line 7\u001B[39m\n\u001B[32m 3\u001B[39m \u001B[38;5;28;01mreturn\u001B[39;00m requests.get(f\"{API}/health\", timeout=\u001B[32m1\u001B[39m).ok\n\u001B[32m 4\u001B[39m \u001B[38;5;28;01mexcept\u001B[39;00m requests.ConnectionError:\n\u001B[32m 5\u001B[39m \u001B[38;5;28;01mreturn\u001B[39;00m \u001B[38;5;28;01mFalse\u001B[39;00m\n\u001B[32m 6\u001B[39m \n\u001B[32m----> \u001B[39m\u001B[32m7\u001B[39m \u001B[38;5;28;01mif\u001B[39;00m \u001B[38;5;28;01mnot\u001B[39;00m server_up():\n\u001B[32m 8\u001B[39m \u001B[38;5;28;01mimport\u001B[39;00m uvicorn\n\u001B[32m 9\u001B[39m \u001B[38;5;28;01mfrom\u001B[39;00m museum_api \u001B[38;5;28;01mimport\u001B[39;00m create_app\n\u001B[32m 10\u001B[39m server = uvicorn.Server(uvicorn.Config(create_app(), host=\u001B[33m\"127.0.0.1\"\u001B[39m, port=\u001B[32m8000\u001B[39m, log_level=\u001B[33m\"warning\"\u001B[39m))\n", + "\u001B[36mCell\u001B[39m\u001B[36m \u001B[39m\u001B[32mIn[2]\u001B[39m\u001B[32m, line 4\u001B[39m, in \u001B[36mserver_up\u001B[39m\u001B[34m()\u001B[39m\n\u001B[32m 1\u001B[39m \u001B[38;5;28;01mdef\u001B[39;00m server_up():\n\u001B[32m 2\u001B[39m \u001B[38;5;28;01mtry\u001B[39;00m:\n\u001B[32m 3\u001B[39m \u001B[38;5;28;01mreturn\u001B[39;00m requests.get(f\"{API}/health\", timeout=\u001B[32m1\u001B[39m).ok\n\u001B[32m----> \u001B[39m\u001B[32m4\u001B[39m \u001B[38;5;28;01mexcept\u001B[39;00m requests.ConnectionError:\n\u001B[32m 5\u001B[39m \u001B[38;5;28;01mreturn\u001B[39;00m \u001B[38;5;28;01mFalse\u001B[39;00m\n", + "\u001B[31mNameError\u001B[39m: name 'API' is not defined" + ] + } + ], + "source": [ + "def server_up():\n", + " try:\n", + " return requests.get(f\"{API}/health\", timeout=1).ok\n", + " except requests.ConnectionError:\n", + " return False\n", + "\n", + "if not server_up():\n", + " import uvicorn\n", + " from museum_api import create_app\n", + " server = uvicorn.Server(uvicorn.Config(create_app(), host=\"127.0.0.1\", port=8000, log_level=\"warning\"))\n", + " threading.Thread(target=server.run, daemon=True).start()\n", + " for _ in range(50):\n", + " if server_up(): break\n", + " time.sleep(0.1)\n", + "\n", + "requests.get(f\"{API}/health\").json()" + ] + }, + { + "cell_type": "markdown", + "id": "742501f7", + "metadata": {}, + "source": [ + "## 2. Load your data\n", + "\n", + "Replace this with however you build your DataFrame. It needs the columns `Name, Visitors, City, Country, Population`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cb58b0f5", + "metadata": {}, + "outputs": [], + "source": [ + "df = pd.read_csv(\"museums.csv\")\n", + "df.head()" + ] + }, + { + "cell_type": "markdown", + "id": "362d6127", + "metadata": {}, + "source": [ + "## 3. Train the model through the API\n", + "\n", + "`scale=\"log\"` fits log(visitors) ~ log(population). Set `aggregate_by_city=True` to count each city once instead of once per museum." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ab229859", + "metadata": {}, + "outputs": [], + "source": [ + "def train(df, scale=\"log\", aggregate_by_city=False):\n", + " records = df[[\"Name\", \"Visitors\", \"City\", \"Country\", \"Population\"]].dropna().to_dict(\"records\")\n", + " r = requests.post(f\"{API}/train\", json={\"records\": records, \"scale\": scale,\n", + " \"aggregate_by_city\": aggregate_by_city})\n", + " r.raise_for_status()\n", + " return r.json()\n", + "\n", + "summary = train(df, scale=\"log\")\n", + "pd.Series(summary).to_frame(\"value\")" + ] + }, + { + "cell_type": "markdown", + "id": "f655bbcf", + "metadata": {}, + "source": [ + "## 4. Plot the regression" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1a23269f", + "metadata": {}, + "outputs": [], + "source": [ + "BLUE, ORANGE, INK, MUTED = \"#2a78d6\", \"#eb6834\", \"#0b0b0b\", \"#52514e\"\n", + "plt.rcParams.update({\"axes.spines.top\": False, \"axes.spines.right\": False,\n", + " \"axes.edgecolor\": MUTED, \"axes.labelcolor\": INK,\n", + " \"xtick.color\": MUTED, \"ytick.color\": MUTED,\n", + " \"axes.grid\": True, \"axes.axisbelow\": True, \"grid.color\": \"#e6e5e0\", \"grid.linewidth\": 0.8,\n", + " \"figure.dpi\": 110})\n", + "\n", + "def human(x, _=None):\n", + " for div, suf in ((1e9, \"B\"), (1e6, \"M\"), (1e3, \"K\")):\n", + " if abs(x) >= div: return f\"{x/div:g}{suf}\"\n", + " return f\"{x:g}\"\n", + "\n", + "points = pd.DataFrame(requests.get(f\"{API}/data\").json())\n", + "\n", + "# Fitted curve: ask the API to predict across the population range\n", + "grid = np.geomspace(points.Population.min(), points.Population.max(), 100)\n", + "curve = pd.DataFrame(requests.post(f\"{API}/predict\", json={\"populations\": grid.tolist()}).json())\n", + "\n", + "fig, ax = plt.subplots(figsize=(9, 6))\n", + "ax.scatter(points.Population, points.Visitors, s=40, color=BLUE, alpha=0.75,\n", + " edgecolor=\"white\", linewidth=1, label=\"Museums\", zorder=3)\n", + "ax.plot(curve.population, curve.predicted_visitors, color=ORANGE, lw=2,\n", + " label=f\"Fit: {summary['equation']}\", zorder=4)\n", + "\n", + "# Label the 5 biggest outliers (furthest from the line on the log scale)\n", + "points[\"logres\"] = np.log10(points.Visitors / points.Predicted)\n", + "for _, r in points.reindex(points.logres.abs().sort_values(ascending=False).index).head(5).iterrows():\n", + " ax.annotate(r.Name, (r.Population, r.Visitors), xytext=(6, 4),\n", + " textcoords=\"offset points\", fontsize=8, color=MUTED)\n", + "\n", + "if summary[\"scale\"] == \"log\":\n", + " ax.set_xscale(\"log\"); ax.set_yscale(\"log\")\n", + "ax.xaxis.set_major_formatter(FuncFormatter(human))\n", + "ax.yaxis.set_major_formatter(FuncFormatter(human))\n", + "ax.set_xlabel(\"City population\"); ax.set_ylabel(\"Annual visitors\")\n", + "cv = summary[\"cv_r2_5fold\"]\n", + "ax.set_title(f\"Visitors vs. city population R² = {summary['r2']:.2f}\"\n", + " + (f\" (cross-validated {cv:.2f})\" if cv is not None else \"\"),\n", + " loc=\"left\", fontsize=12, color=INK)\n", + "ax.legend(frameon=False, loc=\"upper left\")\n", + "plt.tight_layout(); plt.show()\n", + "\n", + "print(summary[\"interpretation\"])" + ] + }, + { + "cell_type": "markdown", + "id": "a4eafc4a", + "metadata": {}, + "source": [ + "## 5. Which museums beat (or miss) their city's size?\n", + "\n", + "Ratio = actual ÷ predicted visitors. Above 1× means the museum draws more than its city's size alone would suggest." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9a823747", + "metadata": {}, + "outputs": [], + "source": [ + "N = 8\n", + "over = pd.DataFrame(requests.get(f\"{API}/residuals\", params={\"top\": N, \"order\": \"over\"}).json())\n", + "under = pd.DataFrame(requests.get(f\"{API}/residuals\", params={\"top\": N, \"order\": \"under\"}).json())\n", + "res = pd.concat([over, under.iloc[::-1]]).drop_duplicates(\"Name\").iloc[::-1]\n", + "\n", + "fig, ax = plt.subplots(figsize=(9, 0.35 * len(res) + 1))\n", + "colors = [BLUE if r >= 1 else ORANGE for r in res.Ratio]\n", + "ax.barh(res.Name + \" (\" + res.City + \")\", np.log10(res.Ratio), color=colors, height=0.7)\n", + "ax.axvline(0, color=MUTED, lw=1)\n", + "ticks = np.array([0.1, 0.2, 0.5, 1, 2, 5, 10])\n", + "ticks = ticks[(np.log10(ticks) >= np.log10(res.Ratio).min() - 0.1) &\n", + " (np.log10(ticks) <= np.log10(res.Ratio).max() + 0.1)]\n", + "ax.set_xticks(np.log10(ticks), [f\"{t:g}×\" for t in ticks])\n", + "ax.set_xlabel(\"Actual ÷ predicted visitors (log scale)\")\n", + "ax.set_title(\"Over-performers (blue) and under-performers (orange)\", loc=\"left\", fontsize=12, color=INK)\n", + "ax.grid(axis=\"y\", visible=False)\n", + "plt.tight_layout(); plt.show()\n", + "\n", + "res[[\"Name\", \"City\", \"Country\", \"Population\", \"Visitors\", \"Predicted\", \"Ratio\"]].round(2)" + ] + }, + { + "cell_type": "markdown", + "id": "ce7a12d7", + "metadata": {}, + "source": [ + "## 6. Compare model variants\n", + "\n", + "Retrains the server with each setting and collects the fit statistics. The last setting tried stays loaded, so the final call re-trains the default." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9adca34e", + "metadata": {}, + "outputs": [], + "source": [ + "rows = []\n", + "for scale in (\"linear\", \"log\"):\n", + " for agg in (False, True):\n", + " s = train(df, scale=scale, aggregate_by_city=agg)\n", + " rows.append({k: s[k] for k in (\"scale\", \"aggregate_by_city\", \"n_samples\",\n", + " \"r2\", \"cv_r2_5fold\", \"pearson_r\", \"spearman_rho\")})\n", + "train(df, scale=\"log\") # restore the default model\n", + "pd.DataFrame(rows).round(3)" + ] + }, + { + "cell_type": "markdown", + "id": "1c0bcd2f", + "metadata": {}, + "source": [ + "## 7. Predict for any city" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6ff399ee", + "metadata": {}, + "outputs": [], + "source": [ + "cities = {\"Montréal\": 1_780_000, \"Lyon\": 520_000, \"Tokyo\": 14_000_000}\n", + "pred = requests.post(f\"{API}/predict\", json={\"populations\": list(cities.values())}).json()\n", + "pd.DataFrame(pred, index=cities.keys()).round(0)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.10" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/poetry.lock b/poetry.lock index fbc8361..9e0f83e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -30,7 +30,7 @@ version = "4.15.1" description = "High-level concurrency and networking framework on top of asyncio or Trio" optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "anyio-4.15.1-py3-none-any.whl", hash = "sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101"}, {file = "anyio-4.15.1.tar.gz", hash = "sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94"}, @@ -49,7 +49,7 @@ version = "1.0.0" description = "Disable App Nap on macOS >= 10.9" optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["dev"] markers = "platform_system == \"Darwin\"" files = [ {file = "appnope-1.0.0-py3-none-any.whl", hash = "sha256:6fe0c04218aab65c54c4ff81638cdbf848d89f5653b74d68638a137f200dd16e"}, @@ -62,7 +62,7 @@ version = "25.1.0" description = "Argon2 for Python" optional = false python-versions = ">=3.8" -groups = ["main"] +groups = ["dev"] files = [ {file = "argon2_cffi-25.1.0-py3-none-any.whl", hash = "sha256:fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741"}, {file = "argon2_cffi-25.1.0.tar.gz", hash = "sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1"}, @@ -77,7 +77,7 @@ version = "26.1.0" description = "Low-level CFFI bindings for Argon2" optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["dev"] files = [ {file = "argon2_cffi_bindings-26.1.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:21ca0396fe5ec995dd54431c32698189666f9224810acfa752e50d2bd94d9df2"}, {file = "argon2_cffi_bindings-26.1.0-cp310-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78de2d65e0b9ea7ce9d1b1c3e87297b2d7305a02c266ee2a2d6910daddd7ee69"}, @@ -134,7 +134,7 @@ version = "1.4.0" description = "Better dates & times for Python" optional = false python-versions = ">=3.8" -groups = ["main"] +groups = ["dev"] files = [ {file = "arrow-1.4.0-py3-none-any.whl", hash = "sha256:749f0769958ebdc79c173ff0b0670d59051a535fa26e8eba02953dc19eb43205"}, {file = "arrow-1.4.0.tar.gz", hash = "sha256:ed0cc050e98001b8779e84d461b0098c4ac597e88704a655582b21d116e526d7"}, @@ -154,7 +154,7 @@ version = "3.0.2" description = "Annotate AST trees with source code positions" optional = false python-versions = ">=3.8" -groups = ["main"] +groups = ["dev"] files = [ {file = "asttokens-3.0.2-py3-none-any.whl", hash = "sha256:9da13157f5b28becde0bd374fc677dcd3c290614264eff096f167c469cd9f933"}, {file = "asttokens-3.0.2.tar.gz", hash = "sha256:3ecdbd8f2cc195f53ccada3a613538bb5f9ef6f6869129f13e03c30a677b8fe2"}, @@ -170,7 +170,7 @@ version = "2.3.0" description = "Simple LRU cache for asyncio" optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["dev"] files = [ {file = "async_lru-2.3.0-py3-none-any.whl", hash = "sha256:eea27b01841909316f2cc739807acea1c623df2be8c5cfad7583286397bb8315"}, {file = "async_lru-2.3.0.tar.gz", hash = "sha256:89bdb258a0140d7313cf8f4031d816a042202faa61d0ab310a0a538baa1c24b6"}, @@ -182,7 +182,7 @@ version = "26.1.0" description = "Classes Without Boilerplate" optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["dev"] files = [ {file = "attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309"}, {file = "attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32"}, @@ -194,7 +194,7 @@ version = "2.18.0" description = "Internationalization utilities" optional = false python-versions = ">=3.8" -groups = ["main"] +groups = ["dev"] files = [ {file = "babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35"}, {file = "babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d"}, @@ -209,7 +209,7 @@ version = "4.15.0" description = "Screen-scraping library" optional = false python-versions = ">=3.7.0" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "beautifulsoup4-4.15.0-py3-none-any.whl", hash = "sha256:d6f88de62e1d4e38ecb1077eb9724cd0eff29d2a08ca16a401e9b9e93f117cf9"}, {file = "beautifulsoup4-4.15.0.tar.gz", hash = "sha256:288e3ca7d54b06f2ac191970bc275c1939cb46d450b255bf6718b04aa37ab4f7"}, @@ -232,7 +232,7 @@ version = "6.4.0" description = "An easy safelist-based HTML-sanitizing tool." optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["dev"] files = [ {file = "bleach-6.4.0-py3-none-any.whl", hash = "sha256:4b6b6a54fff2e69a3dde9d21cc6301220bee3c3cb792187d11403fd795031081"}, {file = "bleach-6.4.0.tar.gz", hash = "sha256:4202482733d85cedd04e59fcb2f89f4e4c7c385a78d3c3c23c30446843a37452"}, @@ -251,7 +251,7 @@ version = "2026.7.22" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.7" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775"}, {file = "certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55"}, @@ -263,7 +263,7 @@ version = "2.1.1" description = "Foreign Function Interface for Python calling C code." optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["dev"] files = [ {file = "cffi-2.1.1-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:baed1e86cc735622097354b9d1281406caf42ff42a886d29faa8e8d1630333be"}, {file = "cffi-2.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ca82be1a1d406ecfe1d25dc16cb33488e5a16bf4438c9fb590484ea29d92478b"}, @@ -376,7 +376,7 @@ version = "3.5.2" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "charset_normalizer-3.5.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:195c26fb65950f8fce54e26349852b7bdd7c5f120aeefbcc440b8a20faaed4a3"}, {file = "charset_normalizer-3.5.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9373ad13ef0d2c0fb761e04e55bfdee5a08b52cef2c882c8fbe9935b1517152e"}, @@ -582,7 +582,7 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -groups = ["main"] +groups = ["dev"] markers = "sys_platform == \"win32\"" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, @@ -595,7 +595,7 @@ version = "0.2.3" description = "Jupyter Python Comm implementation, for usage in ipykernel, xeus-python etc." optional = false python-versions = ">=3.8" -groups = ["main"] +groups = ["dev"] files = [ {file = "comm-0.2.3-py3-none-any.whl", hash = "sha256:c615d91d75f7f04f095b30d1c1711babd43bdc6419c1be9886a85f2f4e489417"}, {file = "comm-0.2.3.tar.gz", hash = "sha256:2dc8048c10962d55d7ad693be1e7045d891b7ce8d999c97963a5e3e99c055971"}, @@ -610,7 +610,7 @@ version = "1.4.0" description = "Python library for calculating contours of 2D quadrilateral grids" optional = false python-versions = ">=3.12" -groups = ["main"] +groups = ["dev"] files = [ {file = "contourpy-1.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:186ba929df36d61b6127da2e89cd1357e4cb79aca647381ab1a6feb0b152877b"}, {file = "contourpy-1.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c76f3a5318164db7d9401132fc1b5364b784c613c93fa506e3b5e6d1bf353ec8"}, @@ -700,7 +700,7 @@ version = "0.12.1" description = "Composable style cycles" optional = false python-versions = ">=3.8" -groups = ["main"] +groups = ["dev"] files = [ {file = "cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30"}, {file = "cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c"}, @@ -716,7 +716,7 @@ version = "1.8.22" description = "An implementation of the Debug Adapter Protocol for Python" optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["dev"] files = [ {file = "debugpy-1.8.22-cp310-cp310-macosx_15_0_x86_64.whl", hash = "sha256:fa47099176d1d612bef69e9f74658a5687a9f05bd80fb5710f99959bace85735"}, {file = "debugpy-1.8.22-cp310-cp310-manylinux_2_34_x86_64.whl", hash = "sha256:7bf29e0d8ce80b100d37fb333e1b193b790d962739f3067796c2b03ffdc9afee"}, @@ -756,7 +756,7 @@ version = "0.7.1" description = "XML bomb protection for Python stdlib modules" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -groups = ["main"] +groups = ["dev"] files = [ {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"}, {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, @@ -768,7 +768,7 @@ version = "2.2.1" description = "Get the currently executing AST node of a frame, and other information" optional = false python-versions = ">=3.8" -groups = ["main"] +groups = ["dev"] files = [ {file = "executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017"}, {file = "executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4"}, @@ -809,7 +809,7 @@ version = "2.22.2" description = "Fastest Python implementation of JSON schema" optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["dev"] files = [ {file = "fastjsonschema-2.22.2-py3-none-any.whl", hash = "sha256:0fb3915616adac85ccfdd737d26be1089845d2019819505b42d39888458f74d4"}, {file = "fastjsonschema-2.22.2.tar.gz", hash = "sha256:72064e12356a7d6ef02165be2946b9abadbdf238536e07eb587e3dbaa33099cf"}, @@ -824,7 +824,7 @@ version = "4.66.1" description = "Tools to manipulate font files" optional = false python-versions = ">=3.11" -groups = ["main"] +groups = ["dev"] files = [ {file = "fonttools-4.66.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d4f76868aea9cc4ce47fdbeaa904c02ee7d85dd0ad095071ae77f0bda6e62cf5"}, {file = "fonttools-4.66.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:34378db9a398b59de18cc79d942f0a907c6fc6301945e065ec888202f607aa3f"}, @@ -905,7 +905,7 @@ version = "1.6.0" description = "Validates fully-qualified domain names against RFC 1123, so that they are acceptable to modern browsers" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,<4,>=2.7" -groups = ["main"] +groups = ["dev"] files = [ {file = "fqdn-1.6.0-py3-none-any.whl", hash = "sha256:6c793c312ccfa29981e1135b3ef5e1277ebe639731a6b0f7350d90e96e63f0b4"}, {file = "fqdn-1.6.0.tar.gz", hash = "sha256:e39bf62e2a9481aa2cb780554fdfd944dd8ee5f230eb831684d539cbafed8de2"}, @@ -920,7 +920,7 @@ version = "0.16.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" optional = false python-versions = ">=3.8" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, @@ -954,7 +954,7 @@ version = "1.0.9" description = "A minimal low-level HTTP client." optional = false python-versions = ">=3.8" -groups = ["main"] +groups = ["dev"] files = [ {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, @@ -976,7 +976,7 @@ version = "0.28.1" description = "The next generation HTTP client." optional = false python-versions = ">=3.8" -groups = ["main"] +groups = ["dev"] files = [ {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, @@ -1001,7 +1001,7 @@ version = "3.20" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "idna-3.20-py3-none-any.whl", hash = "sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c"}, {file = "idna-3.20.tar.gz", hash = "sha256:a7db850025b95ded1eae8a46181a1a6c56c92c96f0e2b005d9ff8dc0210cab44"}, @@ -1016,7 +1016,7 @@ version = "7.4.0" description = "IPython Kernel for Jupyter" optional = false python-versions = ">=3.11" -groups = ["main"] +groups = ["dev"] files = [ {file = "ipykernel-7.4.0-py3-none-any.whl", hash = "sha256:a6757f790ddc5a6006b813d82da1a9dbb65b3a00f2b6607df776418c6566ec1c"}, {file = "ipykernel-7.4.0.tar.gz", hash = "sha256:4330114d22b9b33575b2c7c68753fc8faabbbcfbc60b0cdff9f14dd4ce63742f"}, @@ -1042,7 +1042,7 @@ version = "9.17.1" description = "IPython: Productive Interactive Computing" optional = false python-versions = ">=3.11" -groups = ["main"] +groups = ["dev"] files = [ {file = "ipython-9.17.1-py3-none-any.whl", hash = "sha256:6d1645743cfd1a07eb695d85aa2b5fa66721f8cbae9431d4049f7084bbf06509"}, {file = "ipython-9.17.1.tar.gz", hash = "sha256:8919be8c27f20a6f4423145028063f6637b42a03ce57665bb12015ee1f073529"}, @@ -1074,7 +1074,7 @@ version = "1.1.1" description = "Defines a variety of Pygments lexers for highlighting IPython code." optional = false python-versions = ">=3.8" -groups = ["main"] +groups = ["dev"] files = [ {file = "ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c"}, {file = "ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81"}, @@ -1089,7 +1089,7 @@ version = "20.11.0" description = "Operations with ISO 8601 durations" optional = false python-versions = ">=3.7" -groups = ["main"] +groups = ["dev"] files = [ {file = "isoduration-20.11.0-py3-none-any.whl", hash = "sha256:b2904c2a4228c3d44f409c8ae8e2370eb21a26f7ac2ec5446df141dde3452042"}, {file = "isoduration-20.11.0.tar.gz", hash = "sha256:ac2f9015137935279eac671f94f89eb00584f940f5dc49462a0c4ee692ba1bd9"}, @@ -1104,7 +1104,7 @@ version = "0.20.0" description = "An autocompletion tool for Python that can be used for text editors." optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["dev"] files = [ {file = "jedi-0.20.0-py2.py3-none-any.whl", hash = "sha256:7bdd9c2634f56713299976f4cbd59cb3fa92165cc5e05ea811fb253480728b67"}, {file = "jedi-0.20.0.tar.gz", hash = "sha256:c3f4ccbd276696f4b19c54618d4fb18f9fc24b0aef02acf704b23f487daa1011"}, @@ -1123,7 +1123,7 @@ version = "3.1.6" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" -groups = ["main"] +groups = ["dev"] files = [ {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, @@ -1160,7 +1160,7 @@ version = "0.15.0" description = "A Python implementation of the JSON5 data format." optional = false python-versions = ">=3.8.0" -groups = ["main"] +groups = ["dev"] files = [ {file = "json5-0.15.0-py3-none-any.whl", hash = "sha256:56636a30c0e8a4665fe2179c0212f32eae3796dea89ea6f649b9436ecdb39618"}, {file = "json5-0.15.0.tar.gz", hash = "sha256:7424d1f1eb1d56da6e3d70643f53619862b4ce81440bdb8ecfd6f875e5ba4a71"}, @@ -1172,7 +1172,7 @@ version = "3.2.0" description = "Identify specific nodes in a JSON document (RFC 6901) " optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["dev"] files = [ {file = "jsonpointer-3.2.0-py3-none-any.whl", hash = "sha256:3e25c9bcadf5ae20c3420922234eaba1ae36ab9bef64cd72454aeaceb0b29aad"}, {file = "jsonpointer-3.2.0.tar.gz", hash = "sha256:807db557622fbe07a0d49e19cf4795a269d55eee5ba345fb9959d148eba5ef94"}, @@ -1184,7 +1184,7 @@ version = "4.26.0" description = "An implementation of JSON Schema validation for Python" optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["dev"] files = [ {file = "jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce"}, {file = "jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326"}, @@ -1215,7 +1215,7 @@ version = "2025.9.1" description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["dev"] files = [ {file = "jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe"}, {file = "jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d"}, @@ -1230,7 +1230,7 @@ version = "1.2.3" description = "JupyterLab build tools" optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["dev"] files = [ {file = "jupyter_builder-1.2.3-py3-none-any.whl", hash = "sha256:c5ea5a7190c2a7b082494abade98eece1b2b5bd5dbd7d610606cbcd10a1d08b3"}, {file = "jupyter_builder-1.2.3.tar.gz", hash = "sha256:01aba6794eb9b19e0e29ae21137ca60ba4135c70347d4b9f664a586822b8c809"}, @@ -1250,7 +1250,7 @@ version = "8.10.0" description = "Jupyter protocol implementation and client libraries" optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["dev"] files = [ {file = "jupyter_client-8.10.0-py3-none-any.whl", hash = "sha256:5f73f24f22fa25192cfff6b23c051932a2473a797b05734aff495b392103e14e"}, {file = "jupyter_client-8.10.0.tar.gz", hash = "sha256:9f7116294dca55f1785be880057d44544db9b1567718d92cb33c58886afb9497"}, @@ -1275,7 +1275,7 @@ version = "5.9.1" description = "Jupyter core package. A base package on which Jupyter projects rely." optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["dev"] files = [ {file = "jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407"}, {file = "jupyter_core-5.9.1.tar.gz", hash = "sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508"}, @@ -1295,7 +1295,7 @@ version = "0.12.1" description = "Jupyter Event System library" optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["dev"] files = [ {file = "jupyter_events-0.12.1-py3-none-any.whl", hash = "sha256:c366585253f537a627da52fa7ca7410c5b5301fe893f511e7b077c2d93ec8bcf"}, {file = "jupyter_events-0.12.1.tar.gz", hash = "sha256:faff25f77218335752f35f23c5fe6e4a392a7bd99a5939ccb9b8fbf594636cf3"}, @@ -1322,7 +1322,7 @@ version = "2.3.1" description = "Multi-Language Server WebSocket proxy for Jupyter Notebook/Lab server" optional = false python-versions = ">=3.8" -groups = ["main"] +groups = ["dev"] files = [ {file = "jupyter_lsp-2.3.1-py3-none-any.whl", hash = "sha256:71b954d834e85ff3096400554f2eefaf7fe37053036f9a782b0f7c5e42dadb81"}, {file = "jupyter_lsp-2.3.1.tar.gz", hash = "sha256:fdf8a4aa7d85813976d6e29e95e6a2c8f752701f926f2715305249a3829805a6"}, @@ -1337,7 +1337,7 @@ version = "2.21.1" description = "The backend—i.e. core services, APIs, and REST endpoints—to Jupyter web applications." optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["dev"] files = [ {file = "jupyter_server-2.21.1-py3-none-any.whl", hash = "sha256:2a6467606af7dbae2e7e31640030025969e15db6a649eae334af90415dc71dca"}, {file = "jupyter_server-2.21.1.tar.gz", hash = "sha256:a8960aa29263f6041283e97d4756b099fb49b767baab6371891ecb1bd40a63df"}, @@ -1373,7 +1373,7 @@ version = "0.5.4" description = "A Jupyter Server Extension Providing Terminals." optional = false python-versions = ">=3.8" -groups = ["main"] +groups = ["dev"] files = [ {file = "jupyter_server_terminals-0.5.4-py3-none-any.whl", hash = "sha256:55be353fc74a80bc7f3b20e6be50a55a61cd525626f578dcb66a5708e2007d14"}, {file = "jupyter_server_terminals-0.5.4.tar.gz", hash = "sha256:bbda128ed41d0be9020349f9f1f2a4ab9952a73ed5f5ac9f1419794761fb87f5"}, @@ -1393,7 +1393,7 @@ version = "4.6.4" description = "JupyterLab computational environment" optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["dev"] files = [ {file = "jupyterlab-4.6.4-py3-none-any.whl", hash = "sha256:15b13f991d3985129c797eb84d9949eeb8b6615e14b444868e642411f2c418b2"}, {file = "jupyterlab-4.6.4.tar.gz", hash = "sha256:404f49b081819378524886c9db66dba57a5565981eff885830df1baba3a17df5"}, @@ -1426,7 +1426,7 @@ version = "0.3.0" description = "Pygments theme using JupyterLab CSS variables" optional = false python-versions = ">=3.8" -groups = ["main"] +groups = ["dev"] files = [ {file = "jupyterlab_pygments-0.3.0-py3-none-any.whl", hash = "sha256:841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780"}, {file = "jupyterlab_pygments-0.3.0.tar.gz", hash = "sha256:721aca4d9029252b11cfa9d185e5b5af4d54772bb8072f9b7036f4170054d35d"}, @@ -1438,7 +1438,7 @@ version = "2.28.1" description = "A set of server components for JupyterLab and JupyterLab like applications." optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["dev"] files = [ {file = "jupyterlab_server-2.28.1-py3-none-any.whl", hash = "sha256:4bd36c7c11d872e15cefa4951e12380a47e6901687165332dd50a007cb367ad9"}, {file = "jupyterlab_server-2.28.1.tar.gz", hash = "sha256:0c3c2418d51021ce280916e63dfe4cba8386b4e2787be5c25433c98f560ddb31"}, @@ -1464,7 +1464,7 @@ version = "1.5.1" description = "A fast implementation of the Cassowary constraint solver" optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["dev"] files = [ {file = "kiwisolver-1.5.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1acc7e5b7ef05e9da8bb70cd6c7c4513090213d2e1ad9720f599f0bf6c52aec5"}, {file = "kiwisolver-1.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bad20d4c69c851c982a1e3606f4c293edfd5a87885786c50082412240c4b1ffd"}, @@ -1615,7 +1615,7 @@ version = "1.3.1" description = "a modern parsing library" optional = false python-versions = ">=3.8" -groups = ["main"] +groups = ["dev"] files = [ {file = "lark-1.3.1-py3-none-any.whl", hash = "sha256:c629b661023a014c37da873b4ff58a817398d12635d3bbb2c5a03be7fe5d1e12"}, {file = "lark-1.3.1.tar.gz", hash = "sha256:b426a7a6d6d53189d318f2b6236ab5d6429eaf09259f1ca33eb716eed10d2905"}, @@ -1825,7 +1825,7 @@ version = "3.0.4" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["dev"] files = [ {file = "markupsafe-3.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dd8ea6ebee7aedbf7c749fa80521d9ccf1ba473e0d1e14805caafbaad281c889"}, {file = "markupsafe-3.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dff05cb7016dff1e9fd68f4122c127b65dfc59de5306cfb7ad92f956f230bee2"}, @@ -1986,7 +1986,7 @@ version = "3.11.2" description = "Python plotting package" optional = false python-versions = ">=3.11" -groups = ["main"] +groups = ["dev"] files = [ {file = "matplotlib-3.11.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:5e1e923a3fc3326b99ec0a6ff1ab1338ac6c6cc62ad9d8a9c944197c7f8c6221"}, {file = "matplotlib-3.11.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c27e577ece613ea12a0790e00b4eb80d901c59a3e16cad474f31d8b1529690b9"}, @@ -2060,7 +2060,7 @@ version = "0.2.2" description = "Inline Matplotlib backend for Jupyter" optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["dev"] files = [ {file = "matplotlib_inline-0.2.2-py3-none-any.whl", hash = "sha256:3c821cf1c209f59fb2d2d64abbf5b23b67bcb2210d663f9918dd851c6da1fcf6"}, {file = "matplotlib_inline-0.2.2.tar.gz", hash = "sha256:72f3fe8fce36b70d4a5b612f899090cd0401deddc4ea90e1572b9f4bfb058c79"}, @@ -2078,7 +2078,7 @@ version = "3.3.4" description = "A sane and fast Markdown parser with useful plugins and renderers" optional = false python-versions = ">=3.8" -groups = ["main"] +groups = ["dev"] files = [ {file = "mistune-3.3.4-py3-none-any.whl", hash = "sha256:ee015381e955e370962968befe1d729ab60fafb6a715ac6751763fbce38c8d4a"}, {file = "mistune-3.3.4.tar.gz", hash = "sha256:58b5c96d6fcb61190dfe5fae498d2b2065f99cf61e9649418fd54cf1ada86dfe"}, @@ -2116,7 +2116,7 @@ version = "0.11.0" description = "A client library for executing notebooks. Formerly nbconvert's ExecutePreprocessor." optional = false python-versions = ">=3.10.0" -groups = ["main"] +groups = ["dev"] files = [ {file = "nbclient-0.11.0-py3-none-any.whl", hash = "sha256:ef7fa0d59d6e1d41103933d8a445a18d5de860ca6b613b87b8574accdb3c2895"}, {file = "nbclient-0.11.0.tar.gz", hash = "sha256:04a134a5b087f2c5887f228aca155db50169b8cd9334dee6942c8e927e56081a"}, @@ -2139,7 +2139,7 @@ version = "7.17.1" description = "Convert Jupyter Notebooks (.ipynb files) to other formats." optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["dev"] files = [ {file = "nbconvert-7.17.1-py3-none-any.whl", hash = "sha256:aa85c087b435e7bf1ffd03319f658e285f2b89eccab33bc1ba7025495ab3e7c8"}, {file = "nbconvert-7.17.1.tar.gz", hash = "sha256:34d0d0a7e73ce3cbab6c5aae8f4f468797280b01fd8bd2ca746da8569eddd7d2"}, @@ -2176,7 +2176,7 @@ version = "5.11.1" description = "The Jupyter Notebook format" optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["dev"] files = [ {file = "nbformat-5.11.1-py3-none-any.whl", hash = "sha256:cc6698fa75f4fab8755ead786317815f13a6fee3b53311c0abb1a8b51d52f7ec"}, {file = "nbformat-5.11.1.tar.gz", hash = "sha256:32d4521c68c6e7d5b29c76defaeed9f42ea733142b9b19f88277ce10390b9c4d"}, @@ -2198,7 +2198,7 @@ version = "1.7.3" description = "Patch asyncio to allow nested event loops" optional = false python-versions = ">=3.5" -groups = ["main"] +groups = ["dev"] files = [ {file = "nest_asyncio2-1.7.3-py3-none-any.whl", hash = "sha256:2bc87bdca654e719425145f5e84eaeb0080b013fdd47d65729a7d66243f4987c"}, {file = "nest_asyncio2-1.7.3.tar.gz", hash = "sha256:2e9a84d5d1efe6d020c72988d21aec569bac42d98af2ff6b9de24640c5d22a34"}, @@ -2210,7 +2210,7 @@ version = "0.2.4" description = "A shim layer for notebook traits and config" optional = false python-versions = ">=3.7" -groups = ["main"] +groups = ["dev"] files = [ {file = "notebook_shim-0.2.4-py3-none-any.whl", hash = "sha256:411a5be4e9dc882a074ccbcae671eda64cceb068767e9a3419096986560e1cef"}, {file = "notebook_shim-0.2.4.tar.gz", hash = "sha256:b4b2cfa1b65d98307ca24361f5b30fe785b53c3fd07b7a47e89acb5e6ac638cb"}, @@ -2228,7 +2228,7 @@ version = "2.5.3" description = "Fundamental package for array computing in Python" optional = false python-versions = ">=3.12" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "numpy-2.5.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cb189f09db39283b26bfd061ec16189e14f71c6755207f72a0f7540867afe5b9"}, {file = "numpy-2.5.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f59a878c33d6b88122d80d239bb3b845d58708750b0cb06a09aebb9b18ec696c"}, @@ -2319,7 +2319,7 @@ version = "26.3" description = "Core utilities for Python packages" optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["dev"] files = [ {file = "packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c"}, {file = "packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79"}, @@ -2433,7 +2433,7 @@ version = "1.5.1" description = "Utilities for writing pandoc filters in python" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -groups = ["main"] +groups = ["dev"] files = [ {file = "pandocfilters-1.5.1-py2.py3-none-any.whl", hash = "sha256:93be382804a9cdb0a7267585f157e5d1731bbe5545a85b268d6f5fe6232de2bc"}, {file = "pandocfilters-1.5.1.tar.gz", hash = "sha256:002b4a555ee4ebc03f8b66307e287fa492e4a77b4ea14d3f934328297bb4939e"}, @@ -2445,7 +2445,7 @@ version = "0.8.7" description = "A Python Parser" optional = false python-versions = ">=3.6" -groups = ["main"] +groups = ["dev"] files = [ {file = "parso-0.8.7-py2.py3-none-any.whl", hash = "sha256:a8926eb2a1b915486941fdbd31e86a4baf88fe8c210f25f2f35ecec5b574ca1c"}, {file = "parso-0.8.7.tar.gz", hash = "sha256:eaaac4c9fdd5e9e8852dc778d2d7405897ec510f2a298071453e5e3a07914bb1"}, @@ -2461,7 +2461,7 @@ version = "4.9.0" description = "Pexpect allows easy control of interactive console applications." optional = false python-versions = "*" -groups = ["main"] +groups = ["dev"] markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\"" files = [ {file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"}, @@ -2477,7 +2477,7 @@ version = "12.3.0" description = "Python Imaging Library (fork)" optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["dev"] files = [ {file = "pillow-12.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:6c0016e7b354317c4e9e525b937ac8596c38d2d232b419529b9cd7a1cd46e39a"}, {file = "pillow-12.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bcc33feacfaefce60c12fd500a277533bdc02b10a19f7f6d348763d8140bbba7"}, @@ -2582,7 +2582,7 @@ version = "4.12.3" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["dev"] files = [ {file = "platformdirs-4.12.3-py3-none-any.whl", hash = "sha256:080f3b39423b5abfca9a23d84c4e9795f54d395cd8459867a8ded44084fcd5f8"}, {file = "platformdirs-4.12.3.tar.gz", hash = "sha256:427fc0bb321ae0c5b037fa03238ca74820437be162e78b4848c4d4055b9b766c"}, @@ -2594,7 +2594,7 @@ version = "0.26.0" description = "Python client for the Prometheus monitoring system." optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["dev"] files = [ {file = "prometheus_client-0.26.0-py3-none-any.whl", hash = "sha256:fa93d06737aa02bacd05794768508bb97d2fbee28cb3bca04eaae92f0ca953d6"}, {file = "prometheus_client-0.26.0.tar.gz", hash = "sha256:04a91bcf94e2cf74a44a1a874d651a2e853ed354b6e822f3b7487751465d5c2b"}, @@ -2611,7 +2611,7 @@ version = "3.0.53" description = "Library for building powerful interactive command lines in Python" optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["dev"] files = [ {file = "prompt_toolkit-3.0.53-py3-none-any.whl", hash = "sha256:01c0891d7f9237d5e339f7d3e42cdae80b7534abb1c7c0e3352efba6231492f2"}, {file = "prompt_toolkit-3.0.53.tar.gz", hash = "sha256:9ec8a0ad96d5c56148b3f914aa79c1564c3fde5d2e6b876e7bc327e353cf8fa6"}, @@ -2626,7 +2626,7 @@ version = "7.2.2" description = "Cross-platform lib for process and system monitoring." optional = false python-versions = ">=3.6" -groups = ["main"] +groups = ["dev"] markers = "sys_platform != \"emscripten\" and sys_platform != \"cygwin\"" files = [ {file = "psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b"}, @@ -2662,7 +2662,7 @@ version = "0.7.0" description = "Run a subprocess in a pseudo terminal" optional = false python-versions = "*" -groups = ["main"] +groups = ["dev"] markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\" or os_name != \"nt\"" files = [ {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, @@ -2675,7 +2675,7 @@ version = "0.2.4" description = "Safely evaluate AST nodes without side effects" optional = false python-versions = "*" -groups = ["main"] +groups = ["dev"] files = [ {file = "pure_eval-0.2.4-py3-none-any.whl", hash = "sha256:96cae060a313cfaad51bb761278bfb0e62dc0248d9315a81173752dc546cd37a"}, {file = "pure_eval-0.2.4.tar.gz", hash = "sha256:260c2774686e651b79f8b8e7fc9d80b3599ea6a66334b47d5f4abb69fc2c0ea1"}, @@ -2690,7 +2690,7 @@ version = "3.0" description = "C parser in Python" optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["dev"] markers = "implementation_name != \"PyPy\"" files = [ {file = "pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992"}, @@ -2858,7 +2858,7 @@ version = "2.21.0" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["dev"] files = [ {file = "pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9"}, {file = "pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c"}, @@ -2873,7 +2873,7 @@ version = "3.3.3" description = "pyparsing - Classes and methods to define and execute parsing grammars" optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["dev"] files = [ {file = "pyparsing-3.3.3-py3-none-any.whl", hash = "sha256:ece8c00a69cf01b45d0b1dedabb469c90d8caf996d4fda40f147627a122849a4"}, {file = "pyparsing-3.3.3.tar.gz", hash = "sha256:928ae7e20211f3b6f3915a72f06a0cfd29ab9d24279dd6346b6b1a7146397d36"}, @@ -2888,7 +2888,7 @@ version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, @@ -2918,7 +2918,7 @@ version = "4.2.0" description = "JSON Log Formatter for the Python Logging Package" optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["dev"] files = [ {file = "python_json_logger-4.2.0-py3-none-any.whl", hash = "sha256:158a52126fcd6869e09574d2b66272666f3dc8f468c62637ef9a1fa883719cb9"}, {file = "python_json_logger-4.2.0.tar.gz", hash = "sha256:e371ebe22ec01e289850102091a2b1f6fc9e655c7f1f5f29073936756c290afa"}, @@ -2930,7 +2930,7 @@ version = "3.0.5" description = "Pseudo terminal support for Windows from Python." optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["dev"] markers = "os_name == \"nt\"" files = [ {file = "pywinpty-3.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:b467dcad72365bc2205ed8b6e694e817d71de269d46e56cfe267dfa9d3d30e1b"}, @@ -2956,7 +2956,7 @@ version = "6.0.3" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.8" -groups = ["main"] +groups = ["dev"] files = [ {file = "PyYAML-6.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f"}, {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4"}, @@ -3039,7 +3039,7 @@ version = "27.2.0" description = "Python bindings for 0MQ" optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["dev"] files = [ {file = "pyzmq-27.2.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:480dba27b145373b5e103890f17969d891bc9e86746d6b8b29dd70b0d4addc62"}, {file = "pyzmq-27.2.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:722f0a6940be1a483c81029a271d950e04dc2ff113a42e21b3d2b7a0d8e59638"}, @@ -3138,7 +3138,7 @@ version = "0.37.0" description = "JSON Referencing + Python" optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["dev"] files = [ {file = "referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231"}, {file = "referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8"}, @@ -3155,7 +3155,7 @@ version = "2.34.2" description = "Python HTTP for Humans." optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0"}, {file = "requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed"}, @@ -3177,7 +3177,7 @@ version = "0.1.4" description = "A pure python RFC3339 validator" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -groups = ["main"] +groups = ["dev"] files = [ {file = "rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa"}, {file = "rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b"}, @@ -3192,7 +3192,7 @@ version = "0.1.1" description = "Pure python rfc3986 validator" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -groups = ["main"] +groups = ["dev"] files = [ {file = "rfc3986_validator-0.1.1-py2.py3-none-any.whl", hash = "sha256:2f235c432ef459970b4306369336b9d5dbdda31b510ca1e327636e01f528bfa9"}, {file = "rfc3986_validator-0.1.1.tar.gz", hash = "sha256:3d44bde7921b3b9ec3ae4e3adca370438eccebc676456449b145d533b240d055"}, @@ -3204,7 +3204,7 @@ version = "1.1.0" description = "Helper functions to syntactically validate strings according to RFC 3987." optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["dev"] files = [ {file = "rfc3987_syntax-1.1.0-py3-none-any.whl", hash = "sha256:6c3d97604e4c5ce9f714898e05401a0445a641cfa276432b0a648c80856f6a3f"}, {file = "rfc3987_syntax-1.1.0.tar.gz", hash = "sha256:717a62cbf33cffdd16dfa3a497d81ce48a660ea691b1ddd7be710c22f00b4a0d"}, @@ -3222,7 +3222,7 @@ version = "2026.9.1" description = "Python bindings to Rust's persistent data structures (rpds)" optional = false python-versions = ">=3.11" -groups = ["main"] +groups = ["dev"] files = [ {file = "rpds_py-2026.9.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:2711d29b653b3bce48a63d18b9c6b53274669e6d6c4094dddeb4d9a0e45128b2"}, {file = "rpds_py-2026.9.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3231c4c0e521dafa5be0c9f114ee2c2ad46650836f2d72caa86801950c3e7044"}, @@ -3499,7 +3499,7 @@ version = "2.1.0" description = "Send file to trash natively under Mac OS X, Windows and Linux" optional = false python-versions = ">=3.8" -groups = ["main"] +groups = ["dev"] files = [ {file = "send2trash-2.1.0-py3-none-any.whl", hash = "sha256:0da2f112e6d6bb22de6aa6daa7e144831a4febf2a87261451c4ad849fe9a873c"}, {file = "send2trash-2.1.0.tar.gz", hash = "sha256:1c72b39f09457db3c05ce1d19158c2cbef4c32b8bedd02c155e49282b7ea7459"}, @@ -3515,7 +3515,7 @@ version = "1.17.0" description = "Python 2 and 3 compatibility utilities" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, @@ -3527,7 +3527,7 @@ version = "2.10" description = "A modern CSS selector implementation for Beautiful Soup." optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "soupsieve-2.10-py3-none-any.whl", hash = "sha256:8596eb8967d744174820280fa62b4542a2e955bfaccca73ed8a13c6eb8e9b502"}, {file = "soupsieve-2.10.tar.gz", hash = "sha256:49e9380d7d2905463583bafe285e818c7366a9ed7b3aee221c1ac79c905d8bc0"}, @@ -3539,7 +3539,7 @@ version = "0.6.3" description = "Extract data from python stack frames and tracebacks for informative displays" optional = false python-versions = "*" -groups = ["main"] +groups = ["dev"] files = [ {file = "stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695"}, {file = "stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9"}, @@ -3572,29 +3572,13 @@ typing-extensions = {version = ">=4.10.0", markers = "python_version < \"3.13\"" [package.extras] full = ["httpx (>=0.27.0,<0.29.0)", "httpx2 (>=2.0.0)", "itsdangerous", "jinja2", "opentelemetry-api", "python-multipart (>=0.0.18)", "pyyaml"] -[[package]] -name = "tenacity" -version = "9.1.4" -description = "Retry code until it succeeds" -optional = false -python-versions = ">=3.10" -groups = ["main"] -files = [ - {file = "tenacity-9.1.4-py3-none-any.whl", hash = "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55"}, - {file = "tenacity-9.1.4.tar.gz", hash = "sha256:adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a"}, -] - -[package.extras] -doc = ["reno", "sphinx"] -test = ["pytest", "tornado (>=4.5)", "typeguard"] - [[package]] name = "terminado" version = "0.18.1" description = "Tornado websocket backend for the Xterm.js Javascript terminal emulator library." optional = false python-versions = ">=3.8" -groups = ["main"] +groups = ["dev"] files = [ {file = "terminado-0.18.1-py3-none-any.whl", hash = "sha256:a4468e1b37bb318f8a86514f65814e1afc977cf29b3992a4500d9dd305dcceb0"}, {file = "terminado-0.18.1.tar.gz", hash = "sha256:de09f2c4b85de4765f7714688fff57d3e75bad1f909b589fde880460c753fd2e"}, @@ -3628,7 +3612,7 @@ version = "1.5.1" description = "A tiny CSS parser" optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["dev"] files = [ {file = "tinycss2-1.5.1-py3-none-any.whl", hash = "sha256:3415ba0f5839c062696996998176c4a3751d18b7edaaeeb658c9ce21ec150661"}, {file = "tinycss2-1.5.1.tar.gz", hash = "sha256:d339d2b616ba90ccce58da8495a78f46e55d4d25f9fd71dfd526f07e7d53f957"}, @@ -3647,7 +3631,7 @@ version = "6.5.10" description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["dev"] files = [ {file = "tornado-6.5.10-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9261783640e23258694a9ff0795df430a5a7b0a651d3dd53dd0969ad6be16da7"}, {file = "tornado-6.5.10-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:83e6cf438b106c6b3852d70960967bb1b70c87438050dca0981e4b9aa751a4c1"}, @@ -3667,7 +3651,7 @@ version = "5.16.1" description = "Traitlets Python configuration system" optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["dev"] files = [ {file = "traitlets-5.16.1-py3-none-any.whl", hash = "sha256:f775618166caa0396c8e337099240f2bd3e5e917d203b2e6fbe21a58d3cb1f6b"}, {file = "traitlets-5.16.1.tar.gz", hash = "sha256:ed900c2b631aa3a112811139fa97b8d2c3bad5e989656bba4b7e52c7852c18c1"}, @@ -3683,7 +3667,7 @@ version = "4.16.0" description = "Backported and Experimental Type Hints for Python 3.9+" optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8"}, {file = "typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5"}, @@ -3710,11 +3694,12 @@ version = "2026.5" description = "Provider of IANA time zone data" optional = false python-versions = ">=2" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "tzdata-2026.5-py2.py3-none-any.whl", hash = "sha256:b683bd1b6659ddcd810ff02ad09ba821d4bf1065072805063eb35c49617905ac"}, {file = "tzdata-2026.5.tar.gz", hash = "sha256:8cc73c0a0bfca7dbfa59235d60b2eff82231dee33f53d206db1acd9173cfc0a7"}, ] +markers = {main = "sys_platform == \"win32\" or sys_platform == \"emscripten\""} [[package]] name = "uri-template" @@ -3722,7 +3707,7 @@ version = "1.3.0" description = "RFC 6570 URI Template Processor" optional = false python-versions = ">=3.7" -groups = ["main"] +groups = ["dev"] files = [ {file = "uri-template-1.3.0.tar.gz", hash = "sha256:0e00f8eb65e18c7de20d595a14336e9f337ead580c70934141624b6d1ffdacc7"}, {file = "uri_template-1.3.0-py3-none-any.whl", hash = "sha256:a44a133ea12d44a0c0f06d7d42a52d71282e77e2f937d8abd5655b8d56fc1363"}, @@ -3737,7 +3722,7 @@ version = "2.8.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "urllib3-2.8.0-py3-none-any.whl", hash = "sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3"}, {file = "urllib3-2.8.0.tar.gz", hash = "sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63"}, @@ -3774,7 +3759,7 @@ version = "0.9.2" description = "Measures the displayed width of unicode strings in a terminal" optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["dev"] files = [ {file = "wcwidth-0.9.2-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:7ef5a940bd5e30bac6e721f1a48fce0cd7bb3ece19e9c5d139e72c76c35cfd07"}, {file = "wcwidth-0.9.2-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:ae0800c5339423cc53d33a266ad264b42ba8aaa16d4464f6e6b1bee607f50b17"}, @@ -3813,7 +3798,7 @@ version = "25.10.0" description = "A library for working with the color formats defined by HTML and CSS." optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["dev"] files = [ {file = "webcolors-25.10.0-py3-none-any.whl", hash = "sha256:032c727334856fc0b968f63daa252a1ac93d33db2f5267756623c210e57a4f1d"}, {file = "webcolors-25.10.0.tar.gz", hash = "sha256:62abae86504f66d0f6364c2a8520de4a0c47b80c03fc3a5f1815fedbef7c19bf"}, @@ -3825,7 +3810,7 @@ version = "0.6.1" description = "Character encoding aliases for legacy web content" optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "webencodings-0.6.1-py3-none-any.whl", hash = "sha256:7fab6269c8bf237c657876b52058ccb182e861518d1c695c1a9aaa8c1c105d5b"}, {file = "webencodings-0.6.1.tar.gz", hash = "sha256:565f9ad031c702dae404e27a099e3e09186a3ab1b9520f06d215502b651fd910"}, @@ -3841,7 +3826,7 @@ version = "1.9.2" description = "WebSocket client for Python with low level API options" optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["dev"] files = [ {file = "websocket_client-1.9.2-py3-none-any.whl", hash = "sha256:e1a673830a9c7bfa47b1cd3d5e4178f4c9651d80a4eab02c9c23a1c3ec6250ce"}, {file = "websocket_client-1.9.2.tar.gz", hash = "sha256:0fcb57545848be86992e128218fd96dd87a6769ffdb1a968dff79632b85604d0"}, @@ -3852,24 +3837,7 @@ docs = ["Sphinx (>=6.0)", "myst-parser (>=2.0.0)", "sphinx_rtd_theme (>=1.1.0)"] optional = ["python-socks", "wsaccel"] test = ["pytest", "websockets"] -[[package]] -name = "wikipedia-api" -version = "0.16.0" -description = "Python Wrapper for Wikipedia" -optional = false -python-versions = ">=3.10" -groups = ["main"] -files = [ - {file = "wikipedia_api-0.16.0-py3-none-any.whl", hash = "sha256:60da19a8e380c72f7dc259c85aced764c671890ec8708ab6a7afd24c553bf843"}, - {file = "wikipedia_api-0.16.0.tar.gz", hash = "sha256:27967d058cc26d94752e22f81c3a999bbd3ac3fe2dbac8838b53c56db9cdd8ef"}, -] - -[package.dependencies] -click = "8.5.0" -httpx = "0.28.1" -tenacity = "9.1.4" - [metadata] lock-version = "2.1" python-versions = "^3.12" -content-hash = "8abe2b7cd92853b9be76fec2e8d3f9bd8f3ea45a4da3b8e1f621b975076a0947" +content-hash = "a382e6762d608bd4e4faf944236adb4258b03065be27c24676e4f0b2e2b8aa71" diff --git a/pyproject.toml b/pyproject.toml index 262b5b9..6e92caa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,6 @@ dependencies = [ "requests (>=2.34.2,<3.0.0)", "pandas (>=3.0.6,<4.0.0)", "python-dotenv (>=1.2.4,<2.0.0)", - "wikipedia-api (>=0.16.0,<0.17.0)", "lxml (>=6.1.3,<7.0.0)", "html5lib (>=1.1,<2.0)", "beautifulsoup4 (>=4.15.0,<5.0.0)", @@ -19,10 +18,16 @@ dependencies = [ "fastapi (>=0.142.3,<0.143.0)", "scikit-learn (>=1.9.1,<2.0.0)", "uvicorn (>=0.54.0,<0.55.0)", - "jupyterlab (>=4.6.4,<5.0.0)", - "matplotlib (>=3.11.2,<4.0.0)", +# "jupyterlab (>=4.6.4,<5.0.0)", +# "matplotlib (>=3.11.2,<4.0.0)", ] +[tool.poetry.group.dev.dependencies] +jupyterlab = "^4.6.4" +matplotlib = "^3.11.2" + +[tool.poetry] +package-mode = false [build-system] requires = ["poetry-core>=2.0.0,<3.0.0"]