feat: add desktop packaging (pywebview + PyInstaller)

- resource_path.py: frozen/dev path resolution for bundled data files
- app_desktop.py: pywebview entry point (Dash in daemon thread)
- app.spec: PyInstaller onedir config with data files and hidden imports
- Updated queries.py, card_browser.py, app.py to use get_resource_path()
- Added pywebview + pyinstaller to project dependencies
- Fixed unresolved merge conflict in .gitignore
- Removed stale 01_nhs_classic.html and AdditionalAnalytics.md
This commit is contained in:
Andrew Charlwood
2026-02-09 14:53:22 +00:00
parent ee56595292
commit 7e63e6ea45
11 changed files with 491 additions and 720 deletions
-6
View File
@@ -36,16 +36,10 @@ logs/*.jsonl
.web/
.states/
<<<<<<< Updated upstream
# SQLite database (will contain local data)
#*.db
#*.sqlite
=======
# SQLite databases (except pathways.db which contains pre-computed data)
*.db
!data/pathways.db
*.sqlite
>>>>>>> Stashed changes
# Snowflake result cache
data/cache/
-551
View File
@@ -1,551 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HCD Analysis — NHS Classic</title>
<link href="https://fonts.googleapis.com/css2?family=Source+Sans+3:wght@300;400;600;700;900&display=swap" rel="stylesheet">
<style>
:root {
--nhs-blue: #005EB8;
--nhs-dark-blue: #003087;
--nhs-light-blue: #41B6E6;
--nhs-white: #FFFFFF;
--nhs-pale-grey: #E8EDEE;
--nhs-mid-grey: #768692;
--nhs-dark-grey: #425563;
--nhs-green: #009639;
--nhs-yellow: #FFB81C;
--nhs-red: #DA291C;
--sidebar-w: 240px;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Source Sans 3', Arial, sans-serif;
background: #F0F4F5;
color: var(--nhs-dark-grey);
line-height: 1.5;
min-height: 100vh;
}
/* ── Top Header ── */
.top-header {
position: fixed; top: 0; left: 0; right: 0; z-index: 200;
height: 56px;
background: var(--nhs-dark-blue);
display: flex; align-items: center; justify-content: space-between;
padding: 0 24px 0 0;
}
.top-header__brand {
display: flex; align-items: center; gap: 16px;
height: 100%; padding: 0 24px;
background: var(--nhs-blue);
clip-path: polygon(0 0, calc(100% - 16px) 0, 100% 100%, 0 100%);
padding-right: 40px;
}
.top-header__logo {
width: 40px; height: 40px;
background: var(--nhs-white);
border-radius: 4px;
display: grid; place-items: center;
font-weight: 900; font-size: 11px; color: var(--nhs-blue);
letter-spacing: 0.5px;
line-height: 1;
}
.top-header__title {
color: var(--nhs-white);
font-size: 20px; font-weight: 700;
letter-spacing: -0.01em;
}
.top-header__breadcrumb {
color: rgba(255,255,255,0.7);
font-size: 14px; font-weight: 400;
}
.top-header__breadcrumb strong { color: var(--nhs-white); font-weight: 600; }
.top-header__right {
display: flex; align-items: center; gap: 20px; color: rgba(255,255,255,0.8); font-size: 14px;
}
.top-header__right .status-dot {
width: 8px; height: 8px; border-radius: 50%; background: var(--nhs-green);
display: inline-block; margin-right: 4px;
}
/* ── Sidebar ── */
.sidebar {
position: fixed; top: 56px; left: 0; bottom: 0;
width: var(--sidebar-w);
background: var(--nhs-white);
border-right: 1px solid var(--nhs-pale-grey);
overflow-y: auto; z-index: 100;
display: flex; flex-direction: column;
}
.sidebar__section { padding: 16px 0; }
.sidebar__section + .sidebar__section { border-top: 1px solid var(--nhs-pale-grey); }
.sidebar__label {
padding: 0 20px 8px;
font-size: 11px; font-weight: 700;
text-transform: uppercase; letter-spacing: 0.08em;
color: var(--nhs-mid-grey);
}
.sidebar__item {
display: flex; align-items: center; gap: 12px;
padding: 10px 20px;
font-size: 15px; font-weight: 400;
color: var(--nhs-dark-grey);
text-decoration: none;
border-left: 4px solid transparent;
transition: background 0.15s, border-color 0.15s;
cursor: pointer;
}
.sidebar__item:hover { background: #F0F4F5; }
.sidebar__item--active {
background: #E8F0FE;
border-left-color: var(--nhs-blue);
color: var(--nhs-blue);
font-weight: 600;
}
.sidebar__item svg { width: 18px; height: 18px; flex-shrink: 0; }
.sidebar__footer {
margin-top: auto; padding: 16px 20px;
border-top: 1px solid var(--nhs-pale-grey);
font-size: 12px; color: var(--nhs-mid-grey);
}
/* ── Main Content ── */
.main {
margin-left: var(--sidebar-w);
margin-top: 56px;
padding: 24px;
min-height: calc(100vh - 56px);
display: flex; flex-direction: column; gap: 20px;
}
/* ── KPI Row ── */
.kpi-row {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
}
.kpi-card {
background: var(--nhs-white);
border: 1px solid var(--nhs-pale-grey);
border-top: 4px solid var(--nhs-blue);
padding: 20px;
display: flex; flex-direction: column; gap: 2px;
}
.kpi-card--green { border-top-color: var(--nhs-green); }
.kpi-card__label {
font-size: 12px; font-weight: 600;
text-transform: uppercase; letter-spacing: 0.05em;
color: var(--nhs-mid-grey);
}
.kpi-card__value {
font-size: 32px; font-weight: 300;
color: var(--nhs-dark-blue);
line-height: 1.1;
font-variant-numeric: tabular-nums;
}
.kpi-card__sub {
font-size: 13px; color: var(--nhs-mid-grey); margin-top: 4px;
}
/* ── Filter Bar ── */
.filter-bar {
background: var(--nhs-white);
border: 1px solid var(--nhs-pale-grey);
padding: 12px 20px;
display: flex; align-items: center; gap: 16px;
flex-wrap: wrap;
}
.filter-bar__group {
display: flex; align-items: center; gap: 8px;
}
.filter-bar__divider {
width: 1px; height: 28px; background: var(--nhs-pale-grey);
}
.filter-bar__label {
font-size: 12px; font-weight: 600;
text-transform: uppercase; letter-spacing: 0.04em;
color: var(--nhs-mid-grey);
white-space: nowrap;
}
/* Toggle pills */
.toggle-pills {
display: flex; border: 1px solid var(--nhs-pale-grey); overflow: hidden;
}
.toggle-pill {
padding: 6px 16px;
font-size: 14px; font-weight: 600;
color: var(--nhs-dark-grey);
background: var(--nhs-white);
cursor: pointer;
border: none; outline: none;
transition: background 0.15s, color 0.15s;
}
.toggle-pill + .toggle-pill { border-left: 1px solid var(--nhs-pale-grey); }
.toggle-pill--active {
background: var(--nhs-blue);
color: var(--nhs-white);
}
.toggle-pill:hover:not(.toggle-pill--active) { background: #F0F4F5; }
.toggle-pill:focus-visible { box-shadow: 0 0 0 3px var(--nhs-yellow); z-index: 1; }
/* Selects */
.filter-select {
height: 34px; padding: 0 12px;
border: 1px solid var(--nhs-pale-grey);
font-family: inherit; font-size: 14px;
color: var(--nhs-dark-grey);
background: var(--nhs-white);
cursor: pointer;
min-width: 140px;
}
.filter-select:focus { outline: 3px solid var(--nhs-yellow); outline-offset: 0; }
/* ── Chart Area ── */
.chart-card {
background: var(--nhs-white);
border: 1px solid var(--nhs-pale-grey);
flex: 1;
display: flex; flex-direction: column;
}
.chart-card__header {
padding: 16px 20px;
border-bottom: 1px solid var(--nhs-pale-grey);
display: flex; justify-content: space-between; align-items: baseline;
}
.chart-card__title {
font-size: 18px; font-weight: 700;
color: var(--nhs-dark-blue);
}
.chart-card__subtitle {
font-size: 13px; color: var(--nhs-mid-grey);
}
.chart-card__tabs {
display: flex; gap: 0;
border-bottom: 1px solid var(--nhs-pale-grey);
}
.chart-tab {
padding: 10px 24px;
font-size: 14px; font-weight: 600;
color: var(--nhs-mid-grey);
border: none; background: none; cursor: pointer;
border-bottom: 3px solid transparent;
margin-bottom: -1px;
transition: color 0.15s, border-color 0.15s;
}
.chart-tab--active {
color: var(--nhs-blue);
border-bottom-color: var(--nhs-blue);
}
.chart-tab:hover:not(.chart-tab--active) { color: var(--nhs-dark-grey); }
.chart-tab:focus-visible { box-shadow: inset 0 0 0 3px var(--nhs-yellow); }
/* ── Icicle Chart Mock ── */
.icicle {
flex: 1; padding: 20px;
display: flex; flex-direction: column; gap: 3px;
min-height: 420px;
}
.icicle__row {
display: flex; gap: 3px;
flex: 1;
}
.icicle__cell {
display: flex; align-items: center; justify-content: center;
padding: 8px 12px;
color: var(--nhs-white);
font-size: 13px; font-weight: 600;
text-align: center;
position: relative;
overflow: hidden;
transition: filter 0.2s, transform 0.15s;
cursor: pointer;
line-height: 1.2;
}
.icicle__cell:hover { filter: brightness(1.15); transform: scaleY(1.03); z-index: 2; }
.icicle__cell span { position: relative; z-index: 1; text-shadow: 0 1px 2px rgba(0,0,0,0.3); }
.icicle__cell small {
display: block; font-weight: 400; font-size: 11px; opacity: 0.85;
}
/* Level colors */
.lvl-0 { background: var(--nhs-dark-blue); }
.lvl-1a { background: #005EB8; }
.lvl-1b { background: #0072CE; }
.lvl-1c { background: #41B6E6; }
.lvl-1d { background: #768692; }
.lvl-2a { background: #003D7A; }
.lvl-2b { background: #004F9F; }
.lvl-2c { background: #0066B8; }
.lvl-2d { background: #007CC2; }
.lvl-2e { background: #2D9CDB; }
.lvl-2f { background: #5DADE2; }
.lvl-3a { background: #003060; }
.lvl-3b { background: #003D78; }
.lvl-3c { background: #004A90; }
.lvl-3d { background: #0057A8; }
.lvl-3e { background: #0064C0; }
.lvl-3f { background: #2080D0; }
.lvl-3g { background: #4098D8; }
.lvl-3h { background: #60B0E0; }
/* ── Footer ── */
.page-footer {
background: var(--nhs-pale-grey);
border-top: 1px solid #D0D5D6;
padding: 16px 20px;
font-size: 13px; color: var(--nhs-mid-grey);
text-align: center;
}
/* ── Responsive ── */
@media (max-width: 1024px) {
.kpi-row { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 768px) {
.sidebar { display: none; }
.main { margin-left: 0; }
.kpi-row { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<!-- Top Header -->
<header class="top-header">
<div class="top-header__brand">
<div class="top-header__logo">NHS</div>
<div>
<div class="top-header__title">HCD Analysis</div>
</div>
</div>
<div class="top-header__breadcrumb">
Dashboard &rsaquo; <strong>Pathway Analysis</strong>
</div>
<div class="top-header__right">
<span><span class="status-dot"></span> 656,247 records</span>
<span>Last updated: 2h ago</span>
</div>
</header>
<!-- Sidebar -->
<nav class="sidebar" aria-label="Main navigation">
<div class="sidebar__section">
<div class="sidebar__label">Analysis</div>
<a class="sidebar__item sidebar__item--active" href="#">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>
Pathway Overview
</a>
<a class="sidebar__item" href="#">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83 0 2 2 0 010-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"/></svg>
Drug Selection
</a>
<a class="sidebar__item" href="#">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 9l9-7 9 7v11a2 2 0 01-2 2H5a2 2 0 01-2-2z"/></svg>
Trust Selection
</a>
<a class="sidebar__item" href="#">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 01-2 2H4a2 2 0 01-2-2V5a2 2 0 012-2h5l2 3h9a2 2 0 012 2z"/></svg>
Directory Selection
</a>
<a class="sidebar__item" href="#">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14,2 14,8 20,8"/></svg>
Indications
</a>
</div>
<div class="sidebar__section">
<div class="sidebar__label">Reports</div>
<a class="sidebar__item" href="#">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg>
Cost Analysis
</a>
<a class="sidebar__item" href="#">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
Export Data
</a>
</div>
<div class="sidebar__footer">
NHS Norfolk &amp; Waveney ICB<br>High Cost Drugs Programme
</div>
</nav>
<!-- Main Content -->
<main class="main">
<!-- KPIs -->
<section class="kpi-row" aria-label="Key performance indicators">
<div class="kpi-card">
<div class="kpi-card__label">Unique Patients</div>
<div class="kpi-card__value">37,842</div>
<div class="kpi-card__sub">across all trusts</div>
</div>
<div class="kpi-card">
<div class="kpi-card__label">Drug Types</div>
<div class="kpi-card__value">89</div>
<div class="kpi-card__sub">high-cost drugs tracked</div>
</div>
<div class="kpi-card">
<div class="kpi-card__label">Total Cost</div>
<div class="kpi-card__value">&pound;145.2M</div>
<div class="kpi-card__sub">current period spend</div>
</div>
<div class="kpi-card kpi-card--green">
<div class="kpi-card__label">Indication Match</div>
<div class="kpi-card__value">93%</div>
<div class="kpi-card__sub">GP diagnosis confirmed</div>
</div>
</section>
<!-- Filter Bar -->
<section class="filter-bar" aria-label="Filters">
<div class="filter-bar__group">
<span class="filter-bar__label">View</span>
<div class="toggle-pills" role="radiogroup" aria-label="Chart view type">
<button class="toggle-pill toggle-pill--active" role="radio" aria-checked="true">By Directory</button>
<button class="toggle-pill" role="radio" aria-checked="false">By Indication</button>
</div>
</div>
<div class="filter-bar__divider"></div>
<div class="filter-bar__group">
<span class="filter-bar__label">Initiated</span>
<select class="filter-select" aria-label="Treatment initiated period">
<option selected>All years</option>
<option>Last 2 years</option>
<option>Last 1 year</option>
</select>
</div>
<div class="filter-bar__group">
<span class="filter-bar__label">Last seen</span>
<select class="filter-select" aria-label="Last seen period">
<option selected>Last 6 months</option>
<option>Last 12 months</option>
</select>
</div>
<div class="filter-bar__divider"></div>
<div class="filter-bar__group">
<span class="filter-bar__label">Drugs</span>
<select class="filter-select" aria-label="Drug filter">
<option>All 89 drugs</option>
</select>
</div>
<div class="filter-bar__group">
<span class="filter-bar__label">Directorates</span>
<select class="filter-select" aria-label="Directorate filter">
<option>All 14 directorates</option>
</select>
</div>
</section>
<!-- Chart Card -->
<section class="chart-card" aria-label="Patient pathway chart">
<div class="chart-card__header">
<div>
<div class="chart-card__title">Patient Pathway Visualization</div>
<div class="chart-card__subtitle">Trust &rarr; Directorate &rarr; Drug &rarr; Patient Pathway</div>
</div>
</div>
<div class="chart-card__tabs" role="tablist">
<button class="chart-tab chart-tab--active" role="tab" aria-selected="true">Icicle</button>
<button class="chart-tab" role="tab" aria-selected="false">Sankey</button>
<button class="chart-tab" role="tab" aria-selected="false">Timeline</button>
</div>
<!-- Icicle Chart -->
<div class="icicle" role="img" aria-label="Icicle chart showing patient treatment pathways">
<!-- Level 0: Root -->
<div class="icicle__row" style="flex: 0.8;">
<div class="icicle__cell lvl-0" style="flex: 1;">
<span>NHS Norfolk and Waveney ICB<small>37,842 patients &middot; &pound;145.2M</small></span>
</div>
</div>
<!-- Level 1: Directorates -->
<div class="icicle__row" style="flex: 1;">
<div class="icicle__cell lvl-1a" style="flex: 40;">
<span>Rheumatology<small>15,137 &middot; 40%</small></span>
</div>
<div class="icicle__cell lvl-1b" style="flex: 28;">
<span>Dermatology<small>10,596 &middot; 28%</small></span>
</div>
<div class="icicle__cell lvl-1c" style="flex: 18;">
<span>Gastroenterology<small>6,812 &middot; 18%</small></span>
</div>
<div class="icicle__cell lvl-1d" style="flex: 14;">
<span>Oncology<small>5,297 &middot; 14%</small></span>
</div>
</div>
<!-- Level 2: Drugs -->
<div class="icicle__row" style="flex: 1;">
<!-- Under Rheumatology -->
<div class="icicle__cell lvl-2a" style="flex: 22;"><span>ADALIMUMAB<small>8,325</small></span></div>
<div class="icicle__cell lvl-2b" style="flex: 12;"><span>ETANERCEPT<small>4,541</small></span></div>
<div class="icicle__cell lvl-2c" style="flex: 6;"><span>TOCILIZUMAB<small>2,271</small></span></div>
<!-- Under Dermatology -->
<div class="icicle__cell lvl-2d" style="flex: 16;"><span>SECUKINUMAB<small>6,057</small></span></div>
<div class="icicle__cell lvl-2e" style="flex: 12;"><span>DUPILUMAB<small>4,539</small></span></div>
<!-- Under Gastro -->
<div class="icicle__cell lvl-2c" style="flex: 10;"><span>INFLIXIMAB<small>3,787</small></span></div>
<div class="icicle__cell lvl-2a" style="flex: 8;"><span>VEDOLIZUMAB<small>3,025</small></span></div>
<!-- Under Oncology -->
<div class="icicle__cell lvl-2f" style="flex: 8;"><span>PEMBROLIZUMAB<small>3,025</small></span></div>
<div class="icicle__cell lvl-2b" style="flex: 6;"><span>NIVOLUMAB<small>2,272</small></span></div>
</div>
<!-- Level 3: Pathways -->
<div class="icicle__row" style="flex: 1.2;">
<div class="icicle__cell lvl-3a" style="flex: 14;"><span>ADA&rarr;ADA<small>5,310</small></span></div>
<div class="icicle__cell lvl-3b" style="flex: 8;"><span>ADA&rarr;ETA&rarr;SEC<small>3,015</small></span></div>
<div class="icicle__cell lvl-3c" style="flex: 7;"><span>ETA&rarr;ETA<small>2,648</small></span></div>
<div class="icicle__cell lvl-3d" style="flex: 5;"><span>ETA&rarr;ADA<small>1,893</small></span></div>
<div class="icicle__cell lvl-3e" style="flex: 6;"><span>TOC&rarr;TOC<small>2,271</small></span></div>
<div class="icicle__cell lvl-3f" style="flex: 10;"><span>SEC&rarr;SEC<small>3,785</small></span></div>
<div class="icicle__cell lvl-3a" style="flex: 6;"><span>SEC&rarr;DUP<small>2,272</small></span></div>
<div class="icicle__cell lvl-3g" style="flex: 6;"><span>DUP&rarr;DUP<small>2,267</small></span></div>
<div class="icicle__cell lvl-3h" style="flex: 6;"><span>DUP&rarr;SEC<small>2,272</small></span></div>
<div class="icicle__cell lvl-3c" style="flex: 6;"><span>INF&rarr;INF<small>2,272</small></span></div>
<div class="icicle__cell lvl-3b" style="flex: 4;"><span>INF&rarr;VED<small>1,515</small></span></div>
<div class="icicle__cell lvl-3d" style="flex: 5;"><span>VED&rarr;VED<small>1,893</small></span></div>
<div class="icicle__cell lvl-3e" style="flex: 3;"><span>VED&rarr;INF<small>1,132</small></span></div>
<div class="icicle__cell lvl-3f" style="flex: 5;"><span>PEM&rarr;PEM<small>1,893</small></span></div>
<div class="icicle__cell lvl-3g" style="flex: 3;"><span>PEM&rarr;NIV<small>1,132</small></span></div>
<div class="icicle__cell lvl-3h" style="flex: 4;"><span>NIV&rarr;NIV<small>1,514</small></span></div>
<div class="icicle__cell lvl-3a" style="flex: 2;"><span>NIV&rarr;PEM<small>758</small></span></div>
</div>
</div>
</section>
</main>
<!-- Footer -->
<footer class="page-footer">
NHS Norfolk and Waveney ICB &mdash; High Cost Drugs Analysis &middot; Data as of Feb 2026
</footer>
<script>
// Toggle pills interaction
document.querySelectorAll('.toggle-pills').forEach(group => {
group.querySelectorAll('.toggle-pill').forEach(pill => {
pill.addEventListener('click', () => {
group.querySelectorAll('.toggle-pill').forEach(p => {
p.classList.remove('toggle-pill--active');
p.setAttribute('aria-checked', 'false');
});
pill.classList.add('toggle-pill--active');
pill.setAttribute('aria-checked', 'true');
});
});
});
// Chart tabs interaction
document.querySelectorAll('.chart-tab').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('.chart-tab').forEach(t => {
t.classList.remove('chart-tab--active');
t.setAttribute('aria-selected', 'false');
});
tab.classList.add('chart-tab--active');
tab.setAttribute('aria-selected', 'true');
});
});
</script>
</body>
</html>
-154
View File
@@ -1,154 +0,0 @@
# Additional Analytics Charts — Implementation Plan
## UI Approach: Tabbed Chart Area
Extend existing `chart_card.py` tab bar. Currently has Icicle (active), Sankey (disabled), Timeline (disabled). Replace/extend with new tabs.
## Charts to Build (Priority Order)
### Tab 1: Icicle (existing — no change)
### Tab 2: First-Line Market Share — Horizontal Bar Chart
**What**: % of patients starting on each first-line drug, grouped by directorate or indication
**Data source**: `pathway_nodes WHERE level = 3` (drug level). The `colour` column already holds proportion of parent. `value` = patient count.
**Query**: Filter by `chart_type`, `date_filter_id`, optionally `directory` or `trust_name`. Group by `directory`, then show drugs as bars.
**Viz**: Horizontal grouped bar chart. One cluster per directorate/indication (top N), bars within = drugs, length = % of patients. Sorted by total patients desc. NHS blue palette.
**Interaction**: Responds to all existing filters (date, chart type, trust, drug, directorate). Clicking a directorate cluster could filter the icicle.
### Tab 3: Pathway Cost Effectiveness — Lollipop/Dot Plot
**What**: Compare annualized cost per patient across complete treatment pathways within a directorate/indication. Highlights most vs least cost-effective pathways.
**Data source**: `pathway_nodes WHERE level >= 4` (pathway nodes). Fields: `cost_pp_pa` (annualized), `value` (patient count), `ids` (parse to get pathway sequence), `directory`.
**Calculation**: `cost_pp_pa` is already computed as `(total_cost / patients) * (365 / avg_days)` — this IS the "total cost over N years / N years" the user described.
**Query**: Filter to a specific directorate/indication, then show all pathway variants ranked by `cost_pp_pa`.
**Viz**: Horizontal lollipop chart (dot on stick). Y-axis = pathway label (e.g., "Adalimumab → Secukinumab → Rituximab"), X-axis = £ per patient per annum. Dot size = patient count. Colour gradient: green (cheap) → amber → red (expensive).
**Interaction**: Directorate/indication selector drives which pathways are shown. Could also compare across directorates at the drug level (level 3).
**Bonus metric — "Pathway Retention" (fewest switches)**:
- For each 2nd-line pathway (e.g., "Drug A → Drug B"), calculate what % of patients escalate to a 3rd line
- Derivation: `value("Drug A → Drug B") - SUM(value("Drug A → Drug B → *"))` = patients who stayed on 2nd line
- Show as a secondary annotation or companion chart: "Drug B retains 72% of patients (no 3rd-line switch needed)"
- This identifies the most effective 2nd-line choices
### Tab 4: Cost Waterfall — Waterfall Chart
**What**: Break down £ per patient per annum by directorate, showing relative cost contribution
**Data source**: `pathway_nodes WHERE level = 2` (directorate/indication level). Field: `cost_pp_pa`, `value`.
**Viz**: Plotly waterfall chart. Each bar = one directorate's average cost_pp_pa. Sorted highest to lowest. Running reference line optional. Use NHS colours.
**Note**: User specifically wants cost_pp_pa (annualized), not total cost.
**Interaction**: Responds to chart_type toggle, date filter, trust filter.
### Tab 5: Drug Switching Sankey — Sankey Diagram
**What**: Flow of patients from 1st-line → 2nd-line → 3rd-line drugs
**Data source**: `pathway_nodes WHERE level >= 3`. Parse `ids` to extract drug transition sequences.
**Parsing**: `ids` format at level 4+: `"TRUST - DIRECTORY - DRUG_A - DRUG_A|DRUG_B"`. Split by " - ", take segments from level 3 onwards, split by "|" to get ordered drug list.
**Viz**: Plotly Sankey. Left nodes = 1st-line drugs, middle = 2nd-line, right = 3rd-line. Link width = patient count. Colour by drug or by directorate.
**Interaction**: Filter by directorate/indication to see switching within a specialty. Filter by trust to compare switching patterns.
### Tab 6: Dosing Interval Comparison — Grouped Bar Chart
**What**: Compare average dosing frequency/weekly interval for a drug across trusts or directorates
**Data source**: Level 3+ nodes, `average_spacing` (HTML string), `average_administered` (JSON array)
**Parsing needed**:
- `average_spacing`: regex to extract weekly interval number from `"given X times with Y weekly interval"`
- `average_administered`: `json.loads()` to get dose counts
**Viz**: Horizontal grouped bars. Y-axis = trust or directorate, X-axis = weekly interval (or total administrations). One colour per drug if comparing multiple.
**Interaction**: Drug selector to pick which drug to compare. Group-by selector (trust vs directorate).
### Tab 7: Directorate × Drug Heatmap
**What**: Matrix showing which drugs are used in which directorates, cells coloured by patient count or cost_pp_pa
**Data source**: Level 3 nodes, pivot `directory` × drug (parsed from `labels` or `ids`)
**Viz**: Plotly heatmap. Rows = directorates (sorted by total patients), columns = drugs (sorted by frequency). Cell colour = patient count or cost. Hover shows details.
**Interaction**: Toggle between patient count / cost / cost_pp_pa colouring.
### Tab 8: Treatment Duration Bars
**What**: Compare average treatment durations across drugs within a directorate
**Data source**: Level 3 nodes, `avg_days` field
**Viz**: Horizontal bar chart. Y-axis = drug, X-axis = average days. Colour intensity by patient count.
**Interaction**: Directorate filter drives which drugs are shown.
---
## Data Layer Changes
### New query functions needed (in `src/data_processing/pathway_queries.py`):
```python
def get_drug_market_share(db_path, date_filter_id, chart_type, directory=None, trust=None):
"""Level 3 nodes grouped by directory, returning drug, value, colour."""
def get_pathway_costs(db_path, date_filter_id, chart_type, directory=None):
"""Level 4+ nodes with cost_pp_pa, parsed pathway labels, patient counts."""
def get_cost_waterfall(db_path, date_filter_id, chart_type, trust=None):
"""Level 2 nodes with cost_pp_pa per directorate/indication."""
def get_drug_transitions(db_path, date_filter_id, chart_type, directory=None):
"""Level 3+ nodes parsed into source→target drug transitions with patient counts."""
def get_dosing_intervals(db_path, date_filter_id, chart_type, drug=None):
"""Level 3 nodes for a specific drug, parsed average_spacing by trust/directory."""
def get_drug_directory_matrix(db_path, date_filter_id, chart_type):
"""Level 3 nodes pivoted as directory × drug with value/cost metrics."""
def get_treatment_durations(db_path, date_filter_id, chart_type, directory=None):
"""Level 3 nodes with avg_days by drug within a directorate."""
```
### Parsing utilities needed:
```python
def parse_average_spacing(spacing_html: str) -> dict:
"""Extract drug_name, dose_count, weekly_interval, total_weeks from HTML string."""
def parse_pathway_drugs(ids: str, level: int) -> list[str]:
"""Extract ordered drug list from ids column at level 4+."""
def calculate_retention_rate(nodes: list[dict]) -> dict:
"""For each N-drug pathway, calculate % not escalating to N+1 drugs."""
```
---
## Callback Architecture
Each tab gets its own callback triggered by `chart-data` store + `active-tab` state:
```
active-tab change → render selected chart
chart-data change → re-render active chart
```
Only the active tab's chart is computed (lazy rendering). Store the active tab in `app-state`.
New callback per chart type in `dash_app/callbacks/`:
- `market_share.py` — builds bar chart from level 3 data
- `pathway_costs.py` — builds lollipop + retention annotations
- `cost_waterfall.py` — builds waterfall from level 2 data
- `sankey.py` — builds Sankey from parsed transitions
- `dosing.py` — builds grouped bars from parsed spacing
- `heatmap.py` — builds heatmap from pivoted matrix
- `duration.py` — builds bar chart from avg_days
---
## Implementation Order
1. **Data parsing utilities** — shared parsing for spacing, pathway drugs, retention
2. **Query functions** — one per chart type in pathway_queries.py
3. **Tab infrastructure** — extend chart_card.py with all tab labels, lazy rendering
4. **Charts one at a time** (in priority order):
- First-Line Market Share (simplest, validates the tab pattern)
- Pathway Cost Effectiveness + Retention (user's key insight)
- Cost Waterfall
- Drug Switching Sankey
- Dosing Interval
- Heatmap
- Treatment Duration
---
## Verification
- Run `python run_dash.py` after each chart addition
- Verify each chart responds to filter changes (date, chart type, trust, directorate, drug)
- Test with both "directory" and "indication" chart types
- Verify icicle chart still works correctly (no regressions)
- Check tab switching is smooth with no unnecessary recomputation
+137
View File
@@ -0,0 +1,137 @@
# -*- mode: python ; coding: utf-8 -*-
"""PyInstaller spec for NHS Pathway Analysis desktop app."""
import os
block_cipher = None
# Project root (where this spec file lives)
ROOT = os.path.abspath(os.path.dirname(SPECPATH)) if 'SPECPATH' in dir() else os.path.abspath('.')
a = Analysis(
['app_desktop.py'],
pathex=[os.path.join(ROOT, 'src')],
binaries=[],
datas=[
('data/pathways.db', 'data'),
('data/DimSearchTerm.csv', 'data'),
('data/defaultTrusts.csv', 'data'),
('dash_app/assets/nhs.css', 'dash_app/assets'),
],
hiddenimports=[
# Dash internals
'dash',
'dash.dash',
'dash.dcc',
'dash.html',
'dash.exceptions',
'dash._utils',
'dash.dependencies',
'dash_mantine_components',
# Plotly
'plotly',
'plotly.graph_objects',
'plotly.io',
'plotly.express',
'plotly.subplots',
# Data
'pandas',
'pandas._libs.tslibs',
'numpy',
'sqlite3',
# App packages (src/ on pathex)
'core',
'core.config',
'core.models',
'core.logging_config',
'core.resource_path',
'data_processing',
'data_processing.database',
'data_processing.schema',
'data_processing.pathway_queries',
'data_processing.parsing',
'data_processing.diagnosis_lookup',
'analysis',
'analysis.pathway_analyzer',
'analysis.statistics',
'visualization',
'visualization.plotly_generator',
# Dash app
'dash_app',
'dash_app.app',
'dash_app.data',
'dash_app.data.queries',
'dash_app.data.card_browser',
'dash_app.callbacks',
'dash_app.callbacks.filters',
'dash_app.callbacks.chart',
'dash_app.callbacks.modals',
'dash_app.callbacks.navigation',
'dash_app.callbacks.trust_comparison',
'dash_app.callbacks.kpi',
'dash_app.callbacks.trends',
'dash_app.components',
'dash_app.components.header',
'dash_app.components.sub_header',
'dash_app.components.sidebar',
'dash_app.components.filter_bar',
'dash_app.components.chart_card',
'dash_app.components.footer',
'dash_app.components.modals',
'dash_app.components.trust_comparison',
'dash_app.components.trends',
# pywebview backend
'webview',
'clr',
'pythonnet',
# Flask (Dash's server)
'flask',
'flask.json',
'flask.json.provider',
],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[
'snowflake',
'snowflake.connector',
'cli',
'pytest',
'tests',
'tkinter',
'matplotlib',
'IPython',
'jupyter',
],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
exe = EXE(
pyz,
a.scripts,
[],
exclude_binaries=True,
name='NHS_Pathway_Analysis',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=False, # --windowed (no console)
icon=None,
)
coll = COLLECT(
exe,
a.binaries,
a.zipfiles,
a.datas,
strip=False,
upx=True,
upx_exclude=[],
name='NHS_Pathway_Analysis',
)
+62
View File
@@ -0,0 +1,62 @@
"""Desktop entry point: Dash app inside a pywebview native window."""
import sys
import socket
import threading
import time
from pathlib import Path
# Ensure src/ is on sys.path so that core/, data_processing/, etc. are importable
_src_dir = str(Path(__file__).resolve().parent / "src")
if _src_dir not in sys.path:
sys.path.insert(0, _src_dir)
import webview
from dash_app.app import app
def find_free_port(start: int = 8050) -> int:
"""Find the first available port starting from *start*."""
for port in range(start, start + 100):
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
try:
s.bind(("127.0.0.1", port))
return port
except OSError:
continue
raise RuntimeError("No free port found")
def wait_for_server(port: int, timeout: float = 30.0) -> None:
"""Block until the Dash server accepts connections."""
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
if s.connect_ex(("127.0.0.1", port)) == 0:
return
time.sleep(0.1)
raise TimeoutError(f"Server did not start within {timeout}s")
def main() -> None:
port = find_free_port()
server_thread = threading.Thread(
target=app.run,
kwargs={"debug": False, "port": port, "use_reloader": False},
daemon=True,
)
server_thread.start()
wait_for_server(port)
webview.create_window(
"NHS Pathway Analysis",
f"http://127.0.0.1:{port}",
width=1400,
height=900,
)
webview.start()
if __name__ == "__main__":
main()
+8 -4
View File
@@ -1,7 +1,10 @@
"""Dash application entry point with layout root and state stores."""
import sys
from dash import Dash, html, dcc
import dash_mantine_components as dmc
from core.resource_path import get_resource_path
from dash_app.components.header import make_header
from dash_app.components.sub_header import make_sub_header
from dash_app.components.sidebar import make_sidebar
@@ -12,10 +15,11 @@ from dash_app.components.modals import make_modals
from dash_app.components.trust_comparison import make_tc_landing, make_tc_dashboard
from dash_app.components.trends import make_trends_landing, make_trends_detail
app = Dash(
__name__,
suppress_callback_exceptions=True,
)
_app_kwargs = {"suppress_callback_exceptions": True}
if getattr(sys, "frozen", False):
_app_kwargs["assets_folder"] = str(get_resource_path("dash_app/assets"))
app = Dash(__name__, **_app_kwargs)
app.layout = dmc.MantineProvider(
children=[
+2 -3
View File
@@ -9,12 +9,11 @@ Also provides get_all_drugs() for the flat "All Drugs" card.
import csv
from collections import defaultdict
from pathlib import Path
from core.resource_path import get_resource_path
from data_processing.diagnosis_lookup import SEARCH_TERM_MERGE_MAP
DATA_DIR = Path(__file__).resolve().parents[2] / "data"
DIM_SEARCH_TERM_PATH = DATA_DIR / "DimSearchTerm.csv"
DIM_SEARCH_TERM_PATH = get_resource_path("data/DimSearchTerm.csv")
def build_directorate_tree() -> dict[str, dict[str, list[str]]]:
+2 -2
View File
@@ -5,9 +5,9 @@ Resolves the database path relative to this file's location and delegates
to the shared functions in src/data_processing/pathway_queries.py.
"""
from pathlib import Path
from typing import Optional
from core.resource_path import get_resource_path
from data_processing.pathway_queries import (
load_initial_data as _load_initial_data,
load_pathway_nodes as _load_pathway_nodes,
@@ -33,7 +33,7 @@ from data_processing.pathway_queries import (
get_trend_data as _get_trend_data,
)
DB_PATH = Path(__file__).resolve().parents[2] / "data" / "pathways.db"
DB_PATH = get_resource_path("data/pathways.db")
def load_initial_data() -> dict:
+2
View File
@@ -13,6 +13,8 @@ dependencies = [
"pillow>=10.0.0",
"plotly>=5.15.0",
"pyarrow>=20.0.0",
"pyinstaller>=6.18.0",
"pywebview>=6.1",
"snowflake-connector-python>=3.0.0",
"tomli>=2.0.0",
]
+18
View File
@@ -0,0 +1,18 @@
"""Resolve file paths for both development and PyInstaller frozen modes."""
import sys
from pathlib import Path
def get_resource_path(relative_path: str) -> Path:
"""Return absolute path to a bundled resource.
In frozen mode (PyInstaller), resolves from sys._MEIPASS.
In dev mode, resolves from the project root (3 parents up from this file:
src/core/resource_path.py → src/core → src → project root).
"""
if getattr(sys, "frozen", False):
base = Path(sys._MEIPASS)
else:
base = Path(__file__).resolve().parents[2]
return base / relative_path
Generated
+260
View File
@@ -5,6 +5,15 @@ resolution-markers = [
"python_full_version < '3.11'",
]
[[package]]
name = "altgraph"
version = "0.17.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7e/f8/97fdf103f38fed6792a1601dbc16cc8aac56e7459a9fff08c812d8ae177a/altgraph-0.17.5.tar.gz", hash = "sha256:c87b395dd12fabde9c99573a9749d67da8d29ef9de0125c7f536699b4a9bc9e7", size = 48428 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a9/ba/000a1996d4308bc65120167c21241a3b205464a2e0b58deda26ae8ac21d1/altgraph-0.17.5-py2.py3-none-any.whl", hash = "sha256:f3a22400bce1b0c701683820ac4f3b159cd301acab067c51c653e06961600597", size = 21228 },
]
[[package]]
name = "asn1crypto"
version = "1.5.1"
@@ -51,6 +60,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/4c/a8/95656f91b795eb47b73a00d36c51c7a5729eafa632c7348caa068ff63e50/botocore-1.42.43-py3-none-any.whl", hash = "sha256:1c0e30f62e274978ac3bcab253e3a859febea634b72b5e343589db7d17f83cd6", size = 14610179 },
]
[[package]]
name = "bottle"
version = "0.13.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7a/71/cca6167c06d00c81375fd668719df245864076d284f7cb46a694cbeb5454/bottle-0.13.4.tar.gz", hash = "sha256:787e78327e12b227938de02248333d788cfe45987edca735f8f88e03472c3f47", size = 98717 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/83/f6/b55ec74cfe68c6584163faa311503c20b0da4c09883a41e8e00d6726c954/bottle-0.13.4-py2.py3-none-any.whl", hash = "sha256:045684fbd2764eac9cdeb824861d1551d113e8b683d8d26e296898d3dd99a12e", size = 103807 },
]
[[package]]
name = "certifi"
version = "2026.1.4"
@@ -243,6 +261,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274 },
]
[[package]]
name = "clr-loader"
version = "0.2.10"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi" },
]
sdist = { url = "https://files.pythonhosted.org/packages/18/24/c12faf3f61614b3131b5c98d3bf0d376b49c7feaa73edca559aeb2aee080/clr_loader-0.2.10.tar.gz", hash = "sha256:81f114afbc5005bafc5efe5af1341d400e22137e275b042a8979f3feb9fc9446", size = 83605 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c8/61/cf819f8e8bb4d4c74661acf2498ba8d4a296714be3478d21eaabf64f5b9b/clr_loader-0.2.10-py3-none-any.whl", hash = "sha256:ebbbf9d511a7fe95fa28a95a4e04cd195b097881dfe66158dc2c281d3536f282", size = 56483 },
]
[[package]]
name = "colorama"
version = "0.4.6"
@@ -682,6 +712,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419 },
]
[[package]]
name = "macholib"
version = "1.16.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "altgraph" },
]
sdist = { url = "https://files.pythonhosted.org/packages/10/2f/97589876ea967487978071c9042518d28b958d87b17dceb7cdc1d881f963/macholib-1.16.4.tar.gz", hash = "sha256:f408c93ab2e995cd2c46e34fe328b130404be143469e41bc366c807448979362", size = 59427 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/d1/a9f36f8ecdf0fb7c9b1e78c8d7af12b8c8754e74851ac7b94a8305540fc7/macholib-1.16.4-py2.py3-none-any.whl", hash = "sha256:da1a3fa8266e30f0ce7e97c6a54eefaae8edd1e5f86f3eb8b95457cae90265ea", size = 38117 },
]
[[package]]
name = "markupsafe"
version = "2.1.3"
@@ -799,6 +841,8 @@ dependencies = [
{ name = "pillow" },
{ name = "plotly" },
{ name = "pyarrow" },
{ name = "pyinstaller" },
{ name = "pywebview" },
{ name = "snowflake-connector-python" },
{ name = "tomli" },
]
@@ -819,12 +863,23 @@ requires-dist = [
{ name = "pillow", specifier = ">=10.0.0" },
{ name = "plotly", specifier = ">=5.15.0" },
{ name = "pyarrow", specifier = ">=20.0.0" },
{ name = "pyinstaller", specifier = ">=6.18.0" },
{ name = "pytest", marker = "extra == 'test'", specifier = ">=8.0.0" },
{ name = "pytest-cov", marker = "extra == 'test'", specifier = ">=4.0.0" },
{ name = "pywebview", specifier = ">=6.1" },
{ name = "snowflake-connector-python", specifier = ">=3.0.0" },
{ name = "tomli", specifier = ">=2.0.0" },
]
[[package]]
name = "pefile"
version = "2024.8.26"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/03/4f/2750f7f6f025a1507cd3b7218691671eecfd0bbebebe8b39aa0fe1d360b8/pefile-2024.8.26.tar.gz", hash = "sha256:3ff6c5d8b43e8c37bb6e6dd5085658d658a7a0bdcd20b6a07b1fcfc1c4e9d632", size = 76008 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/54/16/12b82f791c7f50ddec566873d5bdd245baa1491bac11d15ffb98aecc8f8b/pefile-2024.8.26-py3-none-any.whl", hash = "sha256:76f8b485dcd3b1bb8166f1128d395fa3d87af26360c2358fb75b80019b957c6f", size = 74766 },
]
[[package]]
name = "pillow"
version = "10.0.0"
@@ -897,6 +952,12 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 },
]
[[package]]
name = "proxy-tools"
version = "0.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f2/cf/77d3e19b7fabd03895caca7857ef51e4c409e0ca6b37ee6e9f7daa50b642/proxy_tools-0.1.0.tar.gz", hash = "sha256:ccb3751f529c047e2d8a58440d86b205303cf0fe8146f784d1cbcd94f0a28010", size = 2978 }
[[package]]
name = "pyarrow"
version = "20.0.0"
@@ -968,6 +1029,47 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217 },
]
[[package]]
name = "pyinstaller"
version = "6.18.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "altgraph" },
{ name = "macholib", marker = "sys_platform == 'darwin'" },
{ name = "packaging" },
{ name = "pefile", marker = "sys_platform == 'win32'" },
{ name = "pyinstaller-hooks-contrib" },
{ name = "pywin32-ctypes", marker = "sys_platform == 'win32'" },
{ name = "setuptools" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9f/b8/0fe3359920b0a4e7008e0e93ff383003763e3eee3eb31a07c52868722960/pyinstaller-6.18.0.tar.gz", hash = "sha256:cdc507542783511cad4856fce582fdc37e9f29665ca596889c663c83ec8c6ec9", size = 4034976 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/73/e6/51b0146a1a3eec619e58f5d69fb4e3d0f65a31cbddbeef557c9bb83eeed9/pyinstaller-6.18.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:cb7aa5a71bfa7c0af17a4a4e21855663c89e4bd7c40f1d337c8370636d8847c3", size = 1040056 },
{ url = "https://files.pythonhosted.org/packages/4c/9c/a3634c0ec8e1ed31b373b548848b5c0b39b56edc191cf737e697d484ec23/pyinstaller-6.18.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:07785459b3bf8a48889eac0b4d0667ade84aef8930ce030bc7cbb32f41283b33", size = 734971 },
{ url = "https://files.pythonhosted.org/packages/2c/04/6756442078ccfcd552ccce636be1574035e62f827ffa1f5d8a0382682546/pyinstaller-6.18.0-py3-none-manylinux2014_i686.whl", hash = "sha256:f998675b7ccb2dabbb1dc2d6f18af61d55428ad6d38e6c4d700417411b697d37", size = 746637 },
{ url = "https://files.pythonhosted.org/packages/54/39/fbc56519000cdbf450f472692a7b9b55d42077ce8529f1be631db7b75a36/pyinstaller-6.18.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:779817a0cf69604cddcdb5be1fd4959dc2ce048d6355c73e5da97884df2f3387", size = 744343 },
{ url = "https://files.pythonhosted.org/packages/36/f2/50887badf282fee776e83d1e4feab74c026f50a1ea16e109ed939e32aa28/pyinstaller-6.18.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:31b5d109f8405be0b7cddcede43e7b074792bc9a5bbd54ec000a3e779183c2af", size = 741084 },
{ url = "https://files.pythonhosted.org/packages/1c/08/3a1419183e4713ef77d912ecbdd6ef858689ed9deb34d547133f724ca745/pyinstaller-6.18.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:4328c9837f1aef4fe1a127d4ff1b09a12ce53c827ce87c94117628b0e1fd098b", size = 740943 },
{ url = "https://files.pythonhosted.org/packages/c2/47/309305e36d116f1434b42d91c420ff951fa79b2c398bbd59930c830450be/pyinstaller-6.18.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:3638fc81eb948e5e5eab1d4ad8f216e3fec6d4a350648304f0adb227b746ee5e", size = 740107 },
{ url = "https://files.pythonhosted.org/packages/83/0f/a59a95cd1df59ddbc9e74d5a663387551333bcf19a5dd3086f5c81a2e83c/pyinstaller-6.18.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe59da34269e637f97fd3c43024f764586fc319141d245ff1a2e9af1036aa3", size = 739843 },
{ url = "https://files.pythonhosted.org/packages/9a/09/e7a870e7205cdbd2f8785010a5d3fe48a9df2591156ee34a8b29b774fa14/pyinstaller-6.18.0-py3-none-win32.whl", hash = "sha256:496205e4fa92ec944f9696eb597962a83aef4d4c3479abfab83d730e1edf016b", size = 1323811 },
{ url = "https://files.pythonhosted.org/packages/fb/d5/48eef2002b6d3937ceac2717fe17e9ca3a43a4c9826bafee367dfc75ba85/pyinstaller-6.18.0-py3-none-win_amd64.whl", hash = "sha256:976fabd90ecfbda47571c87055ad73413ec615ff7dea35e12a4304174de78de9", size = 1384389 },
{ url = "https://files.pythonhosted.org/packages/1b/8d/1a88e6e94107de3ea1c842fd59c3aa132d344ad8e52ea458ffa9a748726e/pyinstaller-6.18.0-py3-none-win_arm64.whl", hash = "sha256:dba4b70e3c9ba09aab51152c72a08e58a751851548f77ad35944d32a300c8381", size = 1324869 },
]
[[package]]
name = "pyinstaller-hooks-contrib"
version = "2026.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "packaging" },
{ name = "setuptools" },
]
sdist = { url = "https://files.pythonhosted.org/packages/31/8f/8052ff65067697ee80fde45b9731842e160751c41ac5690ba232c22030e8/pyinstaller_hooks_contrib-2026.0.tar.gz", hash = "sha256:0120893de491a000845470ca9c0b39284731ac6bace26f6849dea9627aaed48e", size = 170311 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d5/b1/9da6ec3e88696018ee7bb9dc4a7310c2cfaebf32923a19598cd342767c10/pyinstaller_hooks_contrib-2026.0-py3-none-any.whl", hash = "sha256:0590db8edeba3e6c30c8474937021f5cd39c0602b4d10f74a064c73911efaca5", size = 452318 },
]
[[package]]
name = "pyjwt"
version = "2.11.0"
@@ -977,6 +1079,109 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/6f/01/c26ce75ba460d5cd503da9e13b21a33804d38c2165dec7b716d06b13010c/pyjwt-2.11.0-py3-none-any.whl", hash = "sha256:94a6bde30eb5c8e04fee991062b534071fd1439ef58d2adc9ccb823e7bcd0469", size = 28224 },
]
[[package]]
name = "pyobjc-core"
version = "12.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b8/b6/d5612eb40be4fd5ef88c259339e6313f46ba67577a95d86c3470b951fce0/pyobjc_core-12.1.tar.gz", hash = "sha256:2bb3903f5387f72422145e1466b3ac3f7f0ef2e9960afa9bcd8961c5cbf8bd21", size = 1000532 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/63/bf/3dbb1783388da54e650f8a6b88bde03c101d9ba93dfe8ab1b1873f1cd999/pyobjc_core-12.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:93418e79c1655f66b4352168f8c85c942707cb1d3ea13a1da3e6f6a143bacda7", size = 676748 },
{ url = "https://files.pythonhosted.org/packages/95/df/d2b290708e9da86d6e7a9a2a2022b91915cf2e712a5a82e306cb6ee99792/pyobjc_core-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c918ebca280925e7fcb14c5c43ce12dcb9574a33cccb889be7c8c17f3bcce8b6", size = 671263 },
{ url = "https://files.pythonhosted.org/packages/64/5a/6b15e499de73050f4a2c88fff664ae154307d25dc04da8fb38998a428358/pyobjc_core-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:818bcc6723561f207e5b5453efe9703f34bc8781d11ce9b8be286bb415eb4962", size = 678335 },
{ url = "https://files.pythonhosted.org/packages/f4/d2/29e5e536adc07bc3d33dd09f3f7cf844bf7b4981820dc2a91dd810f3c782/pyobjc_core-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:01c0cf500596f03e21c23aef9b5f326b9fb1f8f118cf0d8b66749b6cf4cbb37a", size = 677370 },
{ url = "https://files.pythonhosted.org/packages/1b/f0/4b4ed8924cd04e425f2a07269943018d43949afad1c348c3ed4d9d032787/pyobjc_core-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:177aaca84bb369a483e4961186704f64b2697708046745f8167e818d968c88fc", size = 719586 },
{ url = "https://files.pythonhosted.org/packages/25/98/9f4ed07162de69603144ff480be35cd021808faa7f730d082b92f7ebf2b5/pyobjc_core-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:844515f5d86395b979d02152576e7dee9cc679acc0b32dc626ef5bda315eaa43", size = 670164 },
{ url = "https://files.pythonhosted.org/packages/62/50/dc076965c96c7f0de25c0a32b7f8aa98133ed244deaeeacfc758783f1f30/pyobjc_core-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:453b191df1a4b80e756445b935491b974714456ae2cbae816840bd96f86db882", size = 712204 },
]
[[package]]
name = "pyobjc-framework-cocoa"
version = "12.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyobjc-core" },
]
sdist = { url = "https://files.pythonhosted.org/packages/02/a3/16ca9a15e77c061a9250afbae2eae26f2e1579eb8ca9462ae2d2c71e1169/pyobjc_framework_cocoa-12.1.tar.gz", hash = "sha256:5556c87db95711b985d5efdaaf01c917ddd41d148b1e52a0c66b1a2e2c5c1640", size = 2772191 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b2/aa/2b2d7ec3ac4b112a605e9bd5c5e5e4fd31d60a8a4b610ab19cc4838aa92a/pyobjc_framework_cocoa-12.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9b880d3bdcd102809d704b6d8e14e31611443aa892d9f60e8491e457182fdd48", size = 383825 },
{ url = "https://files.pythonhosted.org/packages/3f/07/5760735c0fffc65107e648eaf7e0991f46da442ac4493501be5380e6d9d4/pyobjc_framework_cocoa-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f52228bcf38da64b77328787967d464e28b981492b33a7675585141e1b0a01e6", size = 383812 },
{ url = "https://files.pythonhosted.org/packages/95/bf/ee4f27ec3920d5c6fc63c63e797c5b2cc4e20fe439217085d01ea5b63856/pyobjc_framework_cocoa-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:547c182837214b7ec4796dac5aee3aa25abc665757b75d7f44f83c994bcb0858", size = 384590 },
{ url = "https://files.pythonhosted.org/packages/ad/31/0c2e734165abb46215797bd830c4bdcb780b699854b15f2b6240515edcc6/pyobjc_framework_cocoa-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5a3dcd491cacc2f5a197142b3c556d8aafa3963011110102a093349017705118", size = 384689 },
{ url = "https://files.pythonhosted.org/packages/23/3b/b9f61be7b9f9b4e0a6db18b3c35c4c4d589f2d04e963e2174d38c6555a92/pyobjc_framework_cocoa-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:914b74328c22d8ca261d78c23ef2befc29776e0b85555973927b338c5734ca44", size = 388843 },
{ url = "https://files.pythonhosted.org/packages/59/bb/f777cc9e775fc7dae77b569254570fe46eb842516b3e4fe383ab49eab598/pyobjc_framework_cocoa-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:03342a60fc0015bcdf9b93ac0b4f457d3938e9ef761b28df9564c91a14f0129a", size = 384932 },
{ url = "https://files.pythonhosted.org/packages/58/27/b457b7b37089cad692c8aada90119162dfb4c4a16f513b79a8b2b022b33b/pyobjc_framework_cocoa-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:6ba1dc1bfa4da42d04e93d2363491275fb2e2be5c20790e561c8a9e09b8cf2cc", size = 388970 },
]
[[package]]
name = "pyobjc-framework-quartz"
version = "12.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyobjc-core" },
{ name = "pyobjc-framework-cocoa" },
]
sdist = { url = "https://files.pythonhosted.org/packages/94/18/cc59f3d4355c9456fc945eae7fe8797003c4da99212dd531ad1b0de8a0c6/pyobjc_framework_quartz-12.1.tar.gz", hash = "sha256:27f782f3513ac88ec9b6c82d9767eef95a5cf4175ce88a1e5a65875fee799608", size = 3159099 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/17/f4/50c42c84796886e4d360407fb629000bb68d843b2502c88318375441676f/pyobjc_framework_quartz-12.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c6f312ae79ef8b3019dcf4b3374c52035c7c7bc4a09a1748b61b041bb685a0ed", size = 217799 },
{ url = "https://files.pythonhosted.org/packages/b7/ef/dcd22b743e38b3c430fce4788176c2c5afa8bfb01085b8143b02d1e75201/pyobjc_framework_quartz-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:19f99ac49a0b15dd892e155644fe80242d741411a9ed9c119b18b7466048625a", size = 217795 },
{ url = "https://files.pythonhosted.org/packages/e9/9b/780f057e5962f690f23fdff1083a4cfda5a96d5b4d3bb49505cac4f624f2/pyobjc_framework_quartz-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7730cdce46c7e985535b5a42c31381af4aa6556e5642dc55b5e6597595e57a16", size = 218798 },
{ url = "https://files.pythonhosted.org/packages/ba/2d/e8f495328101898c16c32ac10e7b14b08ff2c443a756a76fd1271915f097/pyobjc_framework_quartz-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:629b7971b1b43a11617f1460cd218bd308dfea247cd4ee3842eb40ca6f588860", size = 219206 },
{ url = "https://files.pythonhosted.org/packages/67/43/b1f0ad3b842ab150a7e6b7d97f6257eab6af241b4c7d14cb8e7fde9214b8/pyobjc_framework_quartz-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:53b84e880c358ba1ddcd7e8d5ea0407d760eca58b96f0d344829162cda5f37b3", size = 224317 },
{ url = "https://files.pythonhosted.org/packages/4a/00/96249c5c7e5aaca5f688ca18b8d8ad05cd7886ebd639b3c71a6a4cadbe75/pyobjc_framework_quartz-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:42d306b07f05ae7d155984503e0fb1b701fecd31dcc5c79fe8ab9790ff7e0de0", size = 219558 },
{ url = "https://files.pythonhosted.org/packages/4d/a6/708a55f3ff7a18c403b30a29a11dccfed0410485a7548c60a4b6d4cc0676/pyobjc_framework_quartz-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0cc08fddb339b2760df60dea1057453557588908e42bdc62184b6396ce2d6e9a", size = 224580 },
]
[[package]]
name = "pyobjc-framework-security"
version = "12.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyobjc-core" },
{ name = "pyobjc-framework-cocoa" },
]
sdist = { url = "https://files.pythonhosted.org/packages/80/aa/796e09a3e3d5cee32ebeebb7dcf421b48ea86e28c387924608a05e3f668b/pyobjc_framework_security-12.1.tar.gz", hash = "sha256:7fecb982bd2f7c4354513faf90ba4c53c190b7e88167984c2d0da99741de6da9", size = 168044 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2e/67/31928b689b72a932c80e35662430355de09163bec8ee334f0994d16c4036/pyobjc_framework_security-12.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:787e9d873535247e2caca2036cbcdc956bcc92d0c06044bec7eefe0a456856b0", size = 41288 },
{ url = "https://files.pythonhosted.org/packages/5e/3d/8d3a39cd292d7c76ab76233498189bc7170fc80f573b415308464f68c7ee/pyobjc_framework_security-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1b2d8819f0fb7b619ec7627a0d8c1cac1a57c5143579ce8ac21548165680684b", size = 41287 },
{ url = "https://files.pythonhosted.org/packages/76/66/5160c0f938fc0515fe8d9af146aac1b093f7ef285ce797fedae161b6c0e8/pyobjc_framework_security-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab42e55f5b782332be5442750fcd9637ee33247d57c7b1d5801bc0e24ee13278", size = 41280 },
{ url = "https://files.pythonhosted.org/packages/32/48/b294ed75247c5cfa00d51925a10237337d24f54961d49a179b20a4307642/pyobjc_framework_security-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:afc36661cc6eb98cd794bed1d6668791e96557d6f72d9ac70aa49022d26af1d4", size = 41284 },
{ url = "https://files.pythonhosted.org/packages/ef/57/0d3ef78779cf5c3bba878b2f824137e50978ad4a21dabe65d8b5ae0fc0d1/pyobjc_framework_security-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:9510c98ab56921d1d416437372605cc1c1f6c1ad8d3061ee56b17bf423dd5427", size = 42162 },
{ url = "https://files.pythonhosted.org/packages/66/4d/63c15f9449c191e7448a05ff8af4a82c39a51bb627bc96dc9697586c0f79/pyobjc_framework_security-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:6319a34508fd87ab6ca3cda6f54e707196197a65b792b292705af967e225438a", size = 41348 },
{ url = "https://files.pythonhosted.org/packages/1a/d8/5aaa2a8124ed04a9d6ca7053dc0fa64e42be51497ed8263a24b744a95598/pyobjc_framework_security-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:03d166371cefdef24908825148eb848f99ee2c0b865870a09dcbb94334dd3e0a", size = 42908 },
]
[[package]]
name = "pyobjc-framework-uniformtypeidentifiers"
version = "12.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyobjc-core" },
{ name = "pyobjc-framework-cocoa" },
]
sdist = { url = "https://files.pythonhosted.org/packages/65/b8/dd9d2a94509a6c16d965a7b0155e78edf520056313a80f0cd352413f0d0b/pyobjc_framework_uniformtypeidentifiers-12.1.tar.gz", hash = "sha256:64510a6df78336579e9c39b873cfcd03371c4b4be2cec8af75a8a3d07dff607d", size = 17030 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4e/5f/1f10f5275b06d213c9897850f1fca9c881c741c1f9190cea6db982b71824/pyobjc_framework_uniformtypeidentifiers-12.1-py2.py3-none-any.whl", hash = "sha256:ec5411e39152304d2a7e0e426c3058fa37a00860af64e164794e0bcffee813f2", size = 4901 },
]
[[package]]
name = "pyobjc-framework-webkit"
version = "12.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyobjc-core" },
{ name = "pyobjc-framework-cocoa" },
]
sdist = { url = "https://files.pythonhosted.org/packages/14/10/110a50e8e6670765d25190ca7f7bfeecc47ec4a8c018cb928f4f82c56e04/pyobjc_framework_webkit-12.1.tar.gz", hash = "sha256:97a54dd05ab5266bd4f614e41add517ae62cdd5a30328eabb06792474b37d82a", size = 284531 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4a/79/b5582113b28cae64cec4aca63d36620421c21ca52f3897388b865a0dbb86/pyobjc_framework_webkit-12.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:231048d250e97323b25e5f1690d09e2415b691c0d57bc13241e442d486ef94c8", size = 49971 },
{ url = "https://files.pythonhosted.org/packages/e5/37/5082a0bbe12e48d4ffa53b0c0f09c77a4a6ffcfa119e26fa8dd77c08dc1c/pyobjc_framework_webkit-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3db734877025614eaef4504fadc0fbbe1279f68686a6f106f2e614e89e0d1a9d", size = 49970 },
{ url = "https://files.pythonhosted.org/packages/db/67/64920c8d201a7fc27962f467c636c4e763b43845baba2e091a50a97a5d52/pyobjc_framework_webkit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:af2c7197447638b92aafbe4847c063b6dd5e1ed83b44d3ce7e71e4c9b042ab5a", size = 50084 },
{ url = "https://files.pythonhosted.org/packages/7a/3d/80d36280164c69220ce99372f7736a028617c207e42cb587716009eecb88/pyobjc_framework_webkit-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1da0c428c9d9891c93e0de51c9f272bfeb96d34356cdf3136cb4ad56ce32ec2d", size = 50096 },
{ url = "https://files.pythonhosted.org/packages/8a/7a/03c29c46866e266b0c705811c55c22625c349b0a80f5cf4776454b13dc4c/pyobjc_framework_webkit-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:1a29e334d5a7dd4a4f0b5647481b6ccf8a107b92e67b2b3c6b368c899f571965", size = 50572 },
{ url = "https://files.pythonhosted.org/packages/3b/ac/924878f239c167ffe3bfc643aee4d6dd5b357e25f6b28db227e40e9e6df3/pyobjc_framework_webkit-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:99d0d28542a266a95ee2585f51765c0331794bca461aaf4d1f5091489d475179", size = 50210 },
{ url = "https://files.pythonhosted.org/packages/2d/86/637cda4983dc0936b73a385f3906256953ac434537b812814cb0b6d231a2/pyobjc_framework_webkit-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1aaa3bf12c7b68e1a36c0b294d2728e06f2cc220775e6dc4541d5046290e4dc8", size = 50680 },
]
[[package]]
name = "pyopenssl"
version = "25.3.0"
@@ -1034,6 +1239,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/36/7a/87837f39d0296e723bb9b62bbb257d0355c7f6128853c78955f57342a56d/python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9", size = 247702 },
]
[[package]]
name = "pythonnet"
version = "3.0.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "clr-loader" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9a/d6/1afd75edd932306ae9bd2c2d961d603dc2b52fcec51b04afea464f1f6646/pythonnet-3.0.5.tar.gz", hash = "sha256:48e43ca463941b3608b32b4e236db92d8d40db4c58a75ace902985f76dac21cf", size = 239212 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cd/f1/bfb6811df4745f92f14c47a29e50e89a36b1533130fcc56452d4660bd2d6/pythonnet-3.0.5-py3-none-any.whl", hash = "sha256:f6702d694d5d5b163c9f3f5cc34e0bed8d6857150237fae411fefb883a656d20", size = 297506 },
]
[[package]]
name = "pytz"
version = "2023.3"
@@ -1043,6 +1260,49 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/7f/99/ad6bd37e748257dd70d6f85d916cafe79c0b0f5e2e95b11f7fbc82bf3110/pytz-2023.3-py2.py3-none-any.whl", hash = "sha256:a151b3abb88eda1d4e34a9814df37de2a80e301e68ba0fd856fb9b46bfbbbffb", size = 502345 },
]
[[package]]
name = "pywebview"
version = "6.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "bottle" },
{ name = "proxy-tools" },
{ name = "pyobjc-core", marker = "sys_platform == 'darwin'" },
{ name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" },
{ name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" },
{ name = "pyobjc-framework-security", marker = "sys_platform == 'darwin'" },
{ name = "pyobjc-framework-uniformtypeidentifiers", marker = "sys_platform == 'darwin'" },
{ name = "pyobjc-framework-webkit", marker = "sys_platform == 'darwin'" },
{ name = "pythonnet", marker = "sys_platform == 'win32'" },
{ name = "qtpy", marker = "sys_platform == 'openbsd6'" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/64/09/75cf92c4db19350ddb084d841e352ce30e89668815d0494d1dd595c85469/pywebview-6.1.tar.gz", hash = "sha256:f0b95047860caf3d921581f9e16b4edb1a125b23e2ce691c4da2968f8b160ff2", size = 496270 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/74/12/ce9006cdcc1834378826b5c0d4b554d899cd25ac9c665569d060f220b0e1/pywebview-6.1-py3-none-any.whl", hash = "sha256:2b552a340557e76a740b045a25937f72dae751e0985d5f5d9556f697ba622ec5", size = 511255 },
]
[[package]]
name = "pywin32-ctypes"
version = "0.2.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756 },
]
[[package]]
name = "qtpy"
version = "2.4.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "packaging" },
]
sdist = { url = "https://files.pythonhosted.org/packages/70/01/392eba83c8e47b946b929d7c46e0f04b35e9671f8bb6fc36b6f7945b4de8/qtpy-2.4.3.tar.gz", hash = "sha256:db744f7832e6d3da90568ba6ccbca3ee2b3b4a890c3d6fbbc63142f6e4cdf5bb", size = 66982 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/69/76/37c0ccd5ab968a6a438f9c623aeecc84c202ab2fabc6a8fd927580c15b5a/QtPy-2.4.3-py3-none-any.whl", hash = "sha256:72095afe13673e017946cc258b8d5da43314197b741ed2890e563cf384b51aa1", size = 95045 },
]
[[package]]
name = "requests"
version = "2.32.5"