diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f5af8b1..7ae1424 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,9 @@ on: branches: [main] pull_request: +permissions: + contents: read + jobs: lint: runs-on: ubuntu-latest @@ -42,6 +45,16 @@ jobs: - name: Module boundary check run: uv run tach check + security: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - run: pip install semgrep + - run: semgrep scan --error --config=p/python --config=p/security-audit --config=p/owasp-top-ten src/ + test: runs-on: ubuntu-latest steps: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8dca671..5490698 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,3 +29,10 @@ repos: types: [python] pass_filenames: false verbose: true + - id: bandit + name: bandit (security scan, medium+) + entry: uv run bandit -r -ll src + language: system + types: [python] + pass_filenames: false + verbose: true diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..2bb2c0f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,43 @@ +# Changelog + +All notable changes to Monarch Forecast are documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project aims to follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added + +- Security scanning: bandit runs in pre-commit at medium+ severity; semgrep + runs in CI with the `p/python`, `p/security-audit`, and `p/owasp-top-ten` + rule packs. +- `SECURITY.md` describing the private vulnerability reporting process. +- `CONTRIBUTING.md` documenting the contribution, testing, and code-style + policy. + +### Changed + +- Hardened on-disk secret handling: session and cache files are created with + `0600` permissions atomically (no pre-`chmod` read window), reject symlinks + and non-regular files, and verify current-user ownership on open. +- Flipped `src.data` and `src.forecast` layering so the forecast engine sits + above raw data fetching; enforced via `tach`. + +### Fixed + +- Skip update check when running without installed package metadata + (avoids spurious "update available" banners from source checkouts). +- Honor credit-card amount overrides when the billing cycle has no charges. +- Avoid empty-`Semantics` dismiss state; tolerate corrupt one-off amounts. +- Accurate chart summary start balance; alerts live-region sync. + +## [0.1.0-alpha.1] — 2026-04-07 + +Initial alpha release. Cash-flow forecasting for Monarch Money checking +accounts, with recurring-transaction detection, credit-card payment +estimation, low-balance alerts, manual one-off adjustments, and cross-platform +desktop builds (macOS / Windows / Linux). + +[Unreleased]: https://github.com/rlorenzo/Monarch-Forecast/compare/v0.1.0-alpha.1...HEAD +[0.1.0-alpha.1]: https://github.com/rlorenzo/Monarch-Forecast/releases/tag/v0.1.0-alpha.1 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..74b1cef --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,52 @@ +# Contributing + +Thanks for your interest in contributing. Contributions of bug reports, +features, documentation, and tests are welcome. + +## Reporting bugs and requesting features + +Open an issue at [GitHub Issues](https://github.com/rlorenzo/Monarch-Forecast/issues) +and include: + +- What you expected to happen and what actually happened. +- Steps to reproduce. +- Your operating system and how you installed the project. +- Relevant log output, if any. + +For security vulnerabilities, follow [SECURITY.md](SECURITY.md) instead — do +not open a public issue. + +## Submitting changes + +1. Open an issue before investing significant time, so we can agree on scope + and approach. +2. Fork the repository and create a branch from `main`. +3. Set up your development environment — see the + [Development](README.md#development) section of the README. +4. Install the pre-commit hooks: `uv run pre-commit install`. They mirror the + checks CI runs. +5. Make your change, including tests (see below). +6. Open a pull request against `main`. All CI checks must pass. + +## Testing + +Pull requests that change behavior should include tests. Bug fixes should +include a regression test that fails without the fix. + +- Unit tests live in [tests/](tests/); mirror the source layout. +- Mock external boundaries (network, OS keychain, third-party clients). +- Prefer fixtures over shared mutable state. + +## Code style + +- Linting and type checking are enforced in CI and pre-commit. Fix all + findings before submitting. +- Run the formatter before committing. +- Keep public APIs documented and typed. + +## Commit messages + +- Use clear, imperative commit messages ("add X", "fix Y", not "added" + / "fixes"). +- Reference issues or PRs where relevant. +- Keep unrelated changes in separate commits. diff --git a/README.md b/README.md index bdf2996..134d234 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,57 @@ # Monarch Forecast -A desktop app built with [Flet](https://flet.dev/) (Flutter for Python) that projects your checking account balance day-by-day using data from [Monarch Money](https://www.monarchmoney.com/). See where your money is headed and spot shortfalls before they happen. +A desktop app built with [Flet](https://flet.dev/) (Flutter for Python) that +projects your checking account balance day-by-day using data from +[Monarch Money](https://www.monarchmoney.com/). See where your money is +headed and spot shortfalls before they happen. ## Screenshots -> *Screenshots coming soon. Run `uv run monarch-forecast` to see the app in action.* +> *Screenshots coming soon. Run `uv run monarch-forecast` to see the app in +> action.* ## Features -- **Cash flow forecasting** — Projects your checking account balance 45+ days ahead by combining recurring income/expenses with one-off transactions and credit card payment estimates -- **Low-balance alerts** — Flags dates where your balance is projected to drop below a safety threshold -- **Manual adjustments** — Add one-off transactions (upcoming bills, expected refunds) to refine the forecast -- **Auto-update notifications** — Checks GitHub Releases for newer versions on startup -- **Cross-platform** — Builds for macOS (.dmg), Windows (.msix), and Linux (.AppImage) +- **Cash flow forecasting** — Projects your checking account balance 45+ + days ahead by combining recurring income/expenses with one-off + transactions and credit card payment estimates +- **Low-balance alerts** — Flags dates where your balance is projected to + drop below a safety threshold +- **Manual adjustments** — Add one-off transactions (upcoming bills, + expected refunds) to refine the forecast +- **Auto-update notifications** — Checks GitHub Releases for newer + versions on startup +- **Cross-platform** — Builds for macOS (.dmg), Windows (.msix), and Linux + (.AppImage) ### How it works -- **Recurring transactions** are detected by analyzing 90 days of transaction history — the app groups by merchant, checks amount consistency, and infers frequency (weekly, biweekly, monthly, or yearly) -- **Credit card payments** are estimated by inferring each card's statement close and due days (from user settings or payment history) and summing charges across the current billing cycle, falling back to the card's recurring payment or current balance when history is insufficient -- Only **checking accounts** are forecasted — credit card, savings, and investment accounts are not included in projections +- **Recurring transactions** are detected by analyzing 90 days of + transaction history — the app groups by merchant, checks amount + consistency, and infers frequency (weekly, biweekly, monthly, or yearly) +- **Credit card payments** are estimated by inferring each card's statement + close and due days (from user settings or payment history) and summing + charges across the current billing cycle, falling back to the card's + recurring payment or current balance when history is insufficient +- Only **checking accounts** are forecasted — credit card, savings, and + investment accounts are not included in projections ## Installation ### From a release -Download the latest installer for your platform from [GitHub Releases](https://github.com/rlorenzo/Monarch-Forecast/releases). +Download the latest installer for your platform from +[GitHub Releases](https://github.com/rlorenzo/Monarch-Forecast/releases). **Platform notes:** -- **macOS** — The `.dmg` is not notarized. On first launch, right-click the app and choose "Open", or go to System Settings > Privacy & Security and click "Open Anyway". -- **Windows** — The `.msix` package may require enabling sideloading in Settings > Apps > Advanced app settings > Choose where to get apps. -- **Linux** — Make the `.AppImage` executable before running: `chmod +x Monarch-Forecast-*.AppImage` +- **macOS** — The `.dmg` is not notarized. On first launch, right-click + the app and choose "Open", or go to System Settings > Privacy & Security + and click "Open Anyway". +- **Windows** — The `.msix` package may require enabling sideloading in + Settings > Apps > Advanced app settings > Choose where to get apps. +- **Linux** — Make the `.AppImage` executable before running: + `chmod +x Monarch-Forecast-*.AppImage` ### From source @@ -45,7 +66,9 @@ uv run monarch-forecast ## Usage -On first launch you'll sign in to Monarch Money and select a checking account. The app projects your balance forward based on recurring transactions. +On first launch you'll sign in to Monarch Money and select a checking +account. The app projects your balance forward based on recurring +transactions. 1. Launch the app and sign in with your Monarch Money email and password 2. If your account has MFA enabled, you'll be prompted for a code @@ -55,20 +78,35 @@ On first launch you'll sign in to Monarch Money and select a checking account. T ### Authentication and data storage -This app uses the [monarchmoney](https://github.com/hammem/monarchmoney) community Python client — an unofficial, reverse-engineered API client (Monarch Money does not offer a public API). MFA is supported. +This app uses the [monarchmoney](https://github.com/hammem/monarchmoney) +community Python client — an unofficial, reverse-engineered API client +(Monarch Money does not offer a public API). MFA is supported. **What is stored locally:** -- **Credentials** — Email and password are stored in your OS keychain via [keyring](https://pypi.org/project/keyring/) (macOS Keychain, Windows Credential Locker, or SecretService on Linux). Cleared on logout. -- **Session token** — Saved to `~/.monarch-forecast/session.pickle` (file permissions `600`) for automatic session restore. Deleted on logout. -- **Preferences** — JSON file at `~/.monarch-forecast/preferences.json` storing excluded recurring items, credit card selections, amount overrides, and one-off transactions. -- **Transaction cache** — SQLite database at `~/.monarch-forecast/cache.db` caching recent Monarch data to avoid hammering the API on every launch. - -Your financial data is only sent to Monarch Money's servers. The only other outbound request is an update check to the GitHub Releases API on startup (no financial data is included). +- **Credentials** — Email and password are stored in your OS keychain via + [keyring](https://pypi.org/project/keyring/) (macOS Keychain, Windows + Credential Locker, or SecretService on Linux). Cleared on logout. +- **Session token** — Saved to `~/.monarch-forecast/session.pickle` (file + permissions `600`) for automatic session restore. Deleted on logout. +- **Preferences** — JSON file at `~/.monarch-forecast/preferences.json` + storing excluded recurring items, credit card selections, amount + overrides, and one-off transactions. +- **Transaction cache** — SQLite database at + `~/.monarch-forecast/cache.db` caching recent Monarch data to avoid + hammering the API on every launch. + +Your financial data is only sent to Monarch Money's servers. The only +other outbound request is an update check to the GitHub Releases API on +startup (no financial data is included). ## Development -This is a [Flet](https://flet.dev/) desktop app. Dependencies are managed with [uv](https://docs.astral.sh/uv/) via `pyproject.toml` and `uv.lock`. Always use `uv sync` for local development — do not install from `requirements.txt` (it exists only as a fallback for the CI build workflow and may not reflect the full locked dependency set). +This is a [Flet](https://flet.dev/) desktop app. Dependencies are managed +with [uv](https://docs.astral.sh/uv/) via `pyproject.toml` and `uv.lock`. +Always use `uv sync` for local development — do not install from +`requirements.txt` (it exists only as a fallback for the CI build workflow +and may not reflect the full locked dependency set). ```bash uv sync # install all dependencies (including dev) @@ -86,7 +124,11 @@ Set up pre-commit hooks (ruff lint/format + ty on every commit): uv run pre-commit install ``` -Tests are expected to pass before opening a PR. CI runs lint, type check, and pytest on all pull requests. +Tests are expected to pass before opening a PR. CI runs lint, type check, +and pytest on all pull requests. + +See [CONTRIBUTING.md](CONTRIBUTING.md) for the contribution and testing +policy, and [SECURITY.md](SECURITY.md) if you've found a vulnerability. ### Building desktop packages locally @@ -98,13 +140,18 @@ brew install cocoapods # required for macOS builds (see note below) uv run flet build macos # produces build/macos/Monarch Forecast.app ``` -**macOS CocoaPods note:** Do not use `sudo gem install cocoapods` — the system Ruby (2.6) is too old and the install will fail with `ffi` gem errors. Use `brew install cocoapods` instead, which bundles its own Ruby. If `flutter doctor` still reports CocoaPods as broken after installing, run `brew reinstall cocoapods`. +**macOS CocoaPods note:** Do not use `sudo gem install cocoapods` — the +system Ruby (2.6) is too old and the install will fail with `ffi` gem +errors. Use `brew install cocoapods` instead, which bundles its own Ruby. +If `flutter doctor` still reports CocoaPods as broken after installing, +run `brew reinstall cocoapods`. -See the [Flet packaging guide](https://flet.dev/docs/publish) for other platform-specific requirements. +See the [Flet packaging guide](https://flet.dev/docs/publish) for other +platform-specific requirements. ### Project structure -``` +```text src/ ├── main.py # Flet app entry point ├── auth/ # Login UI and session management (keyring + MFA) @@ -116,28 +163,66 @@ src/ ## Accessibility -Monarch Forecast is built to be usable with a screen reader, keyboard only, at increased text size, or in high-contrast themes. Known support level: - -- **Screen readers** — Every icon-only button carries a descriptive label, the balance chart exposes a text summary (start balance, ending balance, lowest point, threshold crossings), form errors are announced via live regions, and the Alerts banner is a live region so new shortfall/overdraft notices are spoken when they appear. Best-tested with **VoiceOver on macOS**; **Narrator on Windows** works for buttons and form fields. **Orca on Linux** has uneven support in Flutter desktop today — if you rely on Orca, expect gaps and please open an issue with what you hit. -- **Keyboard** — `Tab`/`Shift+Tab` moves between controls, `Esc` closes any open dialog, and global shortcuts work from anywhere in the dashboard: +Monarch Forecast is built to be usable with a screen reader, keyboard +only, at increased text size, or in high-contrast themes. Known support +level: + +- **Screen readers** — Every icon-only button carries a descriptive label, + the balance chart exposes a text summary (start balance, ending balance, + lowest point, threshold crossings), form errors are announced via live + regions, and the Alerts banner is a live region so new + shortfall/overdraft notices are spoken when they appear. Best-tested + with **VoiceOver on macOS**; **Narrator on Windows** works for buttons + and form fields. **Orca on Linux** has uneven support in Flutter desktop + today — if you rely on Orca, expect gaps and please open an issue with + what you hit. +- **Keyboard** — `Tab`/`Shift+Tab` moves between controls, `Esc` closes + any open dialog, and global shortcuts work from anywhere in the + dashboard: - `⌘R` / `Ctrl+R` — refresh data - `⌘1` / `Ctrl+1` — Overview tab - `⌘2` / `Ctrl+2` — Transactions tab - `⌘3` / `Ctrl+3` — Adjustments tab - Switching tabs auto-focuses the first meaningful control of the new tab. Date fields in the one-off transaction forms accept typed input (`YYYY-MM-DD`, `Jan 05, 2026`, `01/05/2026`), so you never have to open the calendar popover with a mouse. -- **Text scaling** — Icons grow with the OS text size (via the app's Material icon theme). Secondary text uses the theme-aware `ON_SURFACE_VARIANT` color so it remains readable in both light and dark modes. -- **Reduce motion** — On platforms that expose the "reduce motion" accessibility flag, the balance chart draws as straight line segments instead of a curved spline. -- **Alternative to the chart** — If you can't use the balance chart, the **Transactions** tab is a full text equivalent: every projected transaction with date, description, amount, and running balance, in a screen-reader-friendly data table. -**Reporting an accessibility bug:** open an issue at [GitHub Issues](https://github.com/rlorenzo/Monarch-Forecast/issues) with the label `accessibility`. Include your platform, your assistive technology (e.g. VoiceOver, NVDA, Narrator, Orca), and what you expected vs what happened — even small reports help. + Switching tabs auto-focuses the first meaningful control of the new + tab. Date fields in the one-off transaction forms accept typed input + (`YYYY-MM-DD`, `Jan 05, 2026`, `01/05/2026`), so you never have to open + the calendar popover with a mouse. +- **Text scaling** — Icons grow with the OS text size (via the app's + Material icon theme). Secondary text uses the theme-aware + `ON_SURFACE_VARIANT` color so it remains readable in both light and + dark modes. +- **Reduce motion** — On platforms that expose the "reduce motion" + accessibility flag, the balance chart draws as straight line segments + instead of a curved spline. +- **Alternative to the chart** — If you can't use the balance chart, the + **Transactions** tab is a full text equivalent: every projected + transaction with date, description, amount, and running balance, in a + screen-reader-friendly data table. + +**Reporting an accessibility bug:** open an issue at +[GitHub Issues](https://github.com/rlorenzo/Monarch-Forecast/issues) with +the label `accessibility`. Include your platform, your assistive +technology (e.g. VoiceOver, NVDA, Narrator, Orca), and what you expected +vs what happened — even small reports help. ## Troubleshooting -- **Login fails or session won't restore** — Delete `~/.monarch-forecast/session.pickle` and try again. If MFA is enabled on your Monarch account, make sure you enter the code when prompted. -- **Keychain access denied** — On macOS, the app needs Keychain Access permission. On Linux, make sure a SecretService provider (like `gnome-keyring` or `kwallet`) is running. -- **"App is damaged" / Gatekeeper warning (macOS)** — The app is not notarized. Right-click and choose "Open", or allow it in System Settings > Privacy & Security. -- **AppImage won't run (Linux)** — Make sure it's executable: `chmod +x Monarch-Forecast-*.AppImage`. You may also need FUSE installed (`sudo apt install libfuse2` on Ubuntu). -- **Update banner doesn't appear** — The update check is best-effort and requires internet access. It queries the GitHub Releases API on startup; failures are silently ignored. +- **Login fails or session won't restore** — Delete + `~/.monarch-forecast/session.pickle` and try again. If MFA is enabled + on your Monarch account, make sure you enter the code when prompted. +- **Keychain access denied** — On macOS, the app needs Keychain Access + permission. On Linux, make sure a SecretService provider (like + `gnome-keyring` or `kwallet`) is running. +- **"App is damaged" / Gatekeeper warning (macOS)** — The app is not + notarized. Right-click and choose "Open", or allow it in System + Settings > Privacy & Security. +- **AppImage won't run (Linux)** — Make sure it's executable: + `chmod +x Monarch-Forecast-*.AppImage`. You may also need FUSE installed + (`sudo apt install libfuse2` on Ubuntu). +- **Update banner doesn't appear** — The update check is best-effort and + requires internet access. It queries the GitHub Releases API on startup; + failures are silently ignored. ## License diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..8d02599 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,60 @@ +# Security Policy + +We take security issues seriously and appreciate responsible disclosure. Thank +you for helping keep this project and its users safe. + +## Reporting a vulnerability + +**Please do not report vulnerabilities through public GitHub issues, +discussions, or pull requests.** + +Report privately via GitHub's private vulnerability reporting: + +> [Report a vulnerability](https://github.com/rlorenzo/Monarch-Forecast/security/advisories/new) + +If you cannot use GitHub, email `rexlorenzo@gmail.com` with the subject line +`[Monarch Forecast Security]`. + +Please include: + +- A description of the issue and its potential impact. +- Steps to reproduce, ideally with a minimal proof of concept. +- The affected version (git commit SHA or release tag). +- Your operating system and runtime details, if relevant. +- Whether you would like to be credited in the release notes. + +## What to expect + +- **Initial response within 14 days** acknowledging the report. +- Regular status updates as the investigation progresses. +- Coordinated disclosure: we will agree on a timeline with you before any + public discussion of the issue. + +## Scope + +Monarch Forecast is a desktop application that handles financial account +credentials and session data. In scope: + +- **Credential handling** — storage and retrieval of Monarch Money credentials + via `keyring` and the OS keychain. +- **Local data files** — the session pickle, SQLite cache, and preferences + file under `~/.monarch-forecast/`, including file permissions and symlink + handling. +- **Network communication** — traffic to Monarch Money's servers and the + GitHub Releases API. +- **Release artifact integrity** — the installers published on + [GitHub Releases](https://github.com/rlorenzo/Monarch-Forecast/releases). + +Out of scope: + +- Vulnerabilities in Monarch Money's backend or API. +- Vulnerabilities in third-party dependencies already disclosed upstream — + please report those to the relevant project. We will update once an + upstream fix ships. +- Issues that require the attacker to already have local code execution or + filesystem write access as the running user. + +## Supported versions + +Only the latest release receives security updates. Please upgrade before +reporting. diff --git a/pyproject.toml b/pyproject.toml index 82788e8..2949b85 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,7 @@ dev = [ "pre-commit>=4.0", "vulture>=2.14", "tach>=0.34", + "bandit>=1.9", ] [tool.setuptools.packages.find] diff --git a/src/utils/updater.py b/src/utils/updater.py index 8649b13..37f0700 100644 --- a/src/utils/updater.py +++ b/src/utils/updater.py @@ -41,7 +41,9 @@ def check_for_update() -> dict[str, Any] | None: "User-Agent": f"Monarch-Forecast/{CURRENT_VERSION}", }, ) - with urlopen(req, timeout=10) as resp: + # RELEASES_URL is a hardcoded https://api.github.com constant — no + # attacker-controlled scheme is reachable here. + with urlopen(req, timeout=10) as resp: # nosec B310 # nosemgrep: dynamic-urllib-use-detected data = json.loads(resp.read().decode()) except (URLError, json.JSONDecodeError, OSError): return None diff --git a/uv.lock b/uv.lock index 7a21da7..699775f 100644 --- a/uv.lock +++ b/uv.lock @@ -216,6 +216,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", size = 30181, upload-time = "2024-05-28T17:01:53.112Z" }, ] +[[package]] +name = "bandit" +version = "1.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "pyyaml" }, + { name = "rich" }, + { name = "stevedore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/c3/0cb80dfe0f3076e5da7e4c5ad8e57bac6ac357ff4a6406205501cade4965/bandit-1.9.4.tar.gz", hash = "sha256:b589e5de2afe70bd4d53fa0c1da6199f4085af666fde00e8a034f152a52cd628", size = 4242677, upload-time = "2026-02-25T06:44:15.503Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/a4/a26d5b25671d27e03afb5401a0be5899d94ff8fab6a698b1ac5be3ec29ef/bandit-1.9.4-py3-none-any.whl", hash = "sha256:f89ffa663767f5a0585ea075f01020207e966a9c0f2b9ef56a57c7963a3f6f8e", size = 134741, upload-time = "2026-02-25T06:44:13.694Z" }, +] + [[package]] name = "certifi" version = "2026.2.25" @@ -863,6 +878,7 @@ dependencies = [ [package.dev-dependencies] dev = [ + { name = "bandit" }, { name = "pre-commit" }, { name = "pytest" }, { name = "pytest-asyncio" }, @@ -883,6 +899,7 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ + { name = "bandit", specifier = ">=1.9" }, { name = "pre-commit", specifier = ">=4.0" }, { name = "pytest", specifier = ">=8.0" }, { name = "pytest-asyncio", specifier = ">=0.25.0" }, @@ -1600,6 +1617,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl", hash = "sha256:c106e05d5a61449cf6ba9a1e650227ecfb141590d2a98412103ff35d89fc7b2f", size = 24390, upload-time = "2026-03-09T03:43:24.361Z" }, ] +[[package]] +name = "stevedore" +version = "5.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6d/90764092216fa560f6587f83bb70113a8ba510ba436c6476a2b47359057c/stevedore-5.7.0.tar.gz", hash = "sha256:31dd6fe6b3cbe921e21dcefabc9a5f1cf848cf538a1f27543721b8ca09948aa3", size = 516200, upload-time = "2026-02-20T13:27:06.765Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/06/36d260a695f383345ab5bbc3fd447249594ae2fa8dfd19c533d5ae23f46b/stevedore-5.7.0-py3-none-any.whl", hash = "sha256:fd25efbb32f1abb4c9e502f385f0018632baac11f9ee5d1b70f88cc5e22ad4ed", size = 54483, upload-time = "2026-02-20T13:27:05.561Z" }, +] + [[package]] name = "tach" version = "0.34.1"