diff --git a/eu_fact_force/exploration/cytoscape/README.md b/eu_fact_force/exploration/cytoscape/README.md new file mode 100644 index 0000000..5fc0a4d --- /dev/null +++ b/eu_fact_force/exploration/cytoscape/README.md @@ -0,0 +1,20 @@ +# Exploration - Dash Cytoscape + +This folder contains a first version of a local Dash app to explore Cytoscape capabilities. + +# Repo structure +- `app.py`: the main app file. +- `assets/`: the app asset folder, with custom css, icon and plotly template. +- `utils/`: app utility files, including d4g colors and random graph generator. + + +## Setup +- Install `graph` group depedencies using `uv sync --group graph`. +- Start app from here with `pyhon app.py`. +- Visit `http://127.0.0.1:8050/` to see the app on your local. + +## App overview +- This app contains a search bar with an "Search" button to simulate search. +- On search button click, a random network graph will be generated. +- Clicking on a node in the chart will open an offcanevas displaying node metadata. +- A list of all nodes in the graph will also be generated, with node metadatWHen a in each element. \ No newline at end of file diff --git a/eu_fact_force/exploration/cytoscape/app.py b/eu_fact_force/exploration/cytoscape/app.py new file mode 100644 index 0000000..b0b918a --- /dev/null +++ b/eu_fact_force/exploration/cytoscape/app.py @@ -0,0 +1,254 @@ +from dash import Dash, dcc, html +from dash.dependencies import Input, Output, State +from dash.exceptions import PreventUpdate +import dash_bootstrap_components as dbc +import plotly.io as pio +import plotly.graph_objects as go +import dash_cytoscape as cyto + +import json + +from utils.colors import AppColors +from utils.graph import RandomGraphGenerator + +# Plotly template +with open("assets/template.json", "r") as f: + debate_template = json.load(f) +pio.templates["app_template"] = go.layout.Template(debate_template) +pio.templates.default = "app_template" + +# Dash app +app = Dash( + __name__, + suppress_callback_exceptions=True, + external_stylesheets=["custom.css", dbc.themes.BOOTSTRAP], +) + +# Dash params +DASHBOARD_NAME = "EU Fact Force" + +# Custom dash app tab and logo +app.title = DASHBOARD_NAME +app._favicon = "icon.png" + +# Graph generator +generator = RandomGraphGenerator() + +# Header +header = html.Div( + dbc.Row( + dbc.Col( + html.Div( + [ + html.Img(src="assets/icon.png", alt="image", height=50), + html.H1( + DASHBOARD_NAME, + style={ + "color": AppColors.blue, + "font-weight": "bold", + "margin": "0", + "padding": "0", + }, + ), + ], + style={ + "display": "flex", + "alignItems": "center", + "gap": "0px", + }, + ), + width=12, + ), + className="g-0", + ), + style={ + "padding": "1rem", + "background-color": AppColors.green, + "position": "fixed", + "width": "100%", + "zIndex": 1000, + }, +) +# Content +search_bar = html.Div( + children=[ + dbc.Row( + [ + dbc.Col( + dbc.Input( + id="search-input", + placeholder="Naratif de désinformation...", + style={"overflow": "hidden"}, + ) + ), + dbc.Col( + dbc.Button( + "Rechercher", + id="search-button", + color="primary", + className="me-1", + n_clicks=0, + disabled=True, + ), + width="auto", + ), + ], + align="center", + ) + ], + id="search", + style={ + "border-radius": "15px", + "padding": "20px", + "background-color": AppColors.white, + }, +) + +graph = html.Div( + children=cyto.Cytoscape( + id="graph-cytoscape", + stylesheet=generator.stylesheet, + layout={"name": "cose"}, + style={"width": "100%", "height": "400px"}, + ), + id="graph", + style={ + "border-radius": "15px", + "padding": "20px", + "background-color": AppColors.white, + "display": "none", + }, +) + +list_elements = html.Div( + id="list", + style={ + "border-radius": "15px", + "padding": "20px", + "background-color": AppColors.white, + "display": "none", + }, +) + +offcanevas = dbc.Offcanvas( + id="offcanvas", + title="Selectionné", + is_open=False, + placement="end", + style={"width": "50%"}, +) + + +content = html.Div( + [search_bar, html.Br(), graph, html.Br(), list_elements, offcanevas], + style={ + "margin-left": "1rem", + "margin-right": "1rem", + "padding": "1rem", + "padding-top": "120px", + }, + id="page-content", +) + + +# Layout +app.layout = html.Div([dcc.Location(id="url", refresh=False), header, content]) + + +# -------------------- +# Callbacks +# -------------------- + + +# Callback search button activate +@app.callback( + Output("search-button", "disabled"), + inputs=[Input("search-input", "value"), Input("graph", "children")], +) +def activate_search_buton(search_text, graph): + if search_text is None or search_text == "": + return True + else: + return False + + +# Callback update graph +@app.callback( + [ + Output("graph-cytoscape", "elements"), + Output("list", "children"), + Output("graph", "style"), + Output("list", "style"), + Output("search-input", "value"), + ], + inputs=[Input("search-button", "n_clicks")], + state=[State("search-input", "value")], + prevent_updates=True, +) +def update_graph(n_clicks, search_text): + if n_clicks > 0: + graph_elements = generator.get_graph_data() + list_elements = [x["data"] for x in graph_elements if "id" in x["data"]] + list_elements = sorted(list_elements, key=lambda x: x["id"]) + return [ + graph_elements, + dbc.Accordion( + [ + dbc.AccordionItem( + dcc.Markdown( + "\n".join( + [f"- {key.capitalize()} : __{x[key]}__" for key in x] + ) + ), + title=x["label"], + ) + for x in list_elements + ], + start_collapsed=True, + ), + { + "border-radius": "15px", + "padding": "20px", + "background-color": AppColors.white, + "display": "block", + }, + { + "border-radius": "15px", + "padding": "20px", + "background-color": AppColors.white, + "display": "block", + }, + "", + ] + else: + raise PreventUpdate + + +# Callback show selected element +@app.callback( + [ + Output("offcanvas", "is_open"), + Output("offcanvas", "children"), + ], + inputs=[Input("graph-cytoscape", "tapNodeData")], + state=[State("offcanvas", "is_open")], + prevent_initial_call=True, +) +def toggle_offcanvas(node_data, is_open): + if node_data: + return [ + not is_open, + dcc.Markdown( + "\n".join( + [ + f"- {key.capitalize()} : __{node_data[key]}__" + for key in node_data + if key != "timeStamp" + ] + ) + ), + ] + + +if __name__ == "__main__": + app.run(debug=True) diff --git a/eu_fact_force/exploration/cytoscape/assets/custom.css b/eu_fact_force/exploration/cytoscape/assets/custom.css new file mode 100644 index 0000000..77c6827 --- /dev/null +++ b/eu_fact_force/exploration/cytoscape/assets/custom.css @@ -0,0 +1,29 @@ +/* Colors */ +:root { + /* Principal Colors */ + --color-0: #CBDF40; + --color-1: #36C3D7; + --color-2: #F5A414; + --color-gray: #F1F1F1; +} + + +/* Body and text style */ +body { + font-family: "Helvetica", sans-serif; + background-color: var(--color-gray); + font-size: 14px; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + color: black; +} + +hr { + color: black; +} \ No newline at end of file diff --git a/eu_fact_force/exploration/cytoscape/assets/icon.png b/eu_fact_force/exploration/cytoscape/assets/icon.png new file mode 100644 index 0000000..771afe4 Binary files /dev/null and b/eu_fact_force/exploration/cytoscape/assets/icon.png differ diff --git a/eu_fact_force/exploration/cytoscape/assets/template.json b/eu_fact_force/exploration/cytoscape/assets/template.json new file mode 100644 index 0000000..32fedd5 --- /dev/null +++ b/eu_fact_force/exploration/cytoscape/assets/template.json @@ -0,0 +1,46 @@ +{ + "layout": { + "title": { + "x": 0.02 + }, + "colorway": [ + "#CBDF40", + "#36C3D7", + "#F5A414" + ], + "font": { + "size": 16, + "color": "black" + }, + "plot_bgcolor": "white", + "paper_bgcolor": "rgb(0,0,0,0)", + "xaxis": { + "tickfont": { + "size": 12 + }, + "title": { + "font": { + "size": 14 + } + }, + "showgrid": false + }, + "yaxis": { + "tickfont": { + "size": 12 + }, + "title": { + "font": { + "size": 14 + } + }, + "showgrid": false + }, + "legend": { + "font": { + "size": 12 + }, + "bgcolor": "white" + } + } +} \ No newline at end of file diff --git a/eu_fact_force/exploration/cytoscape/utils/colors.py b/eu_fact_force/exploration/cytoscape/utils/colors.py new file mode 100644 index 0000000..95ce120 --- /dev/null +++ b/eu_fact_force/exploration/cytoscape/utils/colors.py @@ -0,0 +1,6 @@ +class AppColors: + green = "#CBDF40" + blue = "#36C3D7" + orange = "#F5A414" + grey = "#F1F1F1" + white = "#FFFFFF" diff --git a/eu_fact_force/exploration/cytoscape/utils/graph.py b/eu_fact_force/exploration/cytoscape/utils/graph.py new file mode 100644 index 0000000..8a717ec --- /dev/null +++ b/eu_fact_force/exploration/cytoscape/utils/graph.py @@ -0,0 +1,65 @@ +import random + +from .colors import AppColors + + +class RandomGraphGenerator: + def __init__(self): + self.n_min_paper_nodes = 5 + self.n_max_paper_nodes = 10 + self.nodes_paper = [ + {"data": {"id": f"node_paper_{i}", "label": f"Paper {i}", "type": "paper"}} + for i in range(self.n_max_paper_nodes) + ] + self.nodes_journal = [ + {"data": {"id": "node_journal_0", "label": "Journal A", "type": "journal"}}, + {"data": {"id": "node_journal_1", "label": "Journal B", "type": "journal"}}, + {"data": {"id": "node_journal_2", "label": "Journal C", "type": "journal"}}, + ] + self.stylesheet = [ + { + "selector": "node", + "style": { + "label": "data(label)", + "text-valign": "center", + "color": "black", + }, + }, + { + "selector": 'node[type="paper"]', + "style": { + "background-color": AppColors.blue, + }, + }, + { + "selector": 'node[type="journal"]', + "style": { + "background-color": AppColors.green, + }, + }, + { + "selector": "edge", + "style": { + "width": 2, + "line-color": "black", + }, + }, + ] + + def get_graph_data(self): + nodes = random.sample( + self.nodes_paper, + random.randint(self.n_min_paper_nodes, self.n_max_paper_nodes), + ) + edges = [] + for source_node in nodes: + target_node = random.sample(self.nodes_journal, 1)[0] + edges.append( + { + "data": { + "source": source_node["data"]["id"], + "target": target_node["data"]["id"], + } + } + ) + return nodes + self.nodes_journal + edges diff --git a/pyproject.toml b/pyproject.toml index dc3cd89..aae849f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,11 @@ dev = [ "ruff>=0.15.0", "seaborn>=0.13.2", ] +graph = [ + "dash>=4.0.0", + "dash-bootstrap-components>=2.0.4", + "dash-cytoscape>=1.0.2", +] parsing = [ "docling>=2.73.1", "docling-hierarchical-pdf>=0.1.3", diff --git a/uv.lock b/uv.lock index ee70200..3e83b5d 100644 --- a/uv.lock +++ b/uv.lock @@ -32,6 +32,11 @@ dev = [ { name = "ruff" }, { name = "seaborn" }, ] +graph = [ + { name = "dash" }, + { name = "dash-bootstrap-components" }, + { name = "dash-cytoscape" }, +] parsing = [ { name = "docling" }, { name = "docling-hierarchical-pdf" }, @@ -65,6 +70,11 @@ dev = [ { name = "ruff", specifier = ">=0.15.0" }, { name = "seaborn", specifier = ">=0.13.2" }, ] +graph = [ + { name = "dash", specifier = ">=4.0.0" }, + { name = "dash-bootstrap-components", specifier = ">=2.0.4" }, + { name = "dash-cytoscape", specifier = ">=1.0.2" }, +] parsing = [ { name = "docling", specifier = ">=2.73.1" }, { name = "docling-hierarchical-pdf", specifier = ">=0.1.3" }, @@ -403,6 +413,15 @@ css = [ { name = "tinycss2" }, ] +[[package]] +name = "blinker" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" }, +] + [[package]] name = "boto3" version = "1.42.59" @@ -703,6 +722,47 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, ] +[[package]] +name = "dash" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flask" }, + { name = "importlib-metadata" }, + { name = "nest-asyncio" }, + { name = "plotly" }, + { name = "requests" }, + { name = "retrying" }, + { name = "setuptools" }, + { name = "typing-extensions" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/20/dd/3aed9bfd81dfd8f44b3a5db0583080ac9470d5e92ee134982bd5c69e286e/dash-4.0.0.tar.gz", hash = "sha256:c5f2bca497af288f552aea3ae208f6a0cca472559003dac84ac21187a1c3a142", size = 6943263, upload-time = "2026-02-03T19:42:27.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/8c/dd63d210b28a7589f4bc1e84880525368147425c717d12834ab562f52d14/dash-4.0.0-py3-none-any.whl", hash = "sha256:e36b4b4eae9e1fa4136bf4f1450ed14ef76063bc5da0b10f8ab07bd57a7cb1ab", size = 7247521, upload-time = "2026-02-03T19:42:25.01Z" }, +] + +[[package]] +name = "dash-bootstrap-components" +version = "2.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dash" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cc/d4/5b7da808ff5acb3a6ca702f504d8ef05bc7d4c475b18dadefd783b1120c3/dash_bootstrap_components-2.0.4.tar.gz", hash = "sha256:c3206c0923774bbc6a6ddaa7822b8d9aa5326b0d3c1e7cd795cc975025fe2484", size = 115599, upload-time = "2025-08-20T19:42:09.449Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/38/1efeec8b4d741c09ccd169baf8a00c07a0176b58e418d4cd0c30dffedd22/dash_bootstrap_components-2.0.4-py3-none-any.whl", hash = "sha256:767cf0084586c1b2b614ccf50f79fe4525fdbbf8e3a161ed60016e584a14f5d1", size = 204044, upload-time = "2025-08-20T19:42:07.928Z" }, +] + +[[package]] +name = "dash-cytoscape" +version = "1.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dash" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ea/b7/0d511af853024241dc3192bea77e4753ea606187bd2dd777a8209a5b01bb/dash_cytoscape-1.0.2.tar.gz", hash = "sha256:a61019d2184d63a2b3b5c06d056d3b867a04223a674cc3c7cf900a561a9a59aa", size = 3992593, upload-time = "2024-07-15T11:39:06.185Z" } + [[package]] name = "dataclasses-json" version = "0.6.7" @@ -1033,6 +1093,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/18/79/1b8fa1bb3568781e84c9200f951c735f3f157429f44be0495da55894d620/filetype-1.2.0-py2.py3-none-any.whl", hash = "sha256:7ce71b6880181241cf7ac8697a2f1eb6a8bd9b429f7ad6d27b8db9ba5f1c2d25", size = 19970, upload-time = "2022-11-02T17:34:01.425Z" }, ] +[[package]] +name = "flask" +version = "3.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "blinker" }, + { name = "click" }, + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "markupsafe" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/00/35d85dcce6c57fdc871f3867d465d780f302a175ea360f62533f12b27e2b/flask-3.1.3.tar.gz", hash = "sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb", size = 759004, upload-time = "2026-02-19T05:00:57.678Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/9c/34f6962f9b9e9c71f6e5ed806e0d0ff03c9d1b0b2340088a0cf4bce09b18/flask-3.1.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c", size = 103424, upload-time = "2026-02-19T05:00:56.027Z" }, +] + [[package]] name = "fonttools" version = "4.61.1" @@ -1374,6 +1451,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, ] +[[package]] +name = "importlib-metadata" +version = "8.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, +] + [[package]] name = "inflection" version = "0.5.1" @@ -1477,6 +1566,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl", hash = "sha256:b2904c2a4228c3d44f409c8ae8e2370eb21a26f7ac2ec5446df141dde3452042", size = 11321, upload-time = "2020-11-01T10:59:58.02Z" }, ] +[[package]] +name = "itsdangerous" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, +] + [[package]] name = "jedi" version = "0.19.2" @@ -2423,6 +2521,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, ] +[[package]] +name = "narwhals" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/b4/02a8add181b8d2cd5da3b667cd102ae536e8c9572ab1a130816d70a89edb/narwhals-2.18.0.tar.gz", hash = "sha256:1de5cee338bc17c338c6278df2c38c0dd4290499fcf70d75e0a51d5f22a6e960", size = 620222, upload-time = "2026-03-10T15:51:27.14Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/75/0b4a10da17a44cf13567d08a9c7632a285297e46253263f1ae119129d10a/narwhals-2.18.0-py3-none-any.whl", hash = "sha256:68378155ee706ac9c5b25868ef62ecddd62947b6df7801a0a156bc0a615d2d0d", size = 444865, upload-time = "2026-03-10T15:51:24.085Z" }, +] + [[package]] name = "nbclient" version = "0.10.4" @@ -2976,6 +3083,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/48/31/05e764397056194206169869b50cf2fee4dbbbc71b344705b9c0d878d4d8/platformdirs-4.9.2-py3-none-any.whl", hash = "sha256:9170634f126f8efdae22fb58ae8a0eaa86f38365bc57897a6c4f781d1f5875bd", size = 21168, upload-time = "2026-02-16T03:56:08.891Z" }, ] +[[package]] +name = "plotly" +version = "6.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "narwhals" }, + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/24/fb/41efe84970cfddefd4ccf025e2cbfafe780004555f583e93dba3dac2cdef/plotly-6.6.0.tar.gz", hash = "sha256:b897f15f3b02028d69f755f236be890ba950d0a42d7dfc619b44e2d8cea8748c", size = 7027956, upload-time = "2026-03-02T21:10:25.321Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/d2/c6e44dba74f17c6216ce1b56044a9b93a929f1c2d5bdaff892512b260f5e/plotly-6.6.0-py3-none-any.whl", hash = "sha256:8d6daf0f87412e0c0bfe72e809d615217ab57cc715899a1e5145135a7800d1d0", size = 9910315, upload-time = "2026-03-02T21:10:18.131Z" }, +] + [[package]] name = "pluggy" version = "1.6.0" @@ -3906,6 +4026,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, ] +[[package]] +name = "retrying" +version = "1.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c8/5a/b17e1e257d3e6f2e7758930e1256832c9ddd576f8631781e6a072914befa/retrying-1.4.2.tar.gz", hash = "sha256:d102e75d53d8d30b88562d45361d6c6c934da06fab31bd81c0420acb97a8ba39", size = 11411, upload-time = "2025-08-03T03:35:25.189Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/f3/6cd296376653270ac1b423bb30bd70942d9916b6978c6f40472d6ac038e7/retrying-1.4.2-py3-none-any.whl", hash = "sha256:bbc004aeb542a74f3569aeddf42a2516efefcdaff90df0eb38fbfbf19f179f59", size = 10859, upload-time = "2025-08-03T03:35:23.829Z" }, +] + [[package]] name = "rfc3339-validator" version = "0.1.4" @@ -4961,6 +5090,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef", size = 82616, upload-time = "2025-10-07T21:16:34.951Z" }, ] +[[package]] +name = "werkzeug" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/f1/ee81806690a87dab5f5653c1f146c92bc066d7f4cebc603ef88eb9e13957/werkzeug-3.1.6.tar.gz", hash = "sha256:210c6bede5a420a913956b4791a7f4d6843a43b6fcee4dfa08a65e93007d0d25", size = 864736, upload-time = "2026-02-19T15:17:18.884Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/ec/d58832f89ede95652fd01f4f24236af7d32b70cab2196dfcc2d2fd13c5c2/werkzeug-3.1.6-py3-none-any.whl", hash = "sha256:7ddf3357bb9564e407607f988f683d72038551200c704012bb9a4c523d42f131", size = 225166, upload-time = "2026-02-19T15:17:17.475Z" }, +] + [[package]] name = "widgetsnbextension" version = "4.0.15" @@ -5136,3 +5277,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/51/47/3fa2286c3cb162c71cdb34c4224d5745a1ceceb391b2bd9b19b668a8d724/yarl-1.23.0-cp314-cp314t-win_arm64.whl", hash = "sha256:44bb7bef4ea409384e3f8bc36c063d77ea1b8d4a5b2706956c0d6695f07dcc25", size = 86041, upload-time = "2026-03-01T22:07:49.026Z" }, { url = "https://files.pythonhosted.org/packages/69/68/c8739671f5699c7dc470580a4f821ef37c32c4cb0b047ce223a7f115757f/yarl-1.23.0-py3-none-any.whl", hash = "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f", size = 48288, upload-time = "2026-03-01T22:07:51.388Z" }, ] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, +]