UI improvements + add axis orientation test
- Rename app to 'Motief: de stematlas' in Home.py - Remove PCA variance caption from compass tab - Hardcode db_path and window_size; remove sidebar inputs - Change trajectories default to [CDA, D66, VVD] - Move quiz to pages/1_Stemwijzer.py; wrap in st.form - Remove quiz tab from main explorer - Add pytest dev dep + fix test fixtures (_load_mp_vectors_for_window) - Add test_pca_axis_orientation with proper PCA variance dominance
This commit is contained in:
@@ -7,7 +7,7 @@ brief descriptions of and links to the two sub-pages.
|
|||||||
import streamlit as st
|
import streamlit as st
|
||||||
|
|
||||||
st.set_page_config(
|
st.set_page_config(
|
||||||
page_title="StemAtlas",
|
page_title="Motief: de stematlas",
|
||||||
page_icon="🗺️",
|
page_icon="🗺️",
|
||||||
layout="centered",
|
layout="centered",
|
||||||
initial_sidebar_state="expanded",
|
initial_sidebar_state="expanded",
|
||||||
@@ -15,9 +15,9 @@ st.set_page_config(
|
|||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
st.title("🗺️ StemAtlas")
|
st.title("🗺️ Motief: de stematlas")
|
||||||
st.markdown(
|
st.markdown(
|
||||||
"**StemAtlas** brengt de Nederlandse Tweede Kamer in kaart op basis van "
|
"**Motief** brengt de Nederlandse Tweede Kamer in kaart op basis van "
|
||||||
"echte stemmingen over moties. Gebruik de Stemwijzer om te ontdekken welke "
|
"echte stemmingen over moties. Gebruik de Stemwijzer om te ontdekken welke "
|
||||||
"partij het beste bij jouw standpunten past, of verken de politieke ruimte "
|
"partij het beste bij jouw standpunten past, of verken de politieke ruimte "
|
||||||
"zelf in de Explorer."
|
"zelf in de Explorer."
|
||||||
|
|||||||
+17
-32
@@ -840,14 +840,6 @@ def build_compass_tab(db_path: str, window_size: str) -> None:
|
|||||||
with col1:
|
with col1:
|
||||||
st.plotly_chart(fig, use_container_width=True)
|
st.plotly_chart(fig, use_container_width=True)
|
||||||
|
|
||||||
# Axis info
|
|
||||||
if axis_def:
|
|
||||||
evr = axis_def.get("explained_variance_ratio", [])
|
|
||||||
if evr:
|
|
||||||
st.caption(
|
|
||||||
f"PCA verklaarde variantie: as 1 = {evr[0] * 100:.1f}%, as 2 = {evr[1] * 100:.1f}%"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Tab 2: Partij Trajectories
|
# Tab 2: Partij Trajectories
|
||||||
@@ -888,9 +880,10 @@ def build_trajectories_tab(db_path: str, window_size: str) -> None:
|
|||||||
|
|
||||||
all_parties_sorted = sorted(all_parties)
|
all_parties_sorted = sorted(all_parties)
|
||||||
|
|
||||||
# Default: prefer known major parties over the automatic "appeared in most windows"
|
# Default: show CDA, D66, VVD — the three parties that span the political centre
|
||||||
# heuristic, which would exclude newer parties like NSC that only have 4 windows.
|
default_parties = [p for p in ["CDA", "D66", "VVD"] if p in all_parties]
|
||||||
default_parties = [p for p in KNOWN_MAJOR_PARTIES if p in all_parties]
|
if not default_parties:
|
||||||
|
default_parties = [p for p in KNOWN_MAJOR_PARTIES if p in all_parties]
|
||||||
if not default_parties:
|
if not default_parties:
|
||||||
default_parties = all_parties_sorted[:6]
|
default_parties = all_parties_sorted[:6]
|
||||||
|
|
||||||
@@ -1541,7 +1534,8 @@ def build_mp_quiz_tab(db_path: str) -> None:
|
|||||||
st.session_state["mp_quiz_asked"] = []
|
st.session_state["mp_quiz_asked"] = []
|
||||||
st.rerun()
|
st.rerun()
|
||||||
|
|
||||||
# main question loop (single question per render)
|
# main question loop (single question per render, wrapped in a form to avoid
|
||||||
|
# premature reruns when the user changes the radio selection)
|
||||||
next_mid = _next_motion_id()
|
next_mid = _next_motion_id()
|
||||||
if next_mid is None:
|
if next_mid is None:
|
||||||
st.info("Geen nieuwe vragen beschikbaar om kandidaten te scheiden.")
|
st.info("Geen nieuwe vragen beschikbaar om kandidaten te scheiden.")
|
||||||
@@ -1557,14 +1551,15 @@ def build_mp_quiz_tab(db_path: str) -> None:
|
|||||||
if motion_row.get("layman_explanation"):
|
if motion_row.get("layman_explanation"):
|
||||||
st.info(motion_row.get("layman_explanation"))
|
st.info(motion_row.get("layman_explanation"))
|
||||||
|
|
||||||
choice = st.radio(
|
with st.form(key=f"mp_quiz_form_{next_mid}"):
|
||||||
"Wat zou jij stemmen?",
|
choice = st.radio(
|
||||||
options=["Voor", "Tegen", "Onthouden", "Geen stem"],
|
"Wat zou jij stemmen?",
|
||||||
index=3,
|
options=["Voor", "Tegen", "Onthouden", "Geen stem"],
|
||||||
key=f"mp_quiz_choice_{next_mid}",
|
index=3,
|
||||||
)
|
)
|
||||||
|
submitted = st.form_submit_button("Beantwoord en verder")
|
||||||
|
|
||||||
if st.button("Beantwoord en verder", key=f"mp_quiz_submit_{next_mid}"):
|
if submitted:
|
||||||
st.session_state["mp_quiz_votes"][str(next_mid)] = choice
|
st.session_state["mp_quiz_votes"][str(next_mid)] = choice
|
||||||
st.session_state["mp_quiz_asked"].append(next_mid)
|
st.session_state["mp_quiz_asked"].append(next_mid)
|
||||||
st.rerun()
|
st.rerun()
|
||||||
@@ -1617,13 +1612,8 @@ def run_app() -> None:
|
|||||||
|
|
||||||
# Sidebar
|
# Sidebar
|
||||||
st.sidebar.title("Instellingen")
|
st.sidebar.title("Instellingen")
|
||||||
db_path = st.sidebar.text_input("DuckDB pad", value="data/motions.db")
|
db_path = "data/motions.db"
|
||||||
window_size = st.sidebar.radio(
|
window_size = "annual"
|
||||||
"Venstergrootte",
|
|
||||||
options=["annual", "quarterly"],
|
|
||||||
format_func=lambda x: "Per jaar" if x == "annual" else "Per kwartaal",
|
|
||||||
index=0,
|
|
||||||
)
|
|
||||||
show_rejected = st.sidebar.checkbox("Toon verworpen moties", value=False)
|
show_rejected = st.sidebar.checkbox("Toon verworpen moties", value=False)
|
||||||
|
|
||||||
# About section
|
# About section
|
||||||
@@ -1649,12 +1639,11 @@ def run_app() -> None:
|
|||||||
"📈 Trajectories",
|
"📈 Trajectories",
|
||||||
"🔍 Motie Zoeken",
|
"🔍 Motie Zoeken",
|
||||||
"📋 Motie Browser",
|
"📋 Motie Browser",
|
||||||
"🧑⚖️ Welk tweede kamerlid ben jij?",
|
|
||||||
"🔬 SVD Components",
|
"🔬 SVD Components",
|
||||||
]
|
]
|
||||||
|
|
||||||
if hasattr(st, "tabs") and callable(getattr(st, "tabs")):
|
if hasattr(st, "tabs") and callable(getattr(st, "tabs")):
|
||||||
tab1, tab2, tab3, tab4, tab5, tab6 = st.tabs(tab_labels)
|
tab1, tab2, tab3, tab4, tab5 = st.tabs(tab_labels)
|
||||||
with tab1:
|
with tab1:
|
||||||
build_compass_tab(db_path, window_size)
|
build_compass_tab(db_path, window_size)
|
||||||
with tab2:
|
with tab2:
|
||||||
@@ -1664,8 +1653,6 @@ def run_app() -> None:
|
|||||||
with tab4:
|
with tab4:
|
||||||
build_browser_tab(db_path, show_rejected)
|
build_browser_tab(db_path, show_rejected)
|
||||||
with tab5:
|
with tab5:
|
||||||
build_mp_quiz_tab(db_path)
|
|
||||||
with tab6:
|
|
||||||
build_svd_components_tab(db_path)
|
build_svd_components_tab(db_path)
|
||||||
else:
|
else:
|
||||||
# Fallback for environments where `st.tabs` is not available: use a radio selector
|
# Fallback for environments where `st.tabs` is not available: use a radio selector
|
||||||
@@ -1678,8 +1665,6 @@ def run_app() -> None:
|
|||||||
build_search_tab(db_path, show_rejected)
|
build_search_tab(db_path, show_rejected)
|
||||||
elif selection == tab_labels[3]:
|
elif selection == tab_labels[3]:
|
||||||
build_browser_tab(db_path, show_rejected)
|
build_browser_tab(db_path, show_rejected)
|
||||||
elif selection == tab_labels[4]:
|
|
||||||
build_mp_quiz_tab(db_path)
|
|
||||||
else:
|
else:
|
||||||
build_svd_components_tab(db_path)
|
build_svd_components_tab(db_path)
|
||||||
|
|
||||||
|
|||||||
+11
-3
@@ -1,5 +1,13 @@
|
|||||||
"""Stemwijzer page — thin wrapper around the existing app module."""
|
"""Stemwijzer page — quiz to find your matching MP."""
|
||||||
|
|
||||||
from app import main # noqa: F401 (module-level set_page_config runs on import)
|
import streamlit as st
|
||||||
|
|
||||||
main()
|
st.set_page_config(
|
||||||
|
page_title="Stemwijzer",
|
||||||
|
page_icon="🗳️",
|
||||||
|
layout="centered",
|
||||||
|
)
|
||||||
|
|
||||||
|
from explorer import build_mp_quiz_tab
|
||||||
|
|
||||||
|
build_mp_quiz_tab("data/motions.db")
|
||||||
|
|||||||
@@ -19,3 +19,8 @@ dependencies = [
|
|||||||
"beautifulsoup4>=4.14.3",
|
"beautifulsoup4>=4.14.3",
|
||||||
"lxml>=6.0.2",
|
"lxml>=6.0.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[dependency-groups]
|
||||||
|
dev = [
|
||||||
|
"pytest>=9.0.2",
|
||||||
|
]
|
||||||
|
|||||||
@@ -5,6 +5,19 @@ import sys
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Helpers shared by orientation tests
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def _make_fake_traj(aligned):
|
||||||
|
fake = types.SimpleNamespace()
|
||||||
|
fake._load_window_ids = lambda db: list(aligned.keys())
|
||||||
|
fake._load_mp_vectors_for_window = lambda db, w: aligned.get(w, {})
|
||||||
|
fake._procrustes_align_windows = lambda x: aligned
|
||||||
|
return fake
|
||||||
|
|
||||||
|
|
||||||
def test_compute_2d_axes_pca_synthetic(monkeypatch):
|
def test_compute_2d_axes_pca_synthetic(monkeypatch):
|
||||||
"""Synthetic test for compute_2d_axes using patched alignment helper."""
|
"""Synthetic test for compute_2d_axes using patched alignment helper."""
|
||||||
|
|
||||||
@@ -14,15 +27,15 @@ def test_compute_2d_axes_pca_synthetic(monkeypatch):
|
|||||||
# _load_window_ids should return ordered windows
|
# _load_window_ids should return ordered windows
|
||||||
fake_traj._load_window_ids = lambda db: ["w1", "w2"]
|
fake_traj._load_window_ids = lambda db: ["w1", "w2"]
|
||||||
|
|
||||||
# _load_mp_vectors_for_window is not used because we patch _procrustes_align_windows
|
|
||||||
fake_traj._load_mp_vectors_for_window = lambda db, w: {}
|
|
||||||
|
|
||||||
# Provide aligned vectors directly
|
# Provide aligned vectors directly
|
||||||
aligned = {
|
aligned = {
|
||||||
"w1": {"Alice": np.array([1.0, 0.0, 0.0]), "Bob": np.array([0.0, 1.0, 0.0])},
|
"w1": {"Alice": np.array([1.0, 0.0, 0.0]), "Bob": np.array([0.0, 1.0, 0.0])},
|
||||||
"w2": {"Alice": np.array([0.8, 0.2, 0.0]), "Bob": np.array([0.1, 0.9, 0.0])},
|
"w2": {"Alice": np.array([0.8, 0.2, 0.0]), "Bob": np.array([0.1, 0.9, 0.0])},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# _load_mp_vectors_for_window returns the pre-aligned vectors (needed for padding step)
|
||||||
|
fake_traj._load_mp_vectors_for_window = lambda db, w: aligned.get(w, {})
|
||||||
|
|
||||||
fake_traj._procrustes_align_windows = lambda x: aligned
|
fake_traj._procrustes_align_windows = lambda x: aligned
|
||||||
|
|
||||||
# Insert fake module into sys.modules for import by analysis.political_axis
|
# Insert fake module into sys.modules for import by analysis.political_axis
|
||||||
@@ -42,3 +55,95 @@ def test_compute_2d_axes_pca_synthetic(monkeypatch):
|
|||||||
assert np.isfinite(coord[0]) and np.isfinite(coord[1])
|
assert np.isfinite(coord[0]) and np.isfinite(coord[1])
|
||||||
|
|
||||||
assert axis_def.get("method") == "pca"
|
assert axis_def.get("method") == "pca"
|
||||||
|
|
||||||
|
|
||||||
|
def test_pca_axis_orientation(monkeypatch):
|
||||||
|
"""PCA axes must be oriented so right parties score higher on X and
|
||||||
|
progressive parties score higher on Y than their respective opposites.
|
||||||
|
|
||||||
|
We construct a minimal vote-matrix world where:
|
||||||
|
- Right MPs (PVV, VVD members) cluster in one direction on dim-0.
|
||||||
|
- Left MPs (SP, GroenLinks-PvdA members) cluster in the opposite direction.
|
||||||
|
- Progressive MPs cluster on dim-1; conservative MPs on the opposite side.
|
||||||
|
|
||||||
|
The orientation logic in compute_2d_axes should flip axis signs so that
|
||||||
|
right_x > left_x and prog_y > cons_y regardless of the raw SVD sign.
|
||||||
|
"""
|
||||||
|
# Build vectors so that right parties are at +1 on dim-0 and
|
||||||
|
# progressive parties are at +1 on dim-1.
|
||||||
|
# We deliberately negate them to test that auto-orient flips them back.
|
||||||
|
# Right/left use magnitude 3, prog/cons use magnitude 1 so that dim-0
|
||||||
|
# dominates PCA variance — ensuring PC1 = left-right axis, PC2 = prog-cons.
|
||||||
|
right_vec = np.array([-3.0, 0.0, 0.0]) # intentionally negative on dim-0
|
||||||
|
left_vec = np.array([3.0, 0.0, 0.0]) # intentionally positive on dim-0
|
||||||
|
prog_vec = np.array([0.0, -1.0, 0.0]) # intentionally negative on dim-1
|
||||||
|
cons_vec = np.array([0.0, 1.0, 0.0]) # intentionally positive on dim-1
|
||||||
|
|
||||||
|
aligned = {
|
||||||
|
"w1": {
|
||||||
|
# Right-leaning MPs
|
||||||
|
"Wilders, G.": right_vec,
|
||||||
|
"Rutte, M.": right_vec + np.array([0.0, 0.0, 0.05]),
|
||||||
|
# Left-leaning MPs
|
||||||
|
"Marijnissen, L.": left_vec,
|
||||||
|
"Klever, A.": left_vec + np.array([0.0, 0.0, 0.05]),
|
||||||
|
# Progressive MPs
|
||||||
|
"Bromet, L.": prog_vec,
|
||||||
|
"Nijboer, H.": prog_vec + np.array([0.0, 0.0, -0.05]),
|
||||||
|
# Conservative MPs
|
||||||
|
"Segers, G.": cons_vec,
|
||||||
|
"Omtzigt, P.": cons_vec + np.array([0.0, 0.0, -0.05]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# mp_metadata rows used by the orientation code (party affiliation)
|
||||||
|
mp_metadata = [
|
||||||
|
("Wilders, G.", "PVV"),
|
||||||
|
("Rutte, M.", "VVD"),
|
||||||
|
("Marijnissen, L.", "SP"),
|
||||||
|
("Klever, A.", "GroenLinks-PvdA"),
|
||||||
|
("Bromet, L.", "GroenLinks-PvdA"),
|
||||||
|
("Nijboer, H.", "SP"),
|
||||||
|
("Segers, G.", "CDA"),
|
||||||
|
("Omtzigt, P.", "Nieuw Sociaal Contract"),
|
||||||
|
]
|
||||||
|
|
||||||
|
fake_traj = _make_fake_traj(aligned)
|
||||||
|
monkeypatch.setitem(sys.modules, "analysis.trajectory", fake_traj)
|
||||||
|
|
||||||
|
# Patch duckdb so the orientation helper can fetch mp_metadata
|
||||||
|
import types as _types
|
||||||
|
|
||||||
|
fake_conn = _types.SimpleNamespace(
|
||||||
|
execute=lambda q: _types.SimpleNamespace(fetchall=lambda: mp_metadata),
|
||||||
|
close=lambda: None,
|
||||||
|
)
|
||||||
|
import duckdb as _duckdb
|
||||||
|
|
||||||
|
monkeypatch.setattr(_duckdb, "connect", lambda db_path, **kw: fake_conn)
|
||||||
|
|
||||||
|
# Need to reload the module so monkeypatched sys.modules takes effect
|
||||||
|
import importlib, analysis.political_axis as _ax
|
||||||
|
|
||||||
|
importlib.reload(_ax)
|
||||||
|
from analysis.political_axis import compute_2d_axes
|
||||||
|
|
||||||
|
positions_by_window, axis_def = compute_2d_axes(
|
||||||
|
db_path="dummy", window_ids=["w1"], method="pca"
|
||||||
|
)
|
||||||
|
|
||||||
|
pos = positions_by_window["w1"]
|
||||||
|
|
||||||
|
# X-axis: right parties should score higher than left parties
|
||||||
|
right_x = np.mean([pos["Wilders, G."][0], pos["Rutte, M."][0]])
|
||||||
|
left_x = np.mean([pos["Marijnissen, L."][0], pos["Klever, A."][0]])
|
||||||
|
assert right_x > left_x, (
|
||||||
|
f"Expected right parties (x={right_x:.3f}) > left parties (x={left_x:.3f}) on X-axis"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Y-axis: progressive parties should score higher than conservative parties
|
||||||
|
prog_y = np.mean([pos["Bromet, L."][1], pos["Nijboer, H."][1]])
|
||||||
|
cons_y = np.mean([pos["Segers, G."][1], pos["Omtzigt, P."][1]])
|
||||||
|
assert prog_y > cons_y, (
|
||||||
|
f"Expected progressive parties (y={prog_y:.3f}) > conservative parties (y={cons_y:.3f}) on Y-axis"
|
||||||
|
)
|
||||||
|
|||||||
@@ -273,6 +273,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
|
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iniconfig"
|
||||||
|
version = "2.3.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jinja2"
|
name = "jinja2"
|
||||||
version = "3.1.6"
|
version = "3.1.6"
|
||||||
@@ -697,6 +706,15 @@ 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" },
|
{ 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"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "protobuf"
|
name = "protobuf"
|
||||||
version = "6.31.1"
|
version = "6.31.1"
|
||||||
@@ -823,6 +841,22 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/b2/e6/94145d714402fd5ade00b5661f2d0ab981219e07f7db9bfa16786cdb9c04/pynndescent-0.6.0-py3-none-any.whl", hash = "sha256:dc8c74844e4c7f5cbd1e0cd6909da86fdc789e6ff4997336e344779c3d5538ef", size = 73511, upload-time = "2026-01-08T21:29:57.306Z" },
|
{ url = "https://files.pythonhosted.org/packages/b2/e6/94145d714402fd5ade00b5661f2d0ab981219e07f7db9bfa16786cdb9c04/pynndescent-0.6.0-py3-none-any.whl", hash = "sha256:dc8c74844e4c7f5cbd1e0cd6909da86fdc789e6ff4997336e344779c3d5538ef", size = 73511, upload-time = "2026-01-08T21:29:57.306Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest"
|
||||||
|
version = "9.0.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||||
|
{ name = "iniconfig" },
|
||||||
|
{ name = "packaging" },
|
||||||
|
{ name = "pluggy" },
|
||||||
|
{ name = "pygments" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-dateutil"
|
name = "python-dateutil"
|
||||||
version = "2.9.0.post0"
|
version = "2.9.0.post0"
|
||||||
@@ -1113,6 +1147,11 @@ dependencies = [
|
|||||||
{ name = "umap-learn" },
|
{ name = "umap-learn" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[package.dev-dependencies]
|
||||||
|
dev = [
|
||||||
|
{ name = "pytest" },
|
||||||
|
]
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "beautifulsoup4", specifier = ">=4.14.3" },
|
{ name = "beautifulsoup4", specifier = ">=4.14.3" },
|
||||||
@@ -1129,6 +1168,9 @@ requires-dist = [
|
|||||||
{ name = "umap-learn", specifier = ">=0.5" },
|
{ name = "umap-learn", specifier = ">=0.5" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[package.metadata.requires-dev]
|
||||||
|
dev = [{ name = "pytest", specifier = ">=9.0.2" }]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "streamlit"
|
name = "streamlit"
|
||||||
version = "1.48.0"
|
version = "1.48.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user