diff --git a/Taskfile.yml b/Taskfile.yml index 698fb61..2f4afee 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -140,3 +140,9 @@ tasks: cmds: - uv run --group dbt dbt docs generate - uv run --group dbt dbt docs serve + + # ----- Streamlit dashboard ----- + dashboard: + desc: Launch the Streamlit analytics dashboard + cmds: + - uv run --group dashboard streamlit run dashboard/app.py diff --git a/dashboard/__init__.py b/dashboard/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dashboard/app.py b/dashboard/app.py new file mode 100644 index 0000000..0c59733 --- /dev/null +++ b/dashboard/app.py @@ -0,0 +1,76 @@ +"""Repolytics analytics dashboard - Streamlit entrypoint. + +Reads the dbt marts read-only from the DuckDB warehouse; defines the global +repo/date filters in the sidebar and renders one page per analytics theme. +""" + +import streamlit as st + +# Imported relative to the dashboard/ app directory, which Streamlit places on +# sys.path when the app is launched via `streamlit run dashboard/app.py`. +from lib import data, queries +from views import ( + _components as c, +) +from views import ( + contributors, + overview, + popularity, + releases, + velocity, +) + +st.set_page_config(page_title="Repolytics", page_icon="📊", layout="wide") + +conn = c.get_conn() + +if not data.has_marts(conn): + st.title("Repolytics") + st.error("No marts found in the warehouse. Run the pipeline and reload.") + st.stop() + +# ----- Global filters (persist across pages) ----- +all_repos = queries.all_repo_names(conn) +st.sidebar.header("Filters") +selected = st.sidebar.multiselect("Repositories", all_repos, default=all_repos) +st.session_state["repos"] = selected or all_repos + +low, high = data.date_bounds(conn) +if low and high and low < high: + date_from, date_to = st.sidebar.slider( + "Activity date range", min_value=low, max_value=high, value=(low, high) + ) + st.session_state["date_from"], st.session_state["date_to"] = date_from, date_to + +st.sidebar.caption( + "Reads the dbt marts read-only. The date range filters the activity and velocity " + "charts; snapshot and all-time views (health, stars/downloads, contributors, " + "releases) show their full history. Stars/downloads trends fill in as snapshots " + "accrue." +) + +st.navigation( + [ + st.Page( + overview.render, + title="Overview", + icon="📊", + url_path="overview", + default=True, + ), + st.Page( + popularity.render, + title="Popularity & Health", + icon="⭐", + url_path="popularity", + ), + st.Page(velocity.render, title="Velocity", icon="🚀", url_path="velocity"), + st.Page( + contributors.render, + title="Contributors", + icon="👥", + url_path="contributors", + ), + st.Page(releases.render, title="Releases", icon="🏷️", url_path="releases"), + ] +).run() diff --git a/dashboard/lib/__init__.py b/dashboard/lib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dashboard/lib/data.py b/dashboard/lib/data.py new file mode 100644 index 0000000..e73092a --- /dev/null +++ b/dashboard/lib/data.py @@ -0,0 +1,57 @@ +"""Read-only DuckDB access for the dashboard (streamlit-free).""" + +from datetime import date +from pathlib import Path + +import duckdb +import pandas as pd + +from repolytics.config import get_settings + + +def connect(duckdb_path: str | Path | None = None) -> duckdb.DuckDBPyConnection: + """Open a read-only DuckDB connection to the warehouse. + + Defaults to `Settings.duckdb_path`. + """ + path = Path(duckdb_path) if duckdb_path is not None else get_settings().duckdb_path + return duckdb.connect(str(path), read_only=True) + + +def run_query( + conn: duckdb.DuckDBPyConnection, sql: str, params: list | None = None +) -> pd.DataFrame: + """Execute `sql` with positional `params` and return a pandas DataFrame.""" + return conn.execute(sql, params or []).df() + + +def has_marts(conn: duckdb.DuckDBPyConnection) -> bool: + """True when the `marts` schema is populated (so the dashboard has data).""" + row = conn.execute( + "select count(*) from information_schema.tables " + "where table_schema = 'marts' and table_name = 'dim_repositories'" + ).fetchone() + return bool(row and row[0]) + + +def date_bounds(conn: duckdb.DuckDBPyConnection) -> tuple[date, date]: + """Min/max event date across all activity facts, for the sidebar date filter. + + Spans commits, PRs (opened), issues (opened), and releases so the slider covers + every date-filtered chart - a repo whose PRs/releases extend past its last commit + is not clipped. + """ + row = conn.execute( + """ + with event_keys as ( + select date_key from marts.fct_commits + union all select opened_date_key from marts.fct_pull_requests + union all select opened_date_key from marts.fct_issues + union all select date_key from marts.fct_releases + ) + select min(d.full_date), max(d.full_date) + from event_keys e + join marts.dim_dates d on e.date_key = d.date_key + """ + ).fetchone() + return row[0], row[1] diff --git a/dashboard/lib/health.py b/dashboard/lib/health.py new file mode 100644 index 0000000..6cef552 --- /dev/null +++ b/dashboard/lib/health.py @@ -0,0 +1,60 @@ +"""Composite project-health score (streamlit-free, pure DataFrame -> DataFrame). + +A blend of five signals, each scored **absolutely** against a fixed reference target. +That keeps a project's score stable regardless of which other projects are selected, +and comparable across sessions and over time. The two rate signals (PR merge rate, +issue close rate) are already 0..1 and are used directly; the unbounded signals +(recent commits, stars) are mapped through a saturating log transform against a +reference, and release recency through an exponential time-decay. + +Input columns (one row per repo, from `queries.health_components`): + repository_name, commits_90d, pr_merge_rate, issue_close_rate, + days_since_release (nullable -> treated as never released -> 0), stars + +Output adds the five `c_*` component scores (0..1) and `health_score` (0..100). +""" + +import numpy as np +import pandas as pd + +WEIGHTS: dict[str, float] = { + "activity": 0.30, # commits in the last 90 days + "pr_responsiveness": 0.20, # PR merge rate, last 12 months + "issue_management": 0.20, # issue close rate, last 12 months + "release_recency": 0.15, # how recently the repo last released + "popularity": 0.15, # current stars (log-scaled) +} + +# Reference targets for the absolute component scores: the value at which a component +# counts as "fully healthy" (or, for release recency, its decay half-life). +TARGET_COMMITS_90D = 90 # ~1 commit/day over the window saturates the activity score +REF_STARS = 50_000 # log-scale reference for "maximally popular" +RELEASE_HALFLIFE_DAYS = 180.0 # released ~6 months ago -> 0.5; never released -> 0 + + +def _log_ratio(series: pd.Series, reference: float) -> pd.Series: + """Saturating log map to 0..1: `log1p(value) / log1p(reference)`, capped at 1.""" + values = series.astype(float).clip(lower=0) + return np.minimum(np.log1p(values) / np.log1p(reference), 1.0) + + +def compute_health(components: pd.DataFrame) -> pd.DataFrame: + """Return `components` with `c_*` component scores and a 0..100 `health_score`.""" + df = components.copy() + + # Missing release -> infinite days -> recency decays to 0. + days = df["days_since_release"].astype(float).fillna(np.inf) + df["c_activity"] = _log_ratio(df["commits_90d"], TARGET_COMMITS_90D) + df["c_pr_responsiveness"] = df["pr_merge_rate"].astype(float).clip(0, 1) + df["c_issue_management"] = df["issue_close_rate"].astype(float).clip(0, 1) + df["c_release_recency"] = 0.5 ** (days / RELEASE_HALFLIFE_DAYS) + df["c_popularity"] = _log_ratio(df["stars"], REF_STARS) + + df["health_score"] = ( + df["c_activity"] * WEIGHTS["activity"] + + df["c_pr_responsiveness"] * WEIGHTS["pr_responsiveness"] + + df["c_issue_management"] * WEIGHTS["issue_management"] + + df["c_release_recency"] * WEIGHTS["release_recency"] + + df["c_popularity"] * WEIGHTS["popularity"] + ) * 100.0 + return df diff --git a/dashboard/lib/queries.py b/dashboard/lib/queries.py new file mode 100644 index 0000000..32f5d31 --- /dev/null +++ b/dashboard/lib/queries.py @@ -0,0 +1,470 @@ +"""Business queries over the marts star schema. + +Every function takes an open DuckDB connection plus filters and returns a pandas +DataFrame. + +Conventions honored from the warehouse contract: +- Facts key dates as integer `date_key` (YYYYMMDD); join `dim_dates` for real dates. +- Facts carry `repository_key` (an SCD2 version); join `dim_repositories` and group + by `repository_name` (stable across versions) for display. +- `repos` is always a list of `repository_name`s; queries filter with `= ANY(?)`. +""" + +from datetime import date + +import duckdb +import pandas as pd + +from lib.data import run_query + + +def _date_clause(date_from: date | None, date_to: date | None) -> tuple[str, list]: + """`(sql_fragment, params)` bounding `d.full_date`, or empty when unset.""" + if date_from is None or date_to is None: + return "", [] + return "and d.full_date between ? and ?", [date_from, date_to] + + +def all_repo_names(conn: duckdb.DuckDBPyConnection) -> list[str]: + """Current repository names, for the sidebar filter (read dynamically).""" + rows = conn.execute( + "select repository_name from marts.dim_repositories " + "where is_current order by repository_name" + ).fetchall() + return [r[0] for r in rows] + + +# ----- Overview ----- +def overview_kpis(conn: duckdb.DuckDBPyConnection, repos: list[str]) -> pd.DataFrame: + """Single-row headline KPIs across the selected repos.""" + sql = """ + with sel as ( + select repository_key, repository_name + from marts.dim_repositories + where repository_name = any(?) + ), + latest_metrics as ( + select m.repository_name, m.stars, m.forks, + row_number() over (partition by m.repository_name + order by m.date_key desc) as rn + from marts.fct_repository_metrics m + where m.repository_name = any(?) + ) + select + (select count(distinct repository_name) from sel) as repos, + (select count(*) + from marts.fct_commits join sel using (repository_key)) as commits, + (select count(distinct contributor_key) + from marts.fct_commits join sel using (repository_key)) as contributors, + (select count(*) + from marts.fct_pull_requests join sel using (repository_key)) as prs, + (select coalesce(avg(is_merged::int), 0) + from marts.fct_pull_requests join sel using (repository_key)) + as pr_merge_rate, + (select count(*) + from marts.fct_issues join sel using (repository_key)) as issues, + (select coalesce(avg(is_closed::int), 0) + from marts.fct_issues join sel using (repository_key)) + as issue_close_rate, + (select count(*) + from marts.fct_releases join sel using (repository_key)) as releases, + (select coalesce(sum(stars), 0) from latest_metrics where rn = 1) as stars, + (select coalesce(sum(forks), 0) from latest_metrics where rn = 1) as forks + """ + return run_query(conn, sql, [repos, repos]) + + +def repo_snapshot(conn: duckdb.DuckDBPyConnection, repos: list[str]) -> pd.DataFrame: + """Current per-repo attributes + latest stars/forks/open_issues snapshot.""" + sql = """ + with latest as ( + select repository_name, stars, forks, open_issues, + row_number() over ( + partition by repository_name order by date_key desc + ) as rn + from marts.fct_repository_metrics + where repository_name = any(?) + ) + select + d.repository_name, d.language, d.license_spdx, d.topics, d.created_at, + l.stars, l.forks, l.open_issues + from marts.dim_repositories d + left join latest l on d.repository_name = l.repository_name and l.rn = 1 + where d.is_current and d.repository_name = any(?) + order by l.stars desc nulls last + """ + return run_query(conn, sql, [repos, repos]) + + +def commits_per_month( + conn: duckdb.DuckDBPyConnection, + repos: list[str], + date_from: date | None = None, + date_to: date | None = None, +) -> pd.DataFrame: + """Commit counts per calendar month per repo.""" + clause, params = _date_clause(date_from, date_to) + sql = f""" + select date_trunc('month', d.full_date) as month, + dr.repository_name, count(*) as commits + from marts.fct_commits c + join marts.dim_dates d on c.date_key = d.date_key + join marts.dim_repositories dr on c.repository_key = dr.repository_key + where dr.repository_name = any(?) {clause} + group by 1, 2 order by 1, 2 + """ + return run_query(conn, sql, [repos, *params]) + + +# ----- Popularity & health ----- +def stars_forks_over_time( + conn: duckdb.DuckDBPyConnection, repos: list[str] +) -> pd.DataFrame: + """Daily stars/forks snapshots per repo (sparse until snapshots accrue).""" + sql = """ + select d.full_date as date, m.repository_name, m.stars, m.forks, m.open_issues + from marts.fct_repository_metrics m + join marts.dim_dates d on m.date_key = d.date_key + where m.repository_name = any(?) + order by 1, 2 + """ + return run_query(conn, sql, [repos]) + + +def downloads_over_time( + conn: duckdb.DuckDBPyConnection, repos: list[str] +) -> pd.DataFrame: + """Daily PyPI download counts per package (sparse until backfilled).""" + sql = """ + select d.full_date as date, f.package, f.download_count as downloads + from marts.fct_daily_downloads f + join marts.dim_dates d on f.date_key = d.date_key + join marts.dim_repositories dr + on f.repository_key = dr.repository_key and dr.repository_name = any(?) + order by 1, 2 + """ + return run_query(conn, sql, [repos]) + + +def downloads_vs_stars( + conn: duckdb.DuckDBPyConnection, repos: list[str] +) -> pd.DataFrame: + """Latest-day downloads vs current stars per repo (popularity correlation).""" + sql = """ + with latest_stars as ( + select repository_name, repository_key, stars, + row_number() over ( + partition by repository_name order by date_key desc + ) as rn + from marts.fct_repository_metrics + where repository_name = any(?) + ), + latest_dl as ( + select dr.repository_name, f.download_count, + row_number() over ( + partition by dr.repository_name order by f.date_key desc + ) as rn + from marts.fct_daily_downloads f + join marts.dim_repositories dr on f.repository_key = dr.repository_key + ) + select s.repository_name, s.stars, coalesce(dl.download_count, 0) as downloads + from latest_stars s + left join latest_dl dl on s.repository_name = dl.repository_name and dl.rn = 1 + where s.rn = 1 + """ + return run_query(conn, sql, [repos]) + + +def health_components( + conn: duckdb.DuckDBPyConnection, repos: list[str] +) -> pd.DataFrame: + """Raw per-repo inputs to the composite health score (see lib.health).""" + sql = """ + with base as ( + select repository_name from marts.dim_repositories + where is_current and repository_name = any(?) + ), + commits90 as ( + select dr.repository_name, count(*) as commits_90d + from marts.fct_commits c + join marts.dim_repositories dr on c.repository_key = dr.repository_key + where c.committed_at >= now() - interval '90 days' + and dr.repository_name = any(?) + group by 1 + ), + pr12 as ( + select dr.repository_name, avg(p.is_merged::int) as pr_merge_rate + from marts.fct_pull_requests p + join marts.dim_dates d on p.opened_date_key = d.date_key + join marts.dim_repositories dr on p.repository_key = dr.repository_key + where d.full_date >= now() - interval '365 days' + and dr.repository_name = any(?) + group by 1 + ), + iss12 as ( + select dr.repository_name, avg(i.is_closed::int) as issue_close_rate + from marts.fct_issues i + join marts.dim_dates d on i.opened_date_key = d.date_key + join marts.dim_repositories dr on i.repository_key = dr.repository_key + where d.full_date >= now() - interval '365 days' + and dr.repository_name = any(?) + group by 1 + ), + rel as ( + select dr.repository_name, + date_diff('day', max(r.published_at), now()) as days_since_release + from marts.fct_releases r + join marts.dim_repositories dr on r.repository_key = dr.repository_key + where dr.repository_name = any(?) + group by 1 + ), + stars as ( + select repository_name, stars, + row_number() over ( + partition by repository_name order by date_key desc + ) as rn + from marts.fct_repository_metrics + where repository_name = any(?) + ) + select b.repository_name, + coalesce(c.commits_90d, 0) as commits_90d, + coalesce(p.pr_merge_rate, 0) as pr_merge_rate, + coalesce(i.issue_close_rate, 0) as issue_close_rate, + r.days_since_release, + coalesce(s.stars, 0) as stars + from base b + left join commits90 c on b.repository_name = c.repository_name + left join pr12 p on b.repository_name = p.repository_name + left join iss12 i on b.repository_name = i.repository_name + left join rel r on b.repository_name = r.repository_name + left join stars s on b.repository_name = s.repository_name and s.rn = 1 + order by b.repository_name + """ + # One bind per `any(?)`: base + commits90 + pr12 + iss12 + rel + stars. + return run_query(conn, sql, [repos, repos, repos, repos, repos, repos]) + + +# ----- Development velocity ----- +def pr_velocity_monthly( + conn: duckdb.DuckDBPyConnection, + repos: list[str], + date_from: date | None = None, + date_to: date | None = None, +) -> pd.DataFrame: + """Median time-to-merge (hours) by merge month, per repo.""" + clause, params = _date_clause(date_from, date_to) + sql = f""" + select date_trunc('month', d.full_date) as month, + dr.repository_name, + median(p.time_to_merge_hours) as median_merge_hours + from marts.fct_pull_requests p + join marts.dim_dates d on p.merged_date_key = d.date_key + join marts.dim_repositories dr on p.repository_key = dr.repository_key + where p.is_merged and dr.repository_name = any(?) {clause} + group by 1, 2 order by 1, 2 + """ + return run_query(conn, sql, [repos, *params]) + + +def pr_throughput_monthly( + conn: duckdb.DuckDBPyConnection, + repos: list[str], + date_from: date | None = None, + date_to: date | None = None, +) -> pd.DataFrame: + """PRs opened, PRs merged, and merge rate by open month, per repo.""" + clause, params = _date_clause(date_from, date_to) + sql = f""" + select date_trunc('month', d.full_date) as month, + dr.repository_name, + count(*) as opened, + sum(p.is_merged::int) as merged, + avg(p.is_merged::int) as merge_rate + from marts.fct_pull_requests p + join marts.dim_dates d on p.opened_date_key = d.date_key + join marts.dim_repositories dr on p.repository_key = dr.repository_key + where dr.repository_name = any(?) {clause} + group by 1, 2 order by 1, 2 + """ + return run_query(conn, sql, [repos, *params]) + + +def issue_resolution_monthly( + conn: duckdb.DuckDBPyConnection, + repos: list[str], + date_from: date | None = None, + date_to: date | None = None, +) -> pd.DataFrame: + """Median time-to-close (hours) and close rate by open month, per repo.""" + clause, params = _date_clause(date_from, date_to) + sql = f""" + select date_trunc('month', d.full_date) as month, + dr.repository_name, + median(i.time_to_close_hours) as median_close_hours, + avg(i.is_closed::int) as close_rate + from marts.fct_issues i + join marts.dim_dates d on i.opened_date_key = d.date_key + join marts.dim_repositories dr on i.repository_key = dr.repository_key + where dr.repository_name = any(?) {clause} + group by 1, 2 order by 1, 2 + """ + return run_query(conn, sql, [repos, *params]) + + +def issue_label_breakdown( + conn: duckdb.DuckDBPyConnection, repos: list[str], limit: int = 15 +) -> pd.DataFrame: + """Top issue labels by volume across the selected repos, split open vs closed. + + A triage view of what the open-source backlog is made of. + """ + sql = """ + select dl.label_name, + count(*) as issues, + sum((not i.is_closed)::int) as open, + sum(i.is_closed::int) as closed + from marts.bridge_issue_labels b + join marts.fct_issues i on b.issue_key = i.issue_key + join marts.dim_repositories dr on i.repository_key = dr.repository_key + join marts.dim_labels dl on b.label_key = dl.label_key + where dr.repository_name = any(?) + group by 1 + order by issues desc + limit ? + """ + return run_query(conn, sql, [repos, limit]) + + +# ----- Contributors & community ----- +def commit_share(conn: duckdb.DuckDBPyConnection, repos: list[str]) -> pd.DataFrame: + """Commits per contributor per repo (feeds bus factor + Lorenz curve).""" + sql = """ + select dr.repository_name, co.username, count(*) as commits + from marts.fct_commits c + join marts.dim_repositories dr on c.repository_key = dr.repository_key + join marts.dim_contributors co on c.contributor_key = co.contributor_key + where dr.repository_name = any(?) + group by 1, 2 + """ + return run_query(conn, sql, [repos]) + + +def contributor_leaderboard( + conn: duckdb.DuckDBPyConnection, repos: list[str], limit: int = 20 +) -> pd.DataFrame: + """Top contributors by commits across the selected repos.""" + sql = """ + select co.username, count(*) as commits, + count(distinct dr.repository_name) as projects + from marts.fct_commits c + join marts.dim_repositories dr on c.repository_key = dr.repository_key + join marts.dim_contributors co on c.contributor_key = co.contributor_key + where dr.repository_name = any(?) + group by 1 order by commits desc limit ? + """ + return run_query(conn, sql, [repos, limit]) + + +def retention_cohort(conn: duckdb.DuckDBPyConnection) -> pd.DataFrame: + """Monthly contributor retention: % of a first-active cohort active N months on. + + Necessarily **repo-agnostic**: that mart's grain is contributor x month across all + tracked projects (no repository_key), so retention spans the whole tracked set and + cannot be filtered by repo. First activity across the set sets each cohort. + """ + sql = """ + with active as ( + select distinct contributor_key, date_trunc('month', event_month) as month + from marts.fct_contributor_activity_monthly + ), + cohort as ( + select contributor_key, min(month) as cohort_month + from active group by 1 + ), + joined as ( + select c.cohort_month, + date_diff('month', c.cohort_month, a.month) as months_since, + a.contributor_key + from active a join cohort c on a.contributor_key = c.contributor_key + ), + sizes as (select cohort_month, count(*) as cohort_size from cohort group by 1) + select j.cohort_month, + j.months_since, + count(distinct j.contributor_key) as active, + s.cohort_size, + count(distinct j.contributor_key) * 1.0 / s.cohort_size as retention + from joined j join sizes s on j.cohort_month = s.cohort_month + where j.months_since >= 0 + group by 1, 2, s.cohort_size + order by 1, 2 + """ + return run_query(conn, sql, []) + + +def active_contributors_monthly(conn: duckdb.DuckDBPyConnection) -> pd.DataFrame: + """Monthly active contributors, split into new vs. returning. + + Repo-agnostic for the same reason as `retention_cohort` (the mart has no + repository grain). + """ + sql = """ + with monthly as ( + select distinct contributor_key, date_trunc('month', event_month) as month + from marts.fct_contributor_activity_monthly + ), + cohort as ( + select contributor_key, min(month) as first_month from monthly group by 1 + ) + select m.month, + count(*) as active, + sum((m.month = c.first_month)::int) as new_contributors, + sum((m.month > c.first_month)::int) as returning_contributors + from monthly m join cohort c using (contributor_key) + group by 1 order by 1 + """ + return run_query(conn, sql, []) + + +def multi_project_contributors(conn: duckdb.DuckDBPyConnection) -> pd.DataFrame: + """Contributors active across more than one tracked project.""" + sql = """ + select username, total_commits, total_prs_opened, total_prs_merged, + distinct_projects_count, primary_project + from marts.dim_contributors + where distinct_projects_count > 1 + order by distinct_projects_count desc, total_commits desc + """ + return run_query(conn, sql, []) + + +# ----- Releases & cadence ----- +def releases(conn: duckdb.DuckDBPyConnection, repos: list[str]) -> pd.DataFrame: + """All releases with publish timestamps, per repo (timeline).""" + sql = """ + select dr.repository_name, r.tag_name, r.release_name, r.published_at + from marts.fct_releases r + join marts.dim_repositories dr on r.repository_key = dr.repository_key + where dr.repository_name = any(?) + order by r.published_at + """ + return run_query(conn, sql, [repos]) + + +def release_gaps(conn: duckdb.DuckDBPyConnection, repos: list[str]) -> pd.DataFrame: + """Days between consecutive releases per repo (cadence distribution).""" + sql = """ + select repository_name, gap_days + from ( + select dr.repository_name, + date_diff('day', + lag(r.published_at) over ( + partition by dr.repository_name order by r.published_at + ), + r.published_at) as gap_days + from marts.fct_releases r + join marts.dim_repositories dr on r.repository_key = dr.repository_key + where dr.repository_name = any(?) + ) t + where gap_days is not null + """ + return run_query(conn, sql, [repos]) diff --git a/dashboard/views/__init__.py b/dashboard/views/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dashboard/views/_components.py b/dashboard/views/_components.py new file mode 100644 index 0000000..4b7ef00 --- /dev/null +++ b/dashboard/views/_components.py @@ -0,0 +1,44 @@ +"""Shared Streamlit helpers for the view pages (UI layer).""" + +from datetime import date + +import pandas as pd +import streamlit as st + +from lib import data + + +@st.cache_resource +def get_conn(): + """One cached read-only DuckDB connection, reused across reruns.""" + return data.connect() + + +def filters() -> tuple[list[str], date | None, date | None]: + """Current sidebar selection (set in `app.py`).""" + return ( + st.session_state.get("repos", []), + st.session_state.get("date_from"), + st.session_state.get("date_to"), + ) + + +def line_or_fallback( + df: pd.DataFrame, x: str, y, color: str | None, label: str +) -> None: + """Line chart when the series spans >=2 periods; otherwise a graceful note. + + When popularity facts (stars/forks/downloads) hold a single daily snapshot, + a trend line would be a single point - fall back to showing the current + values plus a note that the chart fills in as snapshots accrue. + """ + periods = df[x].nunique() if not df.empty else 0 + if periods >= 2: + st.line_chart(df, x=x, y=y, color=color) + elif periods == 1: + st.info( + f"{label} has a single snapshot so far - it becomes a trend as runs accrue." + ) + st.dataframe(df, hide_index=True, width="stretch") + else: + st.info(f"No {label.lower()} data for the current selection.") diff --git a/dashboard/views/contributors.py b/dashboard/views/contributors.py new file mode 100644 index 0000000..06b569d --- /dev/null +++ b/dashboard/views/contributors.py @@ -0,0 +1,141 @@ +"""Contributors & Community page - bus factor, retention, leaderboard.""" + +import altair as alt +import numpy as np +import pandas as pd +import streamlit as st + +from lib import queries +from views import _components as c + + +def _bus_factor(commits: np.ndarray) -> int: + """Min # of top contributors accounting for >=50% of commits.""" + ranked = np.sort(commits)[::-1] + cutoff = 0.5 * ranked.sum() + return int(np.searchsorted(np.cumsum(ranked), cutoff) + 1) + + +def _lorenz(commits: np.ndarray) -> pd.DataFrame: + """Lorenz points: cumulative commit share held by the bottom x% of contributors.""" + ranked = np.sort(commits) # ascending -> classic convex Lorenz curve + k = len(ranked) + return pd.DataFrame( + { + "contributor_frac": np.concatenate([[0.0], np.arange(1, k + 1) / k]), + "commit_frac": np.concatenate([[0.0], np.cumsum(ranked) / ranked.sum()]), + } + ) + + +def render() -> None: + st.title("👥 Contributors & Community") + conn = c.get_conn() + repos, _, _ = c.filters() + + share = queries.commit_share(conn, repos) + + st.subheader("Bus factor") + st.caption( + "How many people you'd have to lose before <50% of commit history is covered. " + "Computed over attributed commits only (commits with no linked GitHub account " + "are excluded)." + ) + if not share.empty: + bf = { + repo: _bus_factor(g["commits"].to_numpy()) + for repo, g in share.groupby("repository_name") + } + cols = st.columns(max(len(bf), 1)) + for col, (repo, value) in zip(cols, bf.items(), strict=False): + col.metric(repo, value) + + lor = pd.concat( + _lorenz(g["commits"].to_numpy()).assign(repository_name=repo) + for repo, g in share.groupby("repository_name") + ) + equality = pd.DataFrame({"contributor_frac": [0, 1], "commit_frac": [0, 1]}) + curve = ( + alt.Chart(lor) + .mark_line() + .encode( + x=alt.X( + "contributor_frac:Q", + title="Cumulative share of contributors", + axis=alt.Axis(format="%"), + ), + y=alt.Y( + "commit_frac:Q", + title="Cumulative share of commits", + axis=alt.Axis(format="%"), + ), + color=alt.Color("repository_name:N", title="Repository"), + ) + ) + line = ( + alt.Chart(equality) + .mark_line(strokeDash=[4, 4], color="gray") + .encode(x="contributor_frac:Q", y="commit_frac:Q") + ) + st.altair_chart(line + curve, width="stretch") + + st.subheader("Contributor leaderboard") + st.dataframe( + queries.contributor_leaderboard(conn, repos), + hide_index=True, + width="stretch", + ) + + st.subheader("Retention cohorts") + st.caption( + "% of each first-active monthly cohort still active N months later. Spans all " + "tracked projects (activity is aggregated per month, not per repo), so this " + "view is not affected by the repository filter." + ) + coh = queries.retention_cohort(conn) + if not coh.empty: + coh = coh[coh["months_since"] <= 24].copy() + recent = sorted(coh["cohort_month"].unique())[-36:] + coh = coh[coh["cohort_month"].isin(recent)] + coh["cohort"] = pd.to_datetime(coh["cohort_month"]).dt.strftime("%Y-%m") + heat = ( + alt.Chart(coh) + .mark_rect() + .encode( + x=alt.X("months_since:O", title="Months since first activity"), + y=alt.Y("cohort:O", title="Cohort", sort="descending"), + color=alt.Color( + "retention:Q", scale=alt.Scale(scheme="blues"), title="Retention" + ), + tooltip=[ + "cohort", + "months_since", + alt.Tooltip("retention:Q", format=".0%"), + "cohort_size", + ], + ) + ) + st.altair_chart(heat, width="stretch") + + st.subheader("Monthly active contributors") + st.caption( + "Distinct contributors active each month, split into new (first-ever active " + "month) vs. returning. Spans all tracked projects (not affected by the " + "repository filter)." + ) + acm = queries.active_contributors_monthly(conn) + c.line_or_fallback( + acm, + "month", + ["new_contributors", "returning_contributors"], + None, + "Monthly active contributors", + ) + + st.subheader("Cross-project contributors") + mp = queries.multi_project_contributors(conn) + st.caption( + f"{len(mp)} contributors active in more than one tracked project. Counted " + "across all tracked projects (not affected by the repository filter)." + ) + st.dataframe(mp, hide_index=True, width="stretch") diff --git a/dashboard/views/overview.py b/dashboard/views/overview.py new file mode 100644 index 0000000..44641f2 --- /dev/null +++ b/dashboard/views/overview.py @@ -0,0 +1,36 @@ +"""Overview page - headline KPIs, current snapshot, and commit activity.""" + +import streamlit as st + +from lib import queries +from views import _components as c + + +def render() -> None: + st.title("📊 Overview") + conn = c.get_conn() + repos, date_from, date_to = c.filters() + + k = queries.overview_kpis(conn, repos).iloc[0] + row1 = st.columns(4) + row1[0].metric("Repositories", int(k.repos)) + row1[1].metric("Commits", f"{int(k.commits):,}") + row1[2].metric("Contributors", f"{int(k.contributors):,}") + row1[3].metric("Releases", int(k.releases)) + + row2 = st.columns(4) + row2[0].metric( + "Pull requests", f"{int(k.prs):,}", f"{k.pr_merge_rate * 100:.0f}% merged" + ) + row2[1].metric( + "Issues", f"{int(k.issues):,}", f"{k.issue_close_rate * 100:.0f}% closed" + ) + row2[2].metric("Stars", f"{int(k.stars):,}") + row2[3].metric("Forks", f"{int(k.forks):,}") + + st.subheader("Current snapshot") + st.dataframe(queries.repo_snapshot(conn, repos), hide_index=True, width="stretch") + + st.subheader("Commit activity (per month)") + cpm = queries.commits_per_month(conn, repos, date_from, date_to) + c.line_or_fallback(cpm, "month", "commits", "repository_name", "Commit activity") diff --git a/dashboard/views/popularity.py b/dashboard/views/popularity.py new file mode 100644 index 0000000..1947258 --- /dev/null +++ b/dashboard/views/popularity.py @@ -0,0 +1,50 @@ +"""Popularity & Health page - stars/forks/downloads + composite health score.""" + +import streamlit as st + +from lib import health, queries +from views import _components as c + +_COMPONENT_COLS = [ + "repository_name", + "health_score", + "c_activity", + "c_pr_responsiveness", + "c_issue_management", + "c_release_recency", + "c_popularity", +] + + +def render() -> None: + st.title("⭐ Popularity & Health") + conn = c.get_conn() + repos, _, _ = c.filters() + + st.subheader("Composite health score") + st.caption( + "Absolute score (0-100) - each project is scored independently against fixed " + "reference targets: a weighted blend of recent commit activity (30%), PR merge " + "rate (20%), issue close rate (20%), release recency (15%), and stars (15%)." + ) + scored = health.compute_health(queries.health_components(conn, repos)) + st.bar_chart(scored, x="repository_name", y="health_score") + st.dataframe(scored[_COMPONENT_COLS], hide_index=True, width="stretch") + + st.subheader("Stars over time") + sf = queries.stars_forks_over_time(conn, repos) + c.line_or_fallback(sf, "date", "stars", "repository_name", "Stars") + + st.subheader("Forks over time") + c.line_or_fallback(sf, "date", "forks", "repository_name", "Forks") + + st.subheader("PyPI downloads over time") + dl = queries.downloads_over_time(conn, repos) + c.line_or_fallback(dl, "date", "downloads", "package", "Downloads") + + st.subheader("Downloads vs stars") + dvs = queries.downloads_vs_stars(conn, repos) + if dvs.empty or dvs["downloads"].sum() == 0: + st.info("Downloads vs stars fills in once PyPI downloads are backfilled.") + else: + st.scatter_chart(dvs, x="downloads", y="stars", color="repository_name") diff --git a/dashboard/views/releases.py b/dashboard/views/releases.py new file mode 100644 index 0000000..ea14fcb --- /dev/null +++ b/dashboard/views/releases.py @@ -0,0 +1,52 @@ +"""Releases & Cadence page - release timeline and inter-release gaps.""" + +import altair as alt +import streamlit as st + +from lib import queries +from views import _components as c + + +def render() -> None: + st.title("🏷️ Releases & Cadence") + conn = c.get_conn() + repos, _, _ = c.filters() + + rel = queries.releases(conn, repos) + if rel.empty: + st.info("No releases for the current selection.") + return + + st.subheader("Release timeline") + timeline = ( + alt.Chart(rel) + .mark_circle(size=60, opacity=0.6) + .encode( + x=alt.X("published_at:T", title="Published"), + y=alt.Y("repository_name:N", title="Repository"), + color=alt.Color("repository_name:N", legend=None), + tooltip=["repository_name", "tag_name", "published_at"], + ) + ) + st.altair_chart(timeline, width="stretch") + + gaps = queries.release_gaps(conn, repos) + col1, col2 = st.columns(2) + with col1: + st.subheader("Median days between releases") + cadence = gaps.groupby("repository_name", as_index=False)["gap_days"].median() + st.bar_chart(cadence, x="repository_name", y="gap_days") + with col2: + st.subheader("Distribution of release gaps (days)") + hist = ( + alt.Chart(gaps) + .mark_bar(opacity=0.7) + .encode( + x=alt.X( + "gap_days:Q", bin=alt.Bin(maxbins=40), title="Days between releases" + ), + y=alt.Y("count()", title="Releases"), + color=alt.Color("repository_name:N", title="Repository"), + ) + ) + st.altair_chart(hist, width="stretch") diff --git a/dashboard/views/velocity.py b/dashboard/views/velocity.py new file mode 100644 index 0000000..2cad591 --- /dev/null +++ b/dashboard/views/velocity.py @@ -0,0 +1,68 @@ +"""Development Velocity page - PR/issue throughput and turnaround trends.""" + +import altair as alt +import streamlit as st + +from lib import queries +from views import _components as c + + +def render() -> None: + st.title("🚀 Development Velocity") + conn = c.get_conn() + repos, date_from, date_to = c.filters() + + st.subheader("PR time-to-merge (median hours, by merge month)") + st.caption("Lower and falling = a healthier, faster review pipeline.") + pv = queries.pr_velocity_monthly(conn, repos, date_from, date_to) + c.line_or_fallback( + pv, "month", "median_merge_hours", "repository_name", "PR velocity" + ) + + tp = queries.pr_throughput_monthly(conn, repos, date_from, date_to) + col1, col2 = st.columns(2) + with col1: + st.subheader("PRs opened per month") + c.line_or_fallback(tp, "month", "opened", "repository_name", "PRs opened") + with col2: + st.subheader("PR merge rate per month") + c.line_or_fallback( + tp, "month", "merge_rate", "repository_name", "PR merge rate" + ) + + ir = queries.issue_resolution_monthly(conn, repos, date_from, date_to) + col3, col4 = st.columns(2) + with col3: + st.subheader("Issue time-to-close (median hours)") + c.line_or_fallback( + ir, "month", "median_close_hours", "repository_name", "Issue resolution" + ) + with col4: + st.subheader("Issue close rate per month") + c.line_or_fallback( + ir, "month", "close_rate", "repository_name", "Issue close rate" + ) + + st.subheader("Issue labels (triage backlog)") + st.caption("Top labels by issue volume in the selection, split open vs. closed.") + labels = queries.issue_label_breakdown(conn, repos) + if labels.empty: + st.info("No labelled issues for the current selection.") + else: + long = labels[["label_name", "open", "closed"]].melt( + id_vars="label_name", + value_vars=["open", "closed"], + var_name="state", + value_name="issues", + ) + chart = ( + alt.Chart(long) + .mark_bar() + .encode( + x=alt.X("issues:Q", title="Issues"), + y=alt.Y("label_name:N", title="Label", sort="-x"), + color=alt.Color("state:N", title="State"), + tooltip=["label_name", "state", "issues"], + ) + ) + st.altair_chart(chart, width="stretch") diff --git a/dbt/models/marts/_exposures.yml b/dbt/models/marts/_exposures.yml new file mode 100644 index 0000000..efb9843 --- /dev/null +++ b/dbt/models/marts/_exposures.yml @@ -0,0 +1,27 @@ +version: 2 + +exposures: + - name: repolytics_dashboard + label: Repolytics Analytics Dashboard + type: application + maturity: high + url: http://localhost:8501 + description: > + Streamlit serving layer (`dashboard/`) that answers the project's driving + questions - project health & popularity, development velocity, contributor + retention & bus factor, and release cadence - by querying the marts read-only. + depends_on: + - ref('fct_commits') + - ref('fct_pull_requests') + - ref('fct_issues') + - ref('fct_releases') + - ref('fct_daily_downloads') + - ref('fct_repository_metrics') + - ref('fct_contributor_activity_monthly') + - ref('bridge_issue_labels') + - ref('dim_repositories') + - ref('dim_contributors') + - ref('dim_labels') + - ref('dim_dates') + owner: + name: Kacper Neumann diff --git a/pyproject.toml b/pyproject.toml index f697a16..0f2dc90 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,6 +12,9 @@ dependencies = [ ] [dependency-groups] +dashboard = [ + "streamlit>=1.58.0", +] dbt = [ "dbt-core>=1.11.11", "dbt-duckdb>=1.10.1", @@ -37,6 +40,11 @@ line-length = 88 [tool.ruff.lint] select = ["E", "W", "F", "I", "UP", "B", "C4", "SIM", "PTH", "RUF"] +[tool.ruff.lint.isort] +# `lib`/`views` are the dashboard app's own packages (imported relative to the +# dashboard/ dir, which Streamlit puts on sys.path), so group them as first-party. +known-first-party = ["lib", "views"] + [tool.pytest.ini_options] testpaths = ["tests/unit", "tests/integration"] addopts = "-ra --strict-markers --strict-config" diff --git a/uv.lock b/uv.lock index 46bb55e..3eac592 100644 --- a/uv.lock +++ b/uv.lock @@ -3,9 +3,13 @@ revision = 3 requires-python = ">=3.13" resolution-markers = [ "python_full_version >= '3.14' and platform_python_implementation != 'PyPy' and sys_platform != 'emscripten'", - "(python_full_version >= '3.14' and platform_python_implementation == 'PyPy') or (python_full_version >= '3.14' and sys_platform == 'emscripten')", + "python_full_version >= '3.14' and platform_python_implementation == 'PyPy' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and platform_python_implementation == 'PyPy' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version < '3.14' and platform_python_implementation != 'PyPy' and sys_platform != 'emscripten'", - "(python_full_version < '3.14' and platform_python_implementation == 'PyPy') or (python_full_version < '3.14' and sys_platform == 'emscripten')", + "python_full_version < '3.14' and platform_python_implementation == 'PyPy' and sys_platform == 'win32'", + "python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version < '3.14' and platform_python_implementation == 'PyPy' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] [[package]] @@ -79,6 +83,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d2/29/6533c317b74f707ea28f8d633734dbda2119bbadfc61b2f3640ba835d0f7/alembic-1.18.4-py3-none-any.whl", hash = "sha256:a5ed4adcf6d8a4cb575f3d759f071b03cd6e5c7618eb796cb52497be25bfe19a", size = 263893, upload-time = "2026-02-10T16:00:49.997Z" }, ] +[[package]] +name = "altair" +version = "6.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "jsonschema" }, + { name = "narwhals" }, + { name = "packaging" }, + { name = "typing-extensions", marker = "python_full_version < '3.15'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/a1/5e6cc638a66da48cfc89a79c2f4810dfec00b63385f9b009ab1f069779bb/altair-6.2.2.tar.gz", hash = "sha256:a1ff9d9cfe81c75414641826312b9471780e19d39293ba0b012933f6b6cba0fe", size = 766606, upload-time = "2026-06-23T12:47:13.384Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/99/d6031f4f146298951c46b1bf1cc160c2a63f6e44b3c13a30054add100d5f/altair-6.2.2-py3-none-any.whl", hash = "sha256:94014f8ad8617c3cb163d1137359cd6db5ba134b9b46d93cfd8b609fd245a583", size = 797613, upload-time = "2026-06-23T12:47:11.451Z" }, +] + [[package]] name = "annotated-doc" version = "0.0.4" @@ -356,6 +376,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, ] +[[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 = "cachetools" version = "7.1.4" @@ -396,7 +425,7 @@ name = "cffi" version = "2.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pycparser", marker = "implementation_name != 'PyPy'" }, + { name = "pycparser", marker = "(implementation_name != 'PyPy' and platform_python_implementation != 'PyPy') or (implementation_name != 'PyPy' and sys_platform == 'emscripten')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } wheels = [ @@ -1759,6 +1788,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/90/91/56c5d560f20e6c20e9e4f55bd0e458f7f162aa689ee350346c04c48eac0b/msgspec-0.21.1-cp314-cp314t-win_arm64.whl", hash = "sha256:0d2cc73df6058d811a126ac3a8ad63a4dfa210c82f9cf5a004802eaf4712de90", size = 183149, upload-time = "2026-04-12T21:44:48.833Z" }, ] +[[package]] +name = "narwhals" +version = "2.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/62/3c/c4ef2164a71c1a63d7f1ae411c4082c5fa872405106db60a4b7114989ad7/narwhals-2.22.1.tar.gz", hash = "sha256:d62920805a0a43b7ff8b54b0c0d3142d796f8a9301836ada37e573d6a33cbcd9", size = 647493, upload-time = "2026-06-05T12:34:34.051Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/ca/36339329c4604adbcc99c899b7eb1ce1a555c499b6a6860757dc9bfed36d/narwhals-2.22.1-py3-none-any.whl", hash = "sha256:60567d774edf77db53906f89d9fbd164e66e56d66d388e1e6990f17ac33cfb53", size = 454815, upload-time = "2026-06-05T12:34:32.289Z" }, +] + [[package]] name = "natsort" version = "8.4.0" @@ -1777,6 +1815,46 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, ] +[[package]] +name = "numpy" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/05/3d27272d30698dc0ecb7fdfaa41ad70303b444f81722bb99bce1d818638a/numpy-2.5.0.tar.gz", hash = "sha256:5a129578019311b6e56bdd714250f19b518f7dceeeb8d1af5490f4942d3f891c", size = 20652461, upload-time = "2026-06-21T20:57:51.95Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/33/07675aaad7f26ea013d5e884d9a0d784b79c6bd7566c333f5a52fa3c610b/numpy-2.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:520e6b8be0a4b65840ac8090d4f51cef4bed66e2b0894d5a520f099adc24a9b2", size = 16784890, upload-time = "2026-06-21T20:56:40.799Z" }, + { url = "https://files.pythonhosted.org/packages/85/4b/953118a730ee3b35e28645e0eb4cf9beec5bdbb954e1ac2f5fcefba6bbc3/numpy-2.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:146b81cdd3967fdb6beca8ba25f00c58741d8f3cbd797f55af0fbe0bfec3469c", size = 11754584, upload-time = "2026-06-21T20:56:43.094Z" }, + { url = "https://files.pythonhosted.org/packages/44/9b/56dd530c367c74ae17411027cea4135ca57e1e0583bf5594cee18bd83217/numpy-2.5.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:126b88d95e8ff9b00c9e717aa540469f21d6180162f84c0caec51b16215d49cd", size = 5313904, upload-time = "2026-06-21T20:56:45.503Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b0/bcd672edad27ecca7da1f7bb0ce72cd1706a4f2d79ae94990afc97c13e1c/numpy-2.5.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d4313cef1594c5ce46c31b6e54e918338f63f16ee9322304e8c9114d6d81c8bd", size = 6648504, upload-time = "2026-06-21T20:56:47.567Z" }, + { url = "https://files.pythonhosted.org/packages/80/9e/15cdfcbd30a1544a46c9e487a00df331c4672450216538705a9e51fa6710/numpy-2.5.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:750fb097caf26fa878746d9d119f6f9da12dedcbff1eea966c3e3447647c4a9e", size = 15150086, upload-time = "2026-06-21T20:56:49.352Z" }, + { url = "https://files.pythonhosted.org/packages/32/4e/8d7656ccaab3e81e97258b8a9bc5f0c8502513a92fb4ceb0a2cbfebc17bf/numpy-2.5.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3893adc2dc7c0412ba76777db55a049215d99c9aa3113003be8f49f4f1290ab9", size = 16647250, upload-time = "2026-06-21T20:56:51.542Z" }, + { url = "https://files.pythonhosted.org/packages/3c/81/97060281b602ed07f21b12f4ec409eac1f75a2f91fbc829ed8b2becf3ad4/numpy-2.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:835e454dd99b238cdc5a3f63bce2371296f5ebc53ca1e0f8e6ddbb6d92a29aab", size = 16512864, upload-time = "2026-06-21T20:56:55.401Z" }, + { url = "https://files.pythonhosted.org/packages/33/ab/4496208146911f8d8ddb54f68a972aafa6c8d44babcb2ea03b0e5cc87c9d/numpy-2.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6f9836778081a0a3c02a6a21493f3e9f5b311f8d2541934f31f05583dc999ea4", size = 18408407, upload-time = "2026-06-21T20:56:57.75Z" }, + { url = "https://files.pythonhosted.org/packages/d4/9f/a4df67c181e4ee8b467aa3332dc2db10fd5c515136831302f3ca48bc0a01/numpy-2.5.0-cp313-cp313-win32.whl", hash = "sha256:0b525be4744b60bb0557ac872d53ef07d085b5f39622bc579c98d3809d05b988", size = 6054431, upload-time = "2026-06-21T20:57:00.016Z" }, + { url = "https://files.pythonhosted.org/packages/30/53/491e1c47c55b62ccc6a63c1c5b8635c73fc2258dddeb9bda27cae4a0ae96/numpy-2.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:44353e2878930039db472b99dc353d749826e4010bd4d2a7f835e94a97a5c748", size = 12414420, upload-time = "2026-06-21T20:57:01.815Z" }, + { url = "https://files.pythonhosted.org/packages/eb/4a/25c2906f541e9d9f4c5769764db732e6627be91a13f4724fa10634d77db4/numpy-2.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:48f54b00711f83a5f796b70c518e8c2b3c5848dda03a54911f23eb68519b9b60", size = 10339533, upload-time = "2026-06-21T20:57:03.961Z" }, + { url = "https://files.pythonhosted.org/packages/86/ad/abc44aaceaf7b17ee1edde2bbb4458da591bc79574cffff50c4bb35f00d1/numpy-2.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f27582c55ba4c750b7c58c8faf021d2cd9324a662b466229db8a417b41368af9", size = 16783807, upload-time = "2026-06-21T20:57:06.253Z" }, + { url = "https://files.pythonhosted.org/packages/5d/39/b72e168daf9c00fb20c9fc996d00437ccecdef3102387775d29d7a62576d/numpy-2.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:28e7137057d551e4a83c4ae414e3451f50568409db7569aacc7f9811ee06a446", size = 11765215, upload-time = "2026-06-21T20:57:08.547Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a0/8400a9c0e3625182347593f5e1f57da9a617a534794805c8df5518154ddc/numpy-2.5.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:e1da54b53e75cd9fcfc23efcc7edab2c6aecf97b6037566d8a0fe804af8ec57c", size = 5324493, upload-time = "2026-06-21T20:57:11.012Z" }, + { url = "https://files.pythonhosted.org/packages/f6/8c/0d104deaa0401c93395a629ec902891618a2eff76d19229139cb5a887bfc/numpy-2.5.0-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:694d8f74e156f7fd01179f1aa8faa2f648ab6ae0f70b6c3fe57a03249aea2303", size = 6645211, upload-time = "2026-06-21T20:57:12.919Z" }, + { url = "https://files.pythonhosted.org/packages/6a/d9/4a4a628c812750363786afc3d33492709a5cd64b215469c16b0f6c7bb811/numpy-2.5.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1a7569a7b53c77716f036bb28cb1c91f166a26ec7d9502cd1e4bdfe502fdec22", size = 15166004, upload-time = "2026-06-21T20:57:14.717Z" }, + { url = "https://files.pythonhosted.org/packages/a0/5e/2a902317d7fc4aa93236e80c932662dadfc459b323d758329e01775125e1/numpy-2.5.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:39a0433bd4086ebd462960cf375e19195bb07b53dc1d87dd5fcf47ad78576f03", size = 16650797, upload-time = "2026-06-21T20:57:16.906Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a0/a0090e6329f4ca5992c07847bb579c5259a19953dc57255bb08793142ffb/numpy-2.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:929f0c79ac38bcbd7154fe631dc907abfeddbcc5027a896bd1f7767323271e7a", size = 16524647, upload-time = "2026-06-21T20:57:19.165Z" }, + { url = "https://files.pythonhosted.org/packages/5e/7d/6caf27734c42b65837e7461ed0dbbd6b6fc835060c9714ec59d673bb383a/numpy-2.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cc4f247a47bbf070bfd70be53ccdcf47b800af563535e7bbe172322197c30e21", size = 18411841, upload-time = "2026-06-21T20:57:21.638Z" }, + { url = "https://files.pythonhosted.org/packages/13/dc/26edadbd812536769a82c2e9e002234e33feb5da43061d47a044f6d309b7/numpy-2.5.0-cp314-cp314-win32.whl", hash = "sha256:5dc71423499fab3f46f7a7201155ade1669ea101f2f429d332df9e72f8161731", size = 6106361, upload-time = "2026-06-21T20:57:23.844Z" }, + { url = "https://files.pythonhosted.org/packages/f2/9e/4dd1459282229a72d92dece2ae9138e5cac94a72263a7ceb48f37434c925/numpy-2.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:ebb81d9d5443e0309d6c54894c3fbed74ad7da0714352a67b6d773cd189eae73", size = 12551749, upload-time = "2026-06-21T20:57:25.945Z" }, + { url = "https://files.pythonhosted.org/packages/05/a7/6bc6384c080b86c7f6c85c5bc5b540b24f4f679cd144791d99574e90d462/numpy-2.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:3b94d0d0deceebfad3e67ae5c0e5eb87371e8f7a0581cd04a779928c2450cf1e", size = 10617072, upload-time = "2026-06-21T20:57:28.175Z" }, + { url = "https://files.pythonhosted.org/packages/86/6b/4a2b71d66ada5608ae02b63f150dfad520f6940721cb7f029ad270befc0e/numpy-2.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:22f3d43e362d650bc39db1f17851302874a148ca95ba6981c1dfb5fa6862f35b", size = 11881067, upload-time = "2026-06-21T20:57:30.104Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b2/d365eb40a20efb49d67e9feb90494ed8511282ee1f5fa16006675c65397d/numpy-2.5.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:243563efb4cd7528a264567e9fd206c87826457322521d06206a00bfa316c927", size = 5440290, upload-time = "2026-06-21T20:57:32.193Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5e/e9c03188de5f9b767e46a8fe988bcfd3efad066a4a3fda8b9cb11a93f895/numpy-2.5.0-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:84881d825ca75249b189bbee875fcfe3238aa5c479e6100893cda566e8e86826", size = 6748371, upload-time = "2026-06-21T20:57:33.933Z" }, + { url = "https://files.pythonhosted.org/packages/fd/1d/68c186a38a5027bae2c4ddd5ea681fdaf8b4d30fb7301def6d8ad270390f/numpy-2.5.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cda12aa4779d42b8771180aba759c96f527d43446d8f380ab59e2b35e8489efd", size = 15214643, upload-time = "2026-06-21T20:57:35.677Z" }, + { url = "https://files.pythonhosted.org/packages/8c/67/73f67b7c7e20635baae9c4c3ead4ae7326a005900297a6110971abd62eb5/numpy-2.5.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c0121101093d2bd74981b10f8837d78e794a8ff57834eb27179f49e1ba11ac6", size = 16690128, upload-time = "2026-06-21T20:57:38.159Z" }, + { url = "https://files.pythonhosted.org/packages/eb/05/d4c1fb0c46d02a27d6b2b8b319a78c90937acec8631c1641874670b31e6f/numpy-2.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d371c92cfa09da00022f501ab67fafaea813d752eb30ac44336d45b1e5b0268a", size = 16577902, upload-time = "2026-06-21T20:57:40.447Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1d/771c797d50fa26e4888989cccf1d50ee51f530d4e455ad2692dcb64fa711/numpy-2.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9990713e9c38154c6861e7547f1e3fc7a87e75ff09bab24ef1cc81d81c2835e9", size = 18452814, upload-time = "2026-06-21T20:57:42.875Z" }, + { url = "https://files.pythonhosted.org/packages/e8/46/52fc0d2a68d7643f0f149eeea5a5d8ea2a3507056ac8afa83c9212606e8b/numpy-2.5.0-cp314-cp314t-win32.whl", hash = "sha256:edadfbd4794b1086c0d822f81863e8a68fc129d132fd0bb9e31e955d7fbbbdb7", size = 6253168, upload-time = "2026-06-21T20:57:45.101Z" }, + { url = "https://files.pythonhosted.org/packages/2a/be/6c8d1118b5f13b2881dc095d5b345de19c6638b8959c17409b6eff84c8aa/numpy-2.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f7e5fa4382967ae6548bd2f174219afb908e294b0d5f625af01166edd5f7d9aa", size = 12736286, upload-time = "2026-06-21T20:57:46.935Z" }, + { url = "https://files.pythonhosted.org/packages/fd/6a/d3a169aaf8536cf228d56a09e04bcb713a2fe4410d4e2105b9419b5a9c89/numpy-2.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:016623417bb330d719d579daf2d6b9a01ddc52e41a9ed61a47f39fde46dcd865", size = 10686451, upload-time = "2026-06-21T20:57:49.313Z" }, +] + [[package]] name = "openlineage-integration-common" version = "1.50.0" @@ -1998,6 +2076,50 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, ] +[[package]] +name = "pandas" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/87/4341c6252d1c47b08768c3d25ac487362bf403f0313ddae4a2a26c9b1b4c/pandas-3.0.3.tar.gz", hash = "sha256:696a4a00a2a2a35d4e5deb3fc946641b96c944f02230e4f76137fe35d806c4fc", size = 4651414, upload-time = "2026-05-11T18:54:29.21Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/90/62d8302883c44308c477e222c3daf7c813a34c8e96985882fbd53d964352/pandas-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:67b3b64c11910cfa29f4e94a14d3bff9ee693b6fc76055e7cad549cee0aec5fa", size = 10331071, upload-time = "2026-05-11T18:52:58.838Z" }, + { url = "https://files.pythonhosted.org/packages/7f/ae/6a6493c783a101f165e4356953ba3c74d6f77f0042fa7d753da9dfbb640c/pandas-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:39436b377d56d2a2e52d0395bdbee171f01068e99af5250509aceeb929f765c7", size = 9875690, upload-time = "2026-05-11T18:53:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/62/7c/5df8e9f56c69a2769fbe9382a5ef8f2658c007e376434e1e2cbb57ad895f/pandas-3.0.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4be06d68f9ddcfc645b87534911da79a8fbffc7573c80e0edcf42a5020624d8", size = 10381634, upload-time = "2026-05-11T18:53:04.393Z" }, + { url = "https://files.pythonhosted.org/packages/99/68/1237369725aa617bb358263d535803e3053fdbc593513ec5ed9c9896b5b6/pandas-3.0.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a4eeb6830daf35a71cc09649bd823e2b542dac246cdee9614c6e4bd65028cd6a", size = 10891243, upload-time = "2026-05-11T18:53:07.643Z" }, + { url = "https://files.pythonhosted.org/packages/25/93/77d108e8af7222b4a503ebde0e30215b1c2e4f8e53a526431890f22d5586/pandas-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1928e07221f82db493cd4af1e23c1bfca524a19a4699887975bff68f49a72bfb", size = 11388659, upload-time = "2026-05-11T18:53:10.634Z" }, + { url = "https://files.pythonhosted.org/packages/d0/bd/eff5b4399f332ac386c853f6cd2bd3fa2ca0061b9f36ecd9c4d7c4265649/pandas-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51b1fe551acb77dac643c6fda86084d8d446c10fe64b06a9cc29c4cc8540e7f2", size = 11942880, upload-time = "2026-05-11T18:53:13.536Z" }, + { url = "https://files.pythonhosted.org/packages/2c/20/559ace4200982c3887d0b86bfd0d856a2143ef8ddab63cc07934951a964c/pandas-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:a82d532a3351d435432cd913edbccaf8b8e01d4dd0e5ced5a8d2e8ecd94c7e44", size = 9757091, upload-time = "2026-05-11T18:53:16.306Z" }, + { url = "https://files.pythonhosted.org/packages/3a/66/69055a09fe200f29f922a3eeec4804611900b95f52d932ece3393c3c0c19/pandas-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:275c14e0fce14a2ec20eee474aecd305478ea3c1e6f6a9d8fe219a165542717e", size = 9057282, upload-time = "2026-05-11T18:53:18.768Z" }, + { url = "https://files.pythonhosted.org/packages/57/0e/efe801b0e6811e8e650cd21b7f2608e30f08a7067e2bf6e8752b0d56ee3c/pandas-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:46997386d528eb40376ecd6b033cf4a8a1e5282580f68f43de875b78cba2199d", size = 10767016, upload-time = "2026-05-11T18:53:21.227Z" }, + { url = "https://files.pythonhosted.org/packages/ea/dc/eb55135a1d5f0f0519f28da1f609a206d2cad1f9c35c32d51e38dd7261ae/pandas-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:261e308dfb22448384b7580cf719d2f998fe2966c92893c3e77d14008af1f066", size = 10420210, upload-time = "2026-05-11T18:53:23.982Z" }, + { url = "https://files.pythonhosted.org/packages/c6/3e/b1d5d955ce33ffecb407465a60bc32769d74fcf68224b7ae67ae11d4dea4/pandas-3.0.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dd1a5d1def6a46002e964510bdc67c368aa0951df5d1d9f8365336f5a1f490cd", size = 10336126, upload-time = "2026-05-11T18:53:26.731Z" }, + { url = "https://files.pythonhosted.org/packages/f5/76/a01261711ab60a22d71b862f0de20e4c504bf80457270ad8cb42110f6abc/pandas-3.0.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d72828c20c6d6e83e1e22a6a3b47b326b71664112fa9705dcbccfd7a39b62085", size = 10728051, upload-time = "2026-05-11T18:53:29.125Z" }, + { url = "https://files.pythonhosted.org/packages/e9/21/ea191195e587b18cf682e97f433f81b2d0fbe341380e80a3e0d6e4403c8e/pandas-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d26cbe1fcfc12e8fd900e2454163e466b2d3af84f7c75481df7683ffc073d870", size = 11350796, upload-time = "2026-05-11T18:53:32.056Z" }, + { url = "https://files.pythonhosted.org/packages/64/69/f0eaaf54939f0e8c6768fd06be9af2cef9b36048b96dfb9e1b2c685a807e/pandas-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3e91cec1879ada0624fc3dc9953c5cbd60208e59c0db28f540c5d6d47502422f", size = 11799741, upload-time = "2026-05-11T18:53:34.985Z" }, + { url = "https://files.pythonhosted.org/packages/45/a4/865e0e510cae5fc2194de4db28be638952de942571ba9125934fd9c01d47/pandas-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:08d789b41f87e0905880e293cedf6197ce71fe67cc081358b1e148a491b9bd13", size = 10499958, upload-time = "2026-05-11T18:53:37.857Z" }, + { url = "https://files.pythonhosted.org/packages/86/54/effdcc3c0ff7a08037889200e148ebe94c16c4f653be078c7b3675955df1/pandas-3.0.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3650109c0f22879df8bd6179ab9ee3d7f1d1d4e7e0094a3f0032d9f51e2e64ac", size = 10336065, upload-time = "2026-05-11T18:53:41.099Z" }, + { url = "https://files.pythonhosted.org/packages/68/10/bf2d6738d72748b961a3751ab89522d58c54efc36a8e1a12161216cd45cf/pandas-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bab900348131a7db1f69a7309ef141fd5680f1487094193bcbbb61791573bf8f", size = 9926101, upload-time = "2026-05-11T18:53:43.515Z" }, + { url = "https://files.pythonhosted.org/packages/ae/e9/e35cf11c8a136e757b956f5f0efdcaa50aecde85ea055f1898dfc68262f3/pandas-3.0.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba7e08b9ac1d54569cd1e256e3668975ed624d6826f7b68df0342b012007bddb", size = 10457553, upload-time = "2026-05-11T18:53:46.394Z" }, + { url = "https://files.pythonhosted.org/packages/58/3b/1cdec6772bdbaf7b25dab360c59f03cadf05492dd724c6540af905389b07/pandas-3.0.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d71c63ae4ebdbf70209742096f1fc46a83a0613c99d4b23766cced9ff8cd62a", size = 10914065, upload-time = "2026-05-11T18:53:49.134Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c2/1ef644445fcd72e3627bceec77e3560636f87ddce4ed841afe76b83b5bf9/pandas-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e3a2ec42c98ffa2565a67e08e218d06d72576d758d90facb7c00805194d8f360", size = 11459188, upload-time = "2026-05-11T18:53:52.527Z" }, + { url = "https://files.pythonhosted.org/packages/7e/49/4d8d4f42cbc9c4adc7a1870f269c02cbd6cd40d059622c06fb298addcbad/pandas-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:335f62418ed562cfc3c49e9e196375c28b729dcef8543abf4f9438e381bf3c76", size = 11982966, upload-time = "2026-05-11T18:53:55.043Z" }, + { url = "https://files.pythonhosted.org/packages/38/55/792619469bab9882d8bbd5865d45a72f6478762d04a9af4bf0d08c503e95/pandas-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:3c20a521bbb85902f79f7270c80a59e1b5452d96d170c034f207181870f97ac5", size = 9876755, upload-time = "2026-05-11T18:53:58.067Z" }, + { url = "https://files.pythonhosted.org/packages/2a/af/33c469653b0ba03b50c3a98192d4c07f0c75c66b263ceb097fce0ee97d31/pandas-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:a2d2dff8a04f3917b55ab3910c32990f8ddf7eceba114947838cefa976a68977", size = 9198658, upload-time = "2026-05-11T18:54:00.733Z" }, + { url = "https://files.pythonhosted.org/packages/a2/fa/b8c257bd76b8bd060c3a9151c1fca05e9b9c5e3af5d0f549c0356f6d143d/pandas-3.0.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:0d589105b3c14645af1738ff279b2995102d8f7a03b0a66dc8d95550eb513e04", size = 10787242, upload-time = "2026-05-11T18:54:03.564Z" }, + { url = "https://files.pythonhosted.org/packages/54/eb/f19206ffb0bf1919002969aa448b4702c6594845156a6f8050674855aac3/pandas-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:13fc1e853d9e04743d11ba75a985ccbc2a317fe07d8af61e445a6fd24dacd6a6", size = 10436369, upload-time = "2026-05-11T18:54:06.311Z" }, + { url = "https://files.pythonhosted.org/packages/fd/24/c7c39fb4fe22b71a0c2d78bf0c585c600092d85f94f086d2b3b2f6ca27e2/pandas-3.0.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:819959dab7bbd0049c15623fbac4e29a191b9528160a61fb1032242d8ced2d9c", size = 10358306, upload-time = "2026-05-11T18:54:09.085Z" }, + { url = "https://files.pythonhosted.org/packages/16/ec/dd2a9eb7fa1204df88c0864164e35b228ac581062ac612ba0a67fd812e4c/pandas-3.0.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:60ae316d3fd75d1858d450d0db0103ea2be3e7d4a95ec2f064f7e2ae63f7b028", size = 10758394, upload-time = "2026-05-11T18:54:11.956Z" }, + { url = "https://files.pythonhosted.org/packages/95/6e/00c61ea8e85b4f6d8d35e11852a1a4998fc7fafc91c6a602d1cc9c972d64/pandas-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd3a518890b400d32f9023722dc9a9a5c969f00b415419a3c06c043f09bb5d7d", size = 11375717, upload-time = "2026-05-11T18:54:14.539Z" }, + { url = "https://files.pythonhosted.org/packages/31/89/8fc1c268969fac43688d65fd92e67df24bd128d53cb4d2eee534cd307399/pandas-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9c39be2d709d01fa972a0cabc522389fceca4f3969332ba25a7d6c5802cf976a", size = 11828897, upload-time = "2026-05-11T18:54:17.146Z" }, + { url = "https://files.pythonhosted.org/packages/56/3b/e7d20dea247a3e6dc0bd8a6953854afbedc03951def4e7371e05e7263e25/pandas-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4db8c527972a821cf5286b40ccc57642a39bc62e62022b42f99f8a67fca8c3a1", size = 10900855, upload-time = "2026-05-11T18:54:19.72Z" }, + { url = "https://files.pythonhosted.org/packages/0f/54/68a0978d1ef8502b8492099beaa6e7a0c1b32e3b5d4f677f5810cb08711c/pandas-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b2c95f8bfc1ee412bf482605d7bfd30c12d1d26bd59fdd91efeef1d4718decb1", size = 9466464, upload-time = "2026-05-11T18:54:22.754Z" }, +] + [[package]] name = "parsedatetime" version = "2.6" @@ -2067,6 +2189,64 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/02/fb/d65db067a67df7252f18b0cb7420dda84078b9e8bfb375215469c14a50be/pendulum-3.2.0-py3-none-any.whl", hash = "sha256:f3a9c18a89b4d9ef39c5fa6a78722aaff8d5be2597c129a3b16b9f40a561acf3", size = 114111, upload-time = "2026-01-30T11:22:22.361Z" }, ] +[[package]] +name = "pillow" +version = "12.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" }, + { url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" }, + { url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" }, + { url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" }, + { url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" }, + { url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" }, + { url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" }, + { url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" }, + { url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" }, + { url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" }, + { url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" }, + { url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" }, + { url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" }, + { url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" }, + { url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" }, + { url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" }, + { url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" }, + { url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" }, + { url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" }, + { url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" }, + { url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" }, + { url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" }, + { url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" }, + { url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" }, + { url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" }, + { url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" }, + { url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" }, + { url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" }, + { url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" }, + { url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" }, + { url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" }, + { url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" }, + { url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" }, + { url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" }, + { url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" }, +] + [[package]] name = "platformdirs" version = "4.10.0" @@ -2149,6 +2329,42 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, ] +[[package]] +name = "pyarrow" +version = "24.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/91/13/13e1069b351bdc3881266e11147ffccf687505dbb0ea74036237f5d454a5/pyarrow-24.0.0.tar.gz", hash = "sha256:85fe721a14dd823aca09127acbb06c3ca723efbd436c004f16bca601b04dcc83", size = 1180261, upload-time = "2026-04-21T10:51:25.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/d3/a1abf004482026ddc17f4503db227787fa3cfe41ec5091ff20e4fea55e57/pyarrow-24.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:02b001b3ed4723caa44f6cd1af2d5c86aa2cf9971dacc2ffa55b21237713dfba", size = 34976759, upload-time = "2026-04-21T10:48:07.258Z" }, + { url = "https://files.pythonhosted.org/packages/4f/4a/34f0a36d28a2dd32225301b79daad44e243dc1a2bb77d43b60749be255c4/pyarrow-24.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:04920d6a71aabd08a0417709efce97d45ea8e6fb733d9ca9ecffb13c67839f68", size = 36658471, upload-time = "2026-04-21T10:48:13.347Z" }, + { url = "https://files.pythonhosted.org/packages/1f/78/543b94712ae8bb1a6023bcc1acf1a740fbff8286747c289cd9468fced2a5/pyarrow-24.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:a964266397740257f16f7bb2e4f08a0c81454004beab8ff59dd531b73610e9f2", size = 45675981, upload-time = "2026-04-21T10:48:20.201Z" }, + { url = "https://files.pythonhosted.org/packages/84/9f/8fb7c222b100d314137fa40ec050de56cd8c6d957d1cfff685ce72f15b17/pyarrow-24.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6f066b179d68c413374294bc1735f68475457c933258df594443bb9d88ddc2a0", size = 48859172, upload-time = "2026-04-21T10:48:27.541Z" }, + { url = "https://files.pythonhosted.org/packages/a7/d3/1ea72538e6c8b3b475ed78d1049a2c518e655761ea50fe1171fc855fcab7/pyarrow-24.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1183baeb14c5f587b1ec52831e665718ce632caab84b7cd6b85fd44f96114495", size = 49385733, upload-time = "2026-04-21T10:48:34.7Z" }, + { url = "https://files.pythonhosted.org/packages/c3/be/c3d8b06a1ba35f2260f8e1f771abbee7d5e345c0937aab90675706b1690a/pyarrow-24.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:806f24b4085453c197a5078218d1ee08783ebbba271badd153d1ae22a3ee804f", size = 51934335, upload-time = "2026-04-21T10:48:42.099Z" }, + { url = "https://files.pythonhosted.org/packages/9c/62/89e07a1e7329d2cde3e3c6994ba0839a24977a2beda8be6005ea3d860b99/pyarrow-24.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:e4505fc6583f7b05ab854934896bcac8253b04ac1171a77dfb73efef92076d91", size = 27271748, upload-time = "2026-04-21T10:49:42.532Z" }, + { url = "https://files.pythonhosted.org/packages/17/1a/cff3a59f80b5b1658549d46611b67163f65e0664431c076ad728bf9d5af4/pyarrow-24.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:1a4e45017efbf115032e4475ee876d525e0e36c742214fbe405332480ecd6275", size = 35238554, upload-time = "2026-04-21T10:48:48.526Z" }, + { url = "https://files.pythonhosted.org/packages/a8/99/cce0f42a327bfef2c420fb6078a3eb834826e5d6697bf3009fe11d2ad051/pyarrow-24.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:7986f1fa71cee060ad00758bcc79d3a93bab8559bf978fab9e53472a2e25a17b", size = 36782301, upload-time = "2026-04-21T10:48:55.181Z" }, + { url = "https://files.pythonhosted.org/packages/2a/66/8e560d5ff6793ca29aca213c53eec0dd482dd46cb93b2819e5aab52e4252/pyarrow-24.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:d3e0b61e8efb24ed38898e5cdc5fffa9124be480008d401a1f8071500494ae42", size = 45721929, upload-time = "2026-04-21T10:49:03.676Z" }, + { url = "https://files.pythonhosted.org/packages/27/0c/a26e25505d030716e078d9f16eb74973cbf0b33b672884e9f9da1c83b871/pyarrow-24.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:55a3bc1e3df3b5567b7d27ef551b2283f0c68a5e86f1cd56abc569da4f31335b", size = 48825365, upload-time = "2026-04-21T10:49:11.714Z" }, + { url = "https://files.pythonhosted.org/packages/5f/eb/771f9ecb0c65e73fe9dccdd1717901b9594f08c4515d000c7c62df573811/pyarrow-24.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:641f795b361874ac9da5294f8f443dfdbee355cf2bd9e3b8d97aaac2306b9b37", size = 49451819, upload-time = "2026-04-21T10:49:21.474Z" }, + { url = "https://files.pythonhosted.org/packages/48/da/61ae89a88732f5a785646f3ec6125dbb640fa98a540eb2b9889caa561403/pyarrow-24.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8adc8e6ce5fccf5dc707046ae4914fd537def529709cc0d285d37a7f9cd442ca", size = 51909252, upload-time = "2026-04-21T10:49:31.164Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1a/8dd5cafab7b66573fa91c03d06d213356ad4edd71813aa75e08ce2b3a844/pyarrow-24.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:9b18371ad2f44044b81a8d23bc2d8a9b6a6226dca775e8e16cfee640473d6c5d", size = 27388127, upload-time = "2026-04-21T10:49:37.334Z" }, + { url = "https://files.pythonhosted.org/packages/ad/80/d022a34ff05d2cbedd8ccf841fc1f532ecfa9eb5ed1711b56d0e0ea71fc9/pyarrow-24.0.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:1cc9057f0319e26333b357e17f3c2c022f1a83739b48a88b25bfd5fa2dc18838", size = 35007997, upload-time = "2026-04-21T10:49:48.796Z" }, + { url = "https://files.pythonhosted.org/packages/1a/ff/f01485fda6f4e5d441afb8dd5e7681e4db18826c1e271852f5d3957d6a80/pyarrow-24.0.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:e6f1278ee4785b6db21229374a1c9e54ec7c549de5d1efc9630b6207de7e170b", size = 36678720, upload-time = "2026-04-21T10:49:55.858Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c2/2d2d5fea814237923f71b36495211f20b43a1576f9a4d6da7e751a64ec6f/pyarrow-24.0.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:adbbedc55506cbdabb830890444fb856bfb0060c46c6f8026c6c2f2cf86ae795", size = 45741852, upload-time = "2026-04-21T10:50:04.624Z" }, + { url = "https://files.pythonhosted.org/packages/8e/3a/28ba9c1c1ebdbb5f1b94dfebb46f207e52e6a554b7fe4132540fde29a3a0/pyarrow-24.0.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:ae8a1145af31d903fa9bb166824d7abe9b4681a000b0159c9fb99c11bc11ad26", size = 48889852, upload-time = "2026-04-21T10:50:12.293Z" }, + { url = "https://files.pythonhosted.org/packages/df/51/4a389acfd31dca009f8fb82d7f510bb4130f2b3a8e18cf00194d0687d8ac/pyarrow-24.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d7027eba1df3b2069e2e8d80f644fa0918b68c46432af3d088ddd390d063ecde", size = 49445207, upload-time = "2026-04-21T10:50:20.677Z" }, + { url = "https://files.pythonhosted.org/packages/19/4b/0bab2b23d2ae901b1b9a03c0efd4b2d070256f8ce3fc43f6e58c167b2081/pyarrow-24.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e56a1ffe9bf7b727432b89104cc0849c21582949dd7bdcb34f17b2001a351a76", size = 51954117, upload-time = "2026-04-21T10:50:29.14Z" }, + { url = "https://files.pythonhosted.org/packages/29/88/f4e9145da0417b3d2c12035a8492b35ff4a3dbc653e614fcfb51d9dedb38/pyarrow-24.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:38be1808cdd068605b787e6ca9119b27eb275a0234e50212c3492331680c3b1e", size = 28001155, upload-time = "2026-04-21T10:51:22.337Z" }, + { url = "https://files.pythonhosted.org/packages/79/4f/46a49a63f43526da895b1a45bbb51d5baf8e4d77159f8528fc3e5490007f/pyarrow-24.0.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:418e48ce50a45a6a6c73c454677203a9c75c966cb1e92ca3370959185f197a05", size = 35250387, upload-time = "2026-04-21T10:50:35.552Z" }, + { url = "https://files.pythonhosted.org/packages/a0/da/d5e0cd5ef00796922404806d5f00325cdadc3441ce2c13fe7115f2df9a64/pyarrow-24.0.0-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:2f16197705a230a78270cdd4ea8a1d57e86b2fdcbc34a1f6aebc72e65c986f9a", size = 36797102, upload-time = "2026-04-21T10:50:42.417Z" }, + { url = "https://files.pythonhosted.org/packages/34/c7/5904145b0a593a05236c882933d439b5720f0a145381179063722fbfc123/pyarrow-24.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:fb24ac194bfc5e86839d7dcd52092ee31e5fe6733fe11f5e3b06ef0812b20072", size = 45745118, upload-time = "2026-04-21T10:50:49.324Z" }, + { url = "https://files.pythonhosted.org/packages/13/d3/cca42fe166d1c6e4d5b80e530b7949104d10e17508a90ae202dac205ce2a/pyarrow-24.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:9700ebd9a51f5895ce75ff4ac4b3c47a7d4b42bc618be8e713e5d56bacf5f931", size = 48844765, upload-time = "2026-04-21T10:50:55.579Z" }, + { url = "https://files.pythonhosted.org/packages/b0/49/942c3b79878ba928324d1e17c274ed84581db8c0a749b24bcf4cbdf15bd3/pyarrow-24.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d8ddd2768da81d3ee08cfea9b597f4abb4e8e1dc8ae7e204b608d23a0d3ab699", size = 49471890, upload-time = "2026-04-21T10:51:02.439Z" }, + { url = "https://files.pythonhosted.org/packages/76/97/ff71431000a75d84135a1ace5ca4ba11726a231a8007bbb320a4c54075d5/pyarrow-24.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:61a3d7eaa97a14768b542f3d284dc6400dd2470d9f080708b13cd46b6ae18136", size = 51932250, upload-time = "2026-04-21T10:51:10.576Z" }, + { url = "https://files.pythonhosted.org/packages/51/be/6f79d55816d5c22557cf27533543d5d70dfe692adfbee4b99f2760674f38/pyarrow-24.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:c91d00057f23b8d353039520dc3a6c09d8608164c692e9f59a175a42b2ae0c19", size = 28131282, upload-time = "2026-04-21T10:51:16.815Z" }, +] + [[package]] name = "pyasn1" version = "0.6.3" @@ -2277,6 +2493,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ae/8d/f1af3832f5e6eb13ba94ee809e72b8ecb5eef226d27ee0bef7d963d943c7/pydantic_settings-2.14.1-py3-none-any.whl", hash = "sha256:6e3c7edfd8277687cdc598f56e5cff0e9bfff0910a3749deaa8d4401c3a2b9de", size = 60964, upload-time = "2026-05-08T13:40:04.958Z" }, ] +[[package]] +name = "pydeck" +version = "0.9.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/df/4e9e7f20f8034a37c6571c93809f6d22388c39978c98d174d656c1a18fd2/pydeck-0.9.2.tar.gz", hash = "sha256:c10d9035e81ead6385264cac8d19402471f6866a15ca1f7df1400f52142bcf87", size = 5849672, upload-time = "2026-04-16T18:30:30.089Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/24/b30ee7d723100fd822de1bb4c0adea62f3419884a75a536f35f355d1e7c0/pydeck-0.9.2-py2.py3-none-any.whl", hash = "sha256:8213dfeacc5f6bfe6825f61c8ee34e3850e8a31fc43924379ec98edb34a75b25", size = 11305615, upload-time = "2026-04-16T18:30:28.133Z" }, +] + [[package]] name = "pygments" version = "2.20.0" @@ -2532,6 +2761,9 @@ dependencies = [ ] [package.dev-dependencies] +dashboard = [ + { name = "streamlit" }, +] dbt = [ { name = "dbt-core" }, { name = "dbt-duckdb" }, @@ -2555,6 +2787,7 @@ requires-dist = [ ] [package.metadata.requires-dev] +dashboard = [{ name = "streamlit", specifier = ">=1.58.0" }] dbt = [ { name = "dbt-core", specifier = ">=1.11.11" }, { name = "dbt-duckdb", specifier = ">=1.10.1" }, @@ -2993,6 +3226,41 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6", size = 73632, upload-time = "2026-06-12T09:23:10.017Z" }, ] +[[package]] +name = "streamlit" +version = "1.58.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "altair" }, + { name = "anyio" }, + { name = "blinker" }, + { name = "cachetools" }, + { name = "click" }, + { name = "gitpython" }, + { name = "httptools" }, + { name = "itsdangerous" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pandas" }, + { name = "pillow" }, + { name = "protobuf" }, + { name = "pyarrow" }, + { name = "pydeck" }, + { name = "python-multipart" }, + { name = "requests" }, + { name = "starlette" }, + { name = "tenacity" }, + { name = "toml" }, + { name = "typing-extensions" }, + { name = "uvicorn" }, + { name = "watchdog", marker = "sys_platform != 'darwin'" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/74/20dac6d6200d6ec0e1c230fb8eeb6a1a423645eacb76e8d802adfc246456/streamlit-1.58.0.tar.gz", hash = "sha256:78a22e7085b053af7ce544442bf4b670771e68c509ba1bdaa056ba0708f49c3d", size = 8721149, upload-time = "2026-05-28T18:02:44.606Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/84/14c36a92fb24f8e1cea452f53b0744b5da69d52cdd2fe22e71e6fbf765d5/streamlit-1.58.0-py3-none-any.whl", hash = "sha256:4ca8a7afc5bd16a5f176ccf4be1e34e8121cad0240becd127fb58a103ea3178d", size = 9219185, upload-time = "2026-05-28T18:02:41.993Z" }, +] + [[package]] name = "structlog" version = "26.1.0" @@ -3050,6 +3318,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a6/a5/c0b6468d3824fe3fde30dbb5e1f687b291608f9473681bbf7dabbf5a87d7/text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8", size = 78154, upload-time = "2019-08-30T21:37:03.543Z" }, ] +[[package]] +name = "toml" +version = "0.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253, upload-time = "2020-11-01T01:40:22.204Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588, upload-time = "2020-11-01T01:40:20.672Z" }, +] + [[package]] name = "tomlkit" version = "0.15.0" @@ -3209,6 +3486,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2c/02/3623e6169bed617ed1e2d372f7c69f92ec28d54c4dfc997055c8578ec148/virtualenv-21.5.1-py3-none-any.whl", hash = "sha256:55aa670b67bbfb991b03fda39bd3276d92c419d702376e98c5df1c9989a26783", size = 4558820, upload-time = "2026-06-16T16:23:56.963Z" }, ] +[[package]] +name = "watchdog" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, +] + [[package]] name = "watchfiles" version = "1.2.0"