Compare commits

..

135 Commits

Author SHA1 Message Date
latest-changes[bot] aabb901269 📝 Update release notes
[skip ci]
2026-11-03 13:46:49 +00:00
dependabot[bot] 42e7ed7df3 ⬆ Bump the npm-packages group across 1 directory with 30 updates (#2525)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Yurii Motov <yurii.motov.monte@gmail.com>
Co-authored-by: pr-push[bot] <pr-push[bot]@users.noreply.github.com>
2026-11-03 14:46:45 +01:00
latest-changes[bot] 9c81b46ff7 📝 Update release notes
[skip ci]
2026-11-03 11:19:51 +00:00
dependabot[bot] 58570c6353 ⬆ Bump playwright from v1.63.0-noble to v1.64.1-noble in /frontend in the docker group across 1 directory (#2521)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Yurii Motov <yurii.motov.monte@gmail.com>
2026-11-03 12:19:47 +01:00
latest-changes[bot] 0a6ec28757 📝 Update release notes
[skip ci]
2026-11-03 11:04:00 +00:00
dependabot[bot] df6f7a501c ⬆ Bump the github-actions group with 6 updates (#2520)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-03 12:03:55 +01:00
latest-changes[bot] 85579cd465 📝 Update release notes
[skip ci]
2026-11-03 11:00:41 +00:00
pr-submit[bot] 2fae9be8f5 ⬆ Bump pre-commit hooks (#2523)
Co-authored-by: pr-submit[bot] <pr-submit[bot]@users.noreply.github.com>
2026-11-03 12:00:37 +01:00
latest-changes[bot] c1c1b204b0 📝 Update release notes
[skip ci]
2026-11-03 10:59:19 +00:00
dependabot[bot] 27a82c4c1c ⬆ Bump dnspython from 2.8.0 to 2.9.0 (#2524)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-03 11:59:16 +01:00
latest-changes[bot] f4bcfaba7c 📝 Update release notes
[skip ci]
2026-10-26 10:47:35 +00:00
dependabot[bot] 0a351e3629 ⬆ Bump certifi from 2026.6.17 to 2026.10.19 (#2518)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-26 11:47:31 +01:00
latest-changes[bot] b1125d8839 📝 Update release notes
[skip ci]
2026-10-15 16:04:31 +00:00
Sebastián Ramírez 99e9ec1ec1 🎨 Enable Ruff formatting for Python in Markdown (#2517) 2026-10-15 18:04:28 +02:00
latest-changes[bot] 7257e606cb 📝 Update release notes
[skip ci]
2026-10-08 15:50:29 +00:00
Yurii Motov 0007f609ec ⬆️ Migrate @tanstack/react-table to 9.x (#2509) 2026-10-08 17:50:25 +02:00
latest-changes[bot] f27b472150 📝 Update release notes
[skip ci]
2026-10-07 07:55:25 +00:00
dependabot[bot] 96d9db1272 ⬆ Bump the npm-packages group across 1 directory with 25 updates (#2506)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Yurii Motov <yurii.motov.monte@gmail.com>
2026-10-07 09:55:21 +02:00
latest-changes[bot] e99fbaeba3 📝 Update release notes
[skip ci]
2026-10-06 13:46:35 +00:00
dependabot[bot] b4082487d2 ⬆ Bump mako from 1.3.12 to 1.4.2 (#2507)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-06 15:46:32 +02:00
latest-changes[bot] ea741e7de7 📝 Update release notes
[skip ci]
2026-10-06 11:50:16 +00:00
dependabot[bot] 754ff56c5a ⬆ Bump the python-packages group across 1 directory with 8 updates (#2497)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Yurii Motov <yurii.motov.monte@gmail.com>
2026-10-06 13:50:13 +02:00
latest-changes[bot] 1762adac60 📝 Update release notes
[skip ci]
2026-10-03 06:49:54 +00:00
dependabot[bot] 7e53197e3f Bump axios from 1.18.0 to 1.20.0 in /frontend (#2505)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Yurii Motov <yurii.motov.monte@gmail.com>
2026-10-03 08:49:51 +02:00
latest-changes[bot] 3ecbabf771 📝 Update release notes
[skip ci]
2026-10-03 05:45:20 +00:00
dependabot[bot] 1bbfc36a0b ⬆ Bump anyio from 4.14.1 to 4.14.2 (#2483)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-03 07:45:17 +02:00
latest-changes[bot] 5cdc56bb2a 📝 Update release notes
[skip ci]
2026-10-03 05:43:15 +00:00
dependabot[bot] fb9dcb0a78 ⬆ Update sentry-sdk[fastapi] requirement from <3.0.0,>=2.68.1 to >=2.70.0,<3.0.0 (#2498)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-03 07:43:13 +02:00
latest-changes[bot] 5af813a478 📝 Update release notes
[skip ci]
2026-10-03 05:39:31 +00:00
pr-submit[bot] 5e5b158d73 ⬆ Bump pre-commit hooks (#2501)
Co-authored-by: pr-submit[bot] <pr-submit[bot]@users.noreply.github.com>
Co-authored-by: Yurii Motov <109919500+YuriiMotov@users.noreply.github.com>
2026-10-03 07:39:28 +02:00
latest-changes[bot] 82080c9c72 📝 Update release notes
[skip ci]
2026-10-03 05:38:43 +00:00
dependabot[bot] 7dd1553697 ⬆ Bump playwright from v1.62.1-noble to v1.63.0-noble in /frontend in the docker group across 1 directory (#2496)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Yurii Motov <yurii.motov.monte@gmail.com>
2026-10-03 07:38:40 +02:00
latest-changes[bot] d58735ff12 📝 Update release notes
[skip ci]
2026-10-03 05:35:30 +00:00
dependabot[bot] f991c04374 ⬆ Bump urllib3 from 2.7.0 to 2.8.0 (#2503)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-03 07:35:27 +02:00
latest-changes[bot] d0af00bf3c 📝 Update release notes
[skip ci]
2026-10-03 05:30:57 +00:00
dependabot[bot] 6225e082cb ⬆ Update psycopg[binary] requirement from <4.0.0,>=3.3.4 to >=3.3.6,<4.0.0 (#2499)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-03 07:30:55 +02:00
latest-changes[bot] 38b1c706ee 📝 Update release notes
[skip ci]
2026-10-03 05:15:04 +00:00
dependabot[bot] aae80fe6bf ⬆ Bump pyjwt from 2.13.0 to 2.15.0 (#2494)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-03 07:15:01 +02:00
latest-changes[bot] 938191aa87 📝 Update release notes
[skip ci]
2026-10-03 05:11:20 +00:00
dependabot[bot] f07a4917b8 ⬆ Bump the github-actions group with 2 updates (#2495)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-03 07:11:13 +02:00
latest-changes[bot] cb740b656d 📝 Update release notes
[skip ci]
2026-09-01 18:42:01 +00:00
dependabot[bot] 041807b946 ⬆ Bump traefik from 3.6 to v3.7 in the docker-compose group (#2465)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-01 20:41:58 +02:00
latest-changes[bot] 3000041090 📝 Update release notes
[skip ci]
2026-09-01 12:36:10 +00:00
pr-submit[bot] ea37d2f5fb ⬆ Bump pre-commit hooks (#2471)
Co-authored-by: pr-submit[bot] <pr-submit[bot]@users.noreply.github.com>
2026-09-01 14:36:07 +02:00
latest-changes[bot] bf63770a9b 📝 Update release notes
[skip ci]
2026-09-01 12:33:17 +00:00
dependabot[bot] 3be4719849 ⬆ Update pwdlib[argon2,bcrypt] requirement from >=0.3.0 to >=0.3.1 (#2468)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-01 14:33:13 +02:00
latest-changes[bot] 2132b209eb 📝 Update release notes
[skip ci]
2026-09-01 12:22:32 +00:00
dependabot[bot] bb92a02ca2 ⬆ Update sentry-sdk[fastapi] requirement from <3.0.0,>=2.66.1 to >=2.68.1,<3.0.0 (#2467)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Yurii Motov <109919500+YuriiMotov@users.noreply.github.com>
2026-09-01 14:22:29 +02:00
latest-changes[bot] 9d4ecbfc85 📝 Update release notes
[skip ci]
2026-09-01 12:16:02 +00:00
dependabot[bot] 10dd26c613 ⬆ Bump the python-packages group with 4 updates (#2466)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Yurii Motov <yurii.motov.monte@gmail.com>
2026-09-01 14:15:59 +02:00
latest-changes[bot] 5b632d4f88 📝 Update release notes
[skip ci]
2026-09-01 10:43:36 +00:00
dependabot[bot] bbe269d382 ⬆ Bump the github-actions group with 4 updates (#2464)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-01 12:43:33 +02:00
latest-changes[bot] 486f054cc8 📝 Update release notes
[skip ci]
2026-08-28 11:53:12 +00:00
Sebastián Ramírez f8dd304eeb ♻️ Simplify database readiness checks (#2460) 2026-08-28 13:53:10 +02:00
latest-changes[bot] 2ccfa25845 📝 Update release notes
[skip ci]
2026-08-27 17:18:32 +00:00
Yurii Motov f8ba0b81cd ⬆️ Bump Typer min version to 0.26.1 (#2459) 2026-08-27 19:18:29 +02:00
latest-changes[bot] 8063fe54f1 📝 Update release notes
[skip ci]
2026-08-25 17:07:34 +00:00
Yurii Motov 4d1a75de20 ⬆️ Bump setup-uv action to 10.0.1 (#2458) 2026-08-25 19:07:31 +02:00
latest-changes[bot] 68adb40d37 📝 Update release notes
[skip ci]
2026-08-21 19:41:30 +00:00
Alejandra 7c11b644a3 ✅ Simplify password reset E2E tests (#2453) 2026-08-21 21:41:27 +02:00
latest-changes[bot] 6b5b886760 📝 Update release notes
[skip ci]
2026-08-21 11:25:06 +00:00
Alejandra 9a5c827ba6 📝 Update README.md with Mailpit and React Email (#2452) 2026-08-21 13:25:03 +02:00
latest-changes[bot] b9cdd92dcc 📝 Update release notes
[skip ci]
2026-08-21 11:09:51 +00:00
Alejandra 00a6aa978e 💄 Refactor email templates and components for improved styling (#2451) 2026-08-21 13:09:48 +02:00
latest-changes[bot] 2734dfd002 📝 Update release notes
[skip ci]
2026-08-21 11:09:34 +00:00
Chris (BS-1495) f369a27746 🐛 Fix Sonner toast theme sync (#2450) 2026-08-21 13:09:31 +02:00
latest-changes[bot] 162344da11 📝 Update release notes
[skip ci]
2026-08-17 08:38:41 +00:00
Alejandra 981ae061b4 ♻️ Replace Mailcatcher with Mailpit for local email testing (#2436) 2026-08-17 10:38:38 +02:00
latest-changes[bot] 75b4026443 📝 Update release notes
[skip ci]
2026-08-14 17:22:11 +00:00
Ide d95518294e 🐛 Add background to the sticky layout header (#2384)
Co-authored-by: Alejandra <90076947+alejsdev@users.noreply.github.com>
2026-08-14 19:22:05 +02:00
latest-changes[bot] c350936d28 📝 Update release notes
[skip ci]
2026-08-12 09:25:43 +00:00
Sebastián Ramírez 42b4ca0586 📝 Simplify project documentation (#2442) 2026-08-12 11:25:40 +02:00
pr-submit[bot] 84c5a9e11a 🔖 Release version 0.12.0 (#2441)
Co-authored-by: pr-submit[bot] <pr-submit[bot]@users.noreply.github.com>
2026-08-12 08:46:45 +00:00
latest-changes[bot] 98fb606b57 📝 Update release notes
[skip ci]
2026-08-12 07:28:45 +00:00
Sebastián Ramírez de2ed05b10 🐳 Include frontend assets in Docker build context (#2440) 2026-08-12 09:28:43 +02:00
latest-changes[bot] ec86250a21 📝 Update release notes
[skip ci]
2026-08-12 07:19:49 +00:00
Sebastián Ramírez 7f11815e43 👷 Migrate automatic labels to Latest Changes (#2439) 2026-08-12 09:19:46 +02:00
latest-changes[bot] a2d2d18108 📝 Update release notes
[skip ci]
2026-08-11 21:16:38 +00:00
Sebastián Ramírez 2d8fb3e4de ☁️ Add FastAPI Cloud deployment (#2438) 2026-08-11 23:16:35 +02:00
pr-submit[bot] ddb3c34f19 🔖 Release version 0.11.1 (#2437)
Co-authored-by: pr-submit[bot] <pr-submit[bot]@users.noreply.github.com>
2026-08-11 21:57:14 +02:00
latest-changes[bot] eae2d6701d 📝 Update release notes
[skip ci]
2026-08-11 19:35:48 +00:00
Sebastián Ramírez 6b9fa4113c ♻️ Simplify Docker Compose deployment (#2435) 2026-08-11 21:35:45 +02:00
latest-changes[bot] b55b3122ab 📝 Update release notes
[skip ci]
2026-08-11 18:08:26 +00:00
dependabot[bot] 6dbb2e8256 ⬆ Bump the npm-packages group across 1 directory with 33 updates (#2433)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Alejandra <90076947+alejsdev@users.noreply.github.com>
Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
Co-authored-by: Yurii Motov <yurii.motov.monte@gmail.com>
2026-08-11 20:08:23 +02:00
latest-changes[bot] 6d75b2d132 📝 Update release notes
[skip ci]
2026-08-11 17:28:37 +00:00
Patrick Arminio 32ab6dd022 ♻️ Migrate to DATABASE_URL instead of separate variables (#2184)
Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
2026-08-11 19:28:34 +02:00
pr-submit[bot] cf1548a983 🔖 Release version 0.11.0 (#2434)
Co-authored-by: pr-submit[bot] <pr-submit[bot]@users.noreply.github.com>
2026-08-11 09:29:32 +00:00
latest-changes[bot] c8f9069168 📝 Update release notes
[skip ci]
2026-08-11 08:53:26 +00:00
Sebastián Ramírez 512e496e1f 👷 Add release automation (#2431) 2026-08-11 10:53:23 +02:00
latest-changes[bot] fd0ca44d77 📝 Update release notes
[skip ci]
2026-08-10 22:01:38 +00:00
Sebastián Ramírez af791805ac ♻️ Remove Copier project generation (#2430) 2026-08-11 00:01:36 +02:00
latest-changes[bot] 9a218cd74a 📝 Update release notes
[skip ci]
2026-08-10 21:42:39 +00:00
Sebastián Ramírez d85803bf64 ♻️ Simplify CORS configuration (#2429)
## AI Disclaimer

Using Codex with gpt-5.6-sol. Reviewed manually.
2026-08-10 23:42:37 +02:00
latest-changes[bot] 781b283849 📝 Update release notes
[skip ci]
2026-08-10 21:33:19 +00:00
Sebastián Ramírez c291285975 📝 Remove obsolete local domain setup (#2428)
## AI Disclaimer

Using Codex with gpt-5.6-sol. Reviewed manually.
2026-08-10 23:33:16 +02:00
latest-changes[bot] b1abd91ba7 📝 Update release notes
[skip ci]
2026-08-10 21:22:02 +00:00
Sebastián Ramírez e95e501951 ♻️ Use FASTAPI_ENV for development (#2427)
## AI Disclaimer

Using Codex with gpt-5.6-sol. Reviewed manually.
2026-08-10 23:21:59 +02:00
latest-changes[bot] 111c5fad66 📝 Update release notes
[skip ci]
2026-08-10 21:09:56 +00:00
Sebastián Ramírez 9d541082a3 ♻️ Run database setup explicitly before startup (#2426)
## AI Disclaimer

Using Codex with gpt-5.6-sol. Reviewed manually.
2026-08-10 23:09:54 +02:00
latest-changes[bot] 8893c6807a 📝 Update release notes
[skip ci]
2026-08-10 20:52:34 +00:00
Sebastián Ramírez 1c5dd70308 ♻️ Require explicit SECRET_KEY (#2425)
## AI Disclaimer

Using Codex with gpt-5.6-sol. Reviewed manually.
2026-08-10 22:52:32 +02:00
latest-changes[bot] b5a664d510 📝 Update release notes
[skip ci]
2026-08-10 20:42:32 +00:00
Sebastián Ramírez 23b0ba9bad 👷 Remove legacy label check (#2424) 2026-08-10 22:42:29 +02:00
latest-changes[bot] 74cf9ed832 📝 Update release notes
[skip ci]
2026-08-10 20:38:18 +00:00
Sebastián Ramírez 4256e83fec ♻️ Simplify uv workspace setup (#2423)
## AI Disclaimer

Using Codex with gpt-5.6-sol. Reviewed manually.
2026-08-10 22:38:15 +02:00
latest-changes[bot] 85cd14d474 📝 Update release notes
[skip ci]
2026-08-10 18:01:15 +00:00
Sebastián Ramírez 5069df32ea ♻️ Use FastAPI entrypoint in Compose (#2420)
## AI Disclaimer

Using Codex with gpt-5.6-sol. Reviewed manually.
2026-08-10 20:01:12 +02:00
latest-changes[bot] f2035ef732 📝 Update release notes
[skip ci]
2026-08-10 17:59:38 +00:00
Alejandra b0ccf1e80e ✨ Add react-email for email templates (#2421) 2026-08-10 19:59:35 +02:00
latest-changes[bot] de53c8fd35 📝 Update release notes
[skip ci]
2026-08-10 17:43:14 +00:00
Sebastián Ramírez 375af70ee2 ✅ Fix sign-up test race (#2422)
## AI Disclaimer

Using Codex with gpt-5.6-sol. Reviewed manually.
2026-08-10 19:43:11 +02:00
latest-changes[bot] 6c2122e37d 📝 Update release notes
[skip ci]
2026-08-10 16:46:22 +00:00
Sebastián Ramírez 2c28c6f7dc 📝 Clarify environment configuration (#2419)
## AI Disclaimer

Using Codex with gpt-5.6-sol. Reviewed manually.
2026-08-10 18:46:19 +02:00
latest-changes[bot] a5aff58850 📝 Update release notes
[skip ci]
2026-08-10 16:33:53 +00:00
Sebastián Ramírez 060795204b ♻️ Require non-empty Compose configuration (#2418)
## AI Disclaimer

Using Codex with gpt-5.6-sol. Reviewed manually.
2026-08-10 18:33:51 +02:00
latest-changes[bot] 1a30f30e51 📝 Update release notes
[skip ci]
2026-08-10 16:17:31 +00:00
Sebastián Ramírez dd15054da0 ♻️ Simplify optional environment configuration (#2417)
## AI Disclaimer

Using Codex with gpt-5.6-sol. Reviewed manually.
2026-08-10 18:17:29 +02:00
latest-changes[bot] bda0325818 📝 Update release notes
[skip ci]
2026-08-10 15:56:36 +00:00
Sebastián Ramírez 2c7e51505e ♻️ Simplify Copier environment configuration (#2416)
## AI Disclaimer

Using Codex with gpt-5.6-sol. Reviewed manually.
2026-08-10 17:56:33 +02:00
latest-changes[bot] 0625bcc8b0 📝 Update release notes
[skip ci]
2026-08-10 15:40:02 +00:00
Sebastián Ramírez 4632845a39 ♻️ Simplify environment defaults (#2415)
## AI Disclaimer

Using Codex with gpt-5.6-sol. Reviewed manually.
2026-08-10 17:39:59 +02:00
latest-changes[bot] 029e929253 📝 Update release notes
[skip ci]
2026-08-10 15:23:50 +00:00
Sebastián Ramírez 8a213ddae7 ♻️ Simplify local development workflow (#2414)
## AI Disclaimer

Using Codex with gpt-5.6-sol. Reviewed manually.
2026-08-10 17:23:47 +02:00
latest-changes[bot] 66f444a63a 📝 Update release notes
[skip ci]
2026-08-09 20:26:21 +00:00
Sebastián Ramírez 9c8983e2a5 ♻️ Simplify environment configuration (#2413) 2026-08-09 22:26:19 +02:00
latest-changes[bot] 2d04828d6a 📝 Update release notes
[skip ci]
2026-08-09 18:21:44 +00:00
Sebastián Ramírez 0137c3f2c3 ♻️ Simplify Traefik deployment, make it part of the stack (#2412) 2026-08-09 20:21:42 +02:00
latest-changes[bot] 75c388ba1c 📝 Update release notes
[skip ci]
2026-08-08 22:47:42 +00:00
Sebastián Ramírez d5b20172e4 🔐 Use PR Submit for pull requests (#2411) 2026-08-09 00:47:39 +02:00
latest-changes[bot] 9097b329d6 📝 Update release notes
[skip ci]
2026-08-08 22:28:20 +00:00
Sebastián Ramírez 6a1c084f6e 👷 Use GitHub CLI for Git authentication (#2410) 2026-08-09 00:28:17 +02:00
latest-changes[bot] 220bcb74aa 📝 Update release notes
[skip ci]
2026-08-08 21:23:32 +00:00
Sebastián Ramírez 34378100a8 👷 Use PR Push commit identity (#2409) 2026-08-08 21:23:30 +00:00
latest-changes[bot] e5cdc73405 📝 Update release notes
[skip ci]
2026-08-08 16:07:42 +00:00
Sebastián Ramírez 96c1147ee7 🔒 Replace pre-commit PAT with PR Push (#2407) 2026-08-08 18:07:39 +02:00
98 changed files with 2991 additions and 2434 deletions
-1
View File
@@ -1 +0,0 @@
{{ _copier_answers|to_json -}}
-26
View File
@@ -1,26 +0,0 @@
from pathlib import Path
import json
# Update the .env file with the answers from the .copier-answers.yml file
# without using Jinja2 templates in the .env file, this way the code works as is
# without needing Copier, but if Copier is used, the .env file will be updated
root_path = Path(__file__).parent.parent
answers_path = Path(__file__).parent / ".copier-answers.yml"
answers = json.loads(answers_path.read_text())
env_path = root_path / ".env"
env_content = env_path.read_text()
lines = []
for line in env_content.splitlines():
for key, value in answers.items():
upper_key = key.upper()
if line.startswith(f"{upper_key}="):
if " " in value:
content = f"{upper_key}={value!r}"
else:
content = f"{upper_key}={value}"
new_line = line.replace(line, content)
lines.append(new_line)
break
else:
lines.append(line)
env_path.write_text("\n".join(lines))
-1
View File
@@ -1,7 +1,6 @@
.git .git
**/__pycache__ **/__pycache__
**/.venv **/.venv
backend/app/frontend
backend/htmlcov backend/htmlcov
frontend/blob-report frontend/blob-report
frontend/dist frontend/dist
+6 -33
View File
@@ -1,45 +1,18 @@
# Domain # Enable development behavior for commands that import the app directly
# This would be set to the production domain with an env var on deployment FASTAPI_ENV=development
# used by Traefik to transmit traffic and acquire TLS certificates
DOMAIN=localhost
# To test the local Traefik config
# DOMAIN=localhost.tiangolo.com
# Public frontend URL, used by the backend to generate links in emails.
# Keep the Vite URL for local development; deployments use the application URL.
FRONTEND_HOST=http://localhost:5173
# In staging and production, set this env var to the frontend host, e.g.
# FRONTEND_HOST=https://example.com
# Environment: local, staging, production
ENVIRONMENT=local
PROJECT_NAME="Full Stack FastAPI Project" PROJECT_NAME="Full Stack FastAPI Project"
STACK_NAME=full-stack-fastapi-project
# Backend
BACKEND_CORS_ORIGINS="http://localhost,https://localhost,http://localhost.tiangolo.com"
SECRET_KEY=changethis SECRET_KEY=changethis
FIRST_SUPERUSER=admin@example.com FIRST_SUPERUSER=admin@example.com
FIRST_SUPERUSER_PASSWORD=changethis FIRST_SUPERUSER_PASSWORD=changethis
# Emails # Emails
SMTP_HOST= SMTP_HOST=localhost
SMTP_USER=
SMTP_PASSWORD=
EMAILS_FROM_EMAIL=info@example.com EMAILS_FROM_EMAIL=info@example.com
SMTP_TLS=True SMTP_TLS=False
SMTP_SSL=False SMTP_PORT=1025
SMTP_PORT=587
# Postgres # Postgres
POSTGRES_SERVER=localhost
POSTGRES_PORT=5432
POSTGRES_DB=app
POSTGRES_USER=postgres
POSTGRES_PASSWORD=changethis POSTGRES_PASSWORD=changethis
DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD}@localhost:5432/app
SENTRY_DSN=
# Configure these with your own Docker registry images
DOCKER_IMAGE_BACKEND=backend
+2
View File
@@ -0,0 +1,2 @@
.env
!backend/app/frontend/
-25
View File
@@ -1,25 +0,0 @@
docs:
- all:
- changed-files:
- any-glob-to-any-file:
- '**/*.md'
- all-globs-to-all-files:
- '!frontend/**'
- '!backend/**'
- '!.github/**'
- '!scripts/**'
- '!.gitignore'
- '!.pre-commit-config.yaml'
internal:
- all:
- changed-files:
- any-glob-to-any-file:
- .github/**
- scripts/**
- .gitignore
- .pre-commit-config.yaml
- all-globs-to-all-files:
- '!./**/*.md'
- '!frontend/**'
- '!backend/**'
+12
View File
@@ -0,0 +1,12 @@
auto-labels:
docs:
- exclude: frontend/**
- exclude: backend/**
- exclude: .github/**
- exclude: scripts/**
- '**/*.md'
internal:
- .github/**
- scripts/**
- .gitignore
- .pre-commit-config.yaml
+2
View File
@@ -0,0 +1,2 @@
workflows:
- .github/workflows/pre-commit.yml
+3
View File
@@ -0,0 +1,3 @@
workflows:
- .github/workflows/bump-pre-commit-hooks.yml
- .github/workflows/prepare-release.yml
+16 -12
View File
@@ -12,6 +12,9 @@ jobs:
if: github.repository_owner == 'fastapi' if: github.repository_owner == 'fastapi'
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 10 timeout-minutes: 10
permissions:
contents: read
id-token: write
steps: steps:
- name: Dump GitHub context - name: Dump GitHub context
env: env:
@@ -19,26 +22,26 @@ jobs:
run: echo "$GITHUB_CONTEXT" run: echo "$GITHUB_CONTEXT"
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: with:
token: ${{ secrets.LATEST_CHANGES }} persist-credentials: false
persist-credentials: true
- name: Set up Python - name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
with: with:
python-version-file: ".python-version" python-version-file: ".python-version"
- name: Setup uv - name: Setup uv
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
with: with:
# Before upgrading uv version, make sure astral-sh/setup-uv knows its checksum. version: "latest-known"
# See: https://github.com/astral-sh/setup-uv/issues/851#issuecomment-4282017837
version: "0.11.18"
cache-dependency-glob: | cache-dependency-glob: |
pyproject.toml pyproject.toml
uv.lock uv.lock
- name: Bump pre-commit hooks - name: Bump pre-commit hooks
run: uv run prek auto-update --freeze --cooldown-days 7 run: uv run prek update --freeze --cooldown-days 7
- name: Get PR Submit token
id: pr-submit
uses: tiangolo/pr-submit@d802fdf59bde80bc3eb8bd3259f4cbeec63de4aa # 0.0.1
- name: Create pull request - name: Create pull request
env: env:
GH_TOKEN: ${{ secrets.LATEST_CHANGES }} GH_TOKEN: ${{ steps.pr-submit.outputs.token }}
BASE_BRANCH: ${{ github.event.repository.default_branch }} BASE_BRANCH: ${{ github.event.repository.default_branch }}
run: | run: |
set -euo pipefail set -euo pipefail
@@ -46,19 +49,20 @@ jobs:
echo "No pre-commit hook updates available" echo "No pre-commit hook updates available"
exit 0 exit 0
fi fi
git config user.name "github-actions[bot]" git config user.name "pr-submit[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com" git config user.email "pr-submit[bot]@users.noreply.github.com"
branch="bump-pre-commit-hooks" branch="bump-pre-commit-hooks"
git switch -C "$branch" git switch -C "$branch"
git add .pre-commit-config.yaml git add .pre-commit-config.yaml
git commit -m "⬆ Bump pre-commit hooks" git commit -m "⬆ Bump pre-commit hooks"
gh auth setup-git
git push --force origin "$branch" git push --force origin "$branch"
if [ -z "$(gh pr list --head "$branch" --state open --json number --jq '.[].number')" ]; then if [ -z "$(gh pr list --head "$branch" --state open --json number --jq '.[].number')" ]; then
gh pr create \ gh pr create \
--base "$BASE_BRANCH" \ --base "$BASE_BRANCH" \
--head "$branch" \ --head "$branch" \
--title "⬆ Bump pre-commit hooks" \ --title "⬆ Bump pre-commit hooks" \
--body "Bump pre-commit hook versions via \`prek auto-update --freeze --cooldown-days 7\`." \ --body "Bump pre-commit hook versions via \`prek update --freeze --cooldown-days 7\`." \
--label internal \ --label internal \
--label dependencies \ --label dependencies \
--label pre-commit --label pre-commit
@@ -0,0 +1,52 @@
name: Create Draft Release
on:
pull_request:
types:
- closed
permissions: {}
jobs:
create-draft-release:
if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'release')
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: write
pull-requests: read
steps:
- name: Dump GitHub context
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT"
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.repository.default_branch }}
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
with:
python-version-file: ".python-version"
- name: Install uv
uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
with:
version: "latest-known"
- name: Extract release details
id: release-details
run: |
set -euo pipefail
version="$(uv run python scripts/prepare_release.py current-version)"
uv run python scripts/prepare_release.py release-notes > draft-release-notes.md
echo "version=$version" >> "$GITHUB_OUTPUT"
- name: Create draft release
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ steps.release-details.outputs.version }}
run: |
set -euo pipefail
gh release create "$VERSION" \
--draft \
--title "$VERSION" \
--notes-file draft-release-notes.md \
--target "$(git rev-parse HEAD)"
@@ -0,0 +1,41 @@
name: Deploy with Docker Compose
on:
workflow_dispatch:
permissions:
contents: read
concurrency:
group: deploy-docker-compose
cancel-in-progress: false
jobs:
deploy:
# Do not deploy the template repository, only projects created from it
if: github.repository != 'fastapi/full-stack-fastapi-template'
runs-on: self-hosted
timeout-minutes: 30
env:
DOMAIN: ${{ vars.DOMAIN }}
PROJECT_NAME: ${{ vars.PROJECT_NAME }}
SECRET_KEY: ${{ secrets.SECRET_KEY }}
FIRST_SUPERUSER: ${{ vars.FIRST_SUPERUSER }}
FIRST_SUPERUSER_PASSWORD: ${{ secrets.FIRST_SUPERUSER_PASSWORD }}
SMTP_HOST: ${{ vars.SMTP_HOST }}
SMTP_USER: ${{ vars.SMTP_USER }}
SMTP_PASSWORD: ${{ secrets.SMTP_PASSWORD }}
EMAILS_FROM_EMAIL: ${{ vars.EMAILS_FROM_EMAIL }}
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
SENTRY_DSN: ${{ vars.SENTRY_DSN }}
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Build
run: docker compose -f compose.yml -f compose.deploy.yml build
- name: Prepare database
run: docker compose -f compose.yml -f compose.deploy.yml run --rm backend bash scripts/prestart.sh
- name: Start application
run: docker compose -f compose.yml -f compose.deploy.yml up -d
-39
View File
@@ -1,39 +0,0 @@
name: Deploy to Production
on:
release:
types:
- published
permissions:
contents: read
jobs:
deploy:
environment: production
# Do not deploy in the main repository, only in user projects
if: github.repository_owner != 'fastapi'
runs-on:
- self-hosted
- production
env:
ENVIRONMENT: production
DOMAIN: ${{ secrets.DOMAIN_PRODUCTION }}
FRONTEND_HOST: https://${{ secrets.DOMAIN_PRODUCTION }}
STACK_NAME: ${{ secrets.STACK_NAME_PRODUCTION }}
SECRET_KEY: ${{ secrets.SECRET_KEY }}
FIRST_SUPERUSER: ${{ secrets.FIRST_SUPERUSER }}
FIRST_SUPERUSER_PASSWORD: ${{ secrets.FIRST_SUPERUSER_PASSWORD }}
SMTP_HOST: ${{ secrets.SMTP_HOST }}
SMTP_USER: ${{ secrets.SMTP_USER }}
SMTP_PASSWORD: ${{ secrets.SMTP_PASSWORD }}
EMAILS_FROM_EMAIL: ${{ secrets.EMAILS_FROM_EMAIL }}
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- run: docker compose -f compose.yml --project-name ${{ secrets.STACK_NAME_PRODUCTION }} build
- run: docker compose -f compose.yml --project-name ${{ secrets.STACK_NAME_PRODUCTION }} up -d
-39
View File
@@ -1,39 +0,0 @@
name: Deploy to Staging
on:
push:
branches:
- master
permissions:
contents: read
jobs:
deploy:
environment: staging
# Do not deploy in the main repository, only in user projects
if: github.repository_owner != 'fastapi'
runs-on:
- self-hosted
- staging
env:
ENVIRONMENT: staging
DOMAIN: ${{ secrets.DOMAIN_STAGING }}
FRONTEND_HOST: https://${{ secrets.DOMAIN_STAGING }}
STACK_NAME: ${{ secrets.STACK_NAME_STAGING }}
SECRET_KEY: ${{ secrets.SECRET_KEY }}
FIRST_SUPERUSER: ${{ secrets.FIRST_SUPERUSER }}
FIRST_SUPERUSER_PASSWORD: ${{ secrets.FIRST_SUPERUSER_PASSWORD }}
SMTP_HOST: ${{ secrets.SMTP_HOST }}
SMTP_USER: ${{ secrets.SMTP_USER }}
SMTP_PASSWORD: ${{ secrets.SMTP_PASSWORD }}
EMAILS_FROM_EMAIL: ${{ secrets.EMAILS_FROM_EMAIL }}
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- run: docker compose -f compose.yml --project-name ${{ secrets.STACK_NAME_STAGING }} build
- run: docker compose -f compose.yml --project-name ${{ secrets.STACK_NAME_STAGING }} up -d
+54
View File
@@ -0,0 +1,54 @@
name: Deploy
on:
push:
branches:
- master
paths-ignore:
- release-notes.md
workflow_dispatch:
permissions:
contents: read
jobs:
deploy:
# Do not deploy the template repository, only projects created from it
if: github.repository != 'fastapi/full-stack-fastapi-template'
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
- name: Set up Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: 1.3.12
- name: Install frontend dependencies
run: bun ci
- name: Build frontend
run: bun run --filter frontend build
env:
# Override frontend/.env so the deployed frontend uses the same origin as the API
VITE_API_URL: ""
- name: Set up uv
uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
with:
version: "latest-known"
- name: Prepare database
run: uv run bash scripts/prestart.sh
working-directory: backend
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
PROJECT_NAME: ${{ vars.PROJECT_NAME }}
SECRET_KEY: ${{ secrets.SECRET_KEY }}
FIRST_SUPERUSER: ${{ vars.FIRST_SUPERUSER }}
FIRST_SUPERUSER_PASSWORD: ${{ secrets.FIRST_SUPERUSER_PASSWORD }}
- name: Deploy to FastAPI Cloud
run: uv run fastapi deploy
env:
FASTAPI_CLOUD_TOKEN: ${{ secrets.FASTAPI_CLOUD_TOKEN }}
FASTAPI_CLOUD_APP_ID: ${{ secrets.FASTAPI_CLOUD_APP_ID }}
-37
View File
@@ -1,37 +0,0 @@
name: Labels
on:
pull_request_target: # zizmor: ignore[dangerous-triggers]
types:
- opened
- synchronize
- reopened
# For label-checker
- labeled
- unlabeled
permissions: {}
jobs:
labeler:
permissions:
contents: read
pull-requests: write
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/labeler@bf12e9b00b37c5c0ca2b87b79b2daf7891dbda13 # v7.0.0
if: ${{ github.event.action != 'labeled' && github.event.action != 'unlabeled' }}
- run: echo "Done adding labels"
# Run this after labeler applied labels
check-labels:
needs:
- labeler
permissions:
pull-requests: read
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: agilepathway/label-checker@c3d16ad512e7cea5961df85ff2486bb774caf3c5 # v1.6.65
with:
one_of: breaking,security,feature,bug,refactor,upgrade,docs,lang-all,internal
repo_token: ${{ secrets.GITHUB_TOKEN }}
+9 -10
View File
@@ -28,7 +28,7 @@ jobs:
fetch-depth: 2 fetch-depth: 2
persist-credentials: false persist-credentials: false
# For pull requests it's not necessary to checkout the code but for the main branch it is # For pull requests it's not necessary to checkout the code but for the main branch it is
- uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2 - uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3
id: filter id: filter
with: with:
filters: | filters: |
@@ -57,7 +57,7 @@ jobs:
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with: with:
bun-version: 1.3.12 bun-version: 1.3.12
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
with: with:
python-version-file: .python-version python-version-file: .python-version
- name: Setup tmate session - name: Setup tmate session
@@ -66,11 +66,9 @@ jobs:
with: with:
limit-access-to-actor: true limit-access-to-actor: true
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
with: with:
# Before upgrading uv version, make sure astral-sh/setup-uv knows its checksum. version: "latest-known"
# See: https://github.com/astral-sh/setup-uv/issues/851#issuecomment-4282017837
version: "0.11.18"
- run: uv sync - run: uv sync
working-directory: backend working-directory: backend
- run: bun ci - run: bun ci
@@ -78,12 +76,13 @@ jobs:
- run: bash scripts/generate-client.sh - run: bash scripts/generate-client.sh
- run: docker compose build - run: docker compose build
- run: docker compose down -v --remove-orphans - run: docker compose down -v --remove-orphans
- run: docker compose run --rm backend bash scripts/prestart.sh
- name: Run Playwright tests - name: Run Playwright tests
run: docker compose run --rm playwright bunx playwright test --fail-on-flaky-tests --trace=retain-on-failure --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} run: docker compose run --rm playwright bunx playwright test --fail-on-flaky-tests --trace=retain-on-failure --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
- run: docker compose down -v --remove-orphans - run: docker compose down -v --remove-orphans
- name: Upload blob report to GitHub Actions Artifacts - name: Upload blob report to GitHub Actions Artifacts
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with: with:
name: blob-report-${{ matrix.shardIndex }} name: blob-report-${{ matrix.shardIndex }}
path: frontend/blob-report path: frontend/blob-report
@@ -108,7 +107,7 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: bun ci run: bun ci
- name: Download blob reports from GitHub Actions Artifacts - name: Download blob reports from GitHub Actions Artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with: with:
path: frontend/all-blob-reports path: frontend/all-blob-reports
pattern: blob-report-* pattern: blob-report-*
@@ -117,7 +116,7 @@ jobs:
run: bunx playwright merge-reports --reporter html ./all-blob-reports run: bunx playwright merge-reports --reporter html ./all-blob-reports
working-directory: frontend working-directory: frontend
- name: Upload HTML report - name: Upload HTML report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with: with:
name: html-report--attempt-${{ github.run_attempt }} name: html-report--attempt-${{ github.run_attempt }}
path: frontend/playwright-report path: frontend/playwright-report
@@ -133,7 +132,7 @@ jobs:
timeout-minutes: 5 timeout-minutes: 5
steps: steps:
- name: Decide whether the needed jobs succeeded or failed - name: Decide whether the needed jobs succeeded or failed
uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # v1.2.2 uses: re-actors/alls-green@b5b5b37504aa4183270bd3d855c52a67f212be35 # v1.3.0
with: with:
jobs: ${{ toJSON(needs) }} jobs: ${{ toJSON(needs) }}
allowed-skips: test-playwright allowed-skips: test-playwright
+33 -21
View File
@@ -2,16 +2,17 @@ name: pre-commit
on: on:
pull_request: pull_request:
permissions: permissions: {}
contents: read
env: env:
# Forks and Dependabot don't have access to secrets CAN_PUSH: ${{ github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]' }}
HAS_SECRETS: ${{ secrets.PRE_COMMIT != '' }}
jobs: jobs:
pre-commit: pre-commit:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
timeout-minutes: 5 timeout-minutes: 5
steps: steps:
- name: Dump GitHub context - name: Dump GitHub context
@@ -20,20 +21,18 @@ jobs:
run: echo "$GITHUB_CONTEXT" run: echo "$GITHUB_CONTEXT"
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
name: Checkout PR for own repo name: Checkout PR for own repo
if: env.HAS_SECRETS == 'true' if: env.CAN_PUSH == 'true'
with: with:
# To be able to commit it needs to fetch the head of the branch, not the # To be able to commit it needs to fetch the head of the branch, not the
# merge commit # merge commit
ref: ${{ github.head_ref }} ref: ${{ github.head_ref }}
# And it needs the full history to be able to compute diffs # And it needs the full history to be able to compute diffs
fetch-depth: 0 fetch-depth: 0
# A token other than the default GITHUB_TOKEN is needed to be able to trigger CI persist-credentials: false
token: ${{ secrets.PRE_COMMIT }} # zizmor: ignore[secrets-outside-env]
persist-credentials: true # Required for `git push` command
# pre-commit lite ci needs the default checkout configs to work # pre-commit lite ci needs the default checkout configs to work
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
name: Checkout PR for fork name: Checkout PR for fork
if: env.HAS_SECRETS == 'false' if: env.CAN_PUSH == 'false'
with: with:
# To be able to commit it needs the head branch of the PR, the remote one # To be able to commit it needs the head branch of the PR, the remote one
ref: ${{ github.event.pull_request.head.sha }} ref: ${{ github.event.pull_request.head.sha }}
@@ -43,15 +42,13 @@ jobs:
with: with:
bun-version: 1.3.12 bun-version: 1.3.12
- name: Set up Python - name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
with: with:
python-version-file: .python-version python-version-file: .python-version
- name: Setup uv - name: Setup uv
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
with: with:
# Before upgrading uv version, make sure astral-sh/setup-uv knows its checksum. version: "latest-known"
# See: https://github.com/astral-sh/setup-uv/issues/851#issuecomment-4282017837
version: "0.11.18"
cache-dependency-glob: | cache-dependency-glob: |
requirements**.txt requirements**.txt
pyproject.toml pyproject.toml
@@ -64,11 +61,26 @@ jobs:
id: precommit id: precommit
run: uv run prek run --from-ref origin/${GITHUB_BASE_REF} --to-ref HEAD --show-diff-on-failure run: uv run prek run --from-ref origin/${GITHUB_BASE_REF} --to-ref HEAD --show-diff-on-failure
continue-on-error: true continue-on-error: true
- name: Commit and push changes - name: Check for changes
if: env.HAS_SECRETS == 'true' id: changes
run: | run: |
git config user.name "github-actions[bot]" if [[ -n "$(git status --porcelain)" ]]; then
git config user.email "github-actions[bot]@users.noreply.github.com" echo "changed=true" >> "$GITHUB_OUTPUT"
else
echo "changed=false" >> "$GITHUB_OUTPUT"
fi
- name: Get PR Push token
id: pr-push
if: env.CAN_PUSH == 'true' && steps.changes.outputs.changed == 'true'
uses: tiangolo/pr-push@f336b3817f32ea9b8273a8c15f8ecb739ac38167 # 0.0.4
- name: Commit and push changes
if: env.CAN_PUSH == 'true' && steps.changes.outputs.changed == 'true'
env:
GH_TOKEN: ${{ steps.pr-push.outputs.token }}
run: |
git config user.name "pr-push[bot]"
git config user.email "pr-push[bot]@users.noreply.github.com"
gh auth setup-git
git add -A git add -A
if git diff --staged --quiet; then if git diff --staged --quiet; then
echo "No changes to commit" echo "No changes to commit"
@@ -76,8 +88,8 @@ jobs:
git commit -m "🎨 Auto format and update with pre-commit" git commit -m "🎨 Auto format and update with pre-commit"
git push git push
fi fi
- uses: pre-commit-ci/lite-action@5d6cc0eb514c891a40562a58a8e71576c5c7fb43 # v1.1.0 - uses: pre-commit-ci/lite-action@062bca0919bc9d6e66755cc05074b70c77e111fc # v1.2.0
if: env.HAS_SECRETS == 'false' if: env.CAN_PUSH == 'false'
with: with:
msg: 🎨 Auto format and update with pre-commit msg: 🎨 Auto format and update with pre-commit
- name: Error out on pre-commit errors - name: Error out on pre-commit errors
@@ -97,6 +109,6 @@ jobs:
GITHUB_CONTEXT: ${{ toJson(github) }} GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT" run: echo "$GITHUB_CONTEXT"
- name: Decide whether the needed jobs succeeded or failed - name: Decide whether the needed jobs succeeded or failed
uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # v1.2.2 uses: re-actors/alls-green@b5b5b37504aa4183270bd3d855c52a67f212be35 # v1.3.0
with: with:
jobs: ${{ toJSON(needs) }} jobs: ${{ toJSON(needs) }}
+82
View File
@@ -0,0 +1,82 @@
name: Prepare Release
on:
workflow_dispatch:
inputs:
bump:
description: Release bump
required: true
type: choice
options:
- patch
- minor
- major
date:
description: Release date in YYYY-MM-DD format. Defaults to today.
required: false
type: string
permissions: {}
jobs:
prepare-release:
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
id-token: write
steps:
- name: Dump GitHub context
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT"
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
with:
python-version-file: ".python-version"
- name: Install uv
uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
with:
version: "latest-known"
- name: Prepare release
env:
BUMP: ${{ inputs.bump }}
RELEASE_DATE: ${{ inputs.date }}
run: uv run python scripts/prepare_release.py prepare "$BUMP" --date "$RELEASE_DATE"
- name: Get release version
id: release-version
run: |
version="$(uv run python scripts/prepare_release.py current-version)"
echo "$version"
echo "version=$version" >> "$GITHUB_OUTPUT"
- name: Get PR Submit token
id: pr-submit
uses: tiangolo/pr-submit@d802fdf59bde80bc3eb8bd3259f4cbeec63de4aa # 0.0.1
- name: Create release pull request
env:
BASE_BRANCH: ${{ github.event.repository.default_branch }}
GH_TOKEN: ${{ steps.pr-submit.outputs.token }}
VERSION: ${{ steps.release-version.outputs.version }}
run: |
set -euo pipefail
branch="release-${VERSION}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
git config user.name "pr-submit[bot]"
git config user.email "pr-submit[bot]@users.noreply.github.com"
git switch -c "$branch"
git add release-notes.md
git commit -m "🔖 Release version ${VERSION}"
gh auth setup-git
git push --set-upstream origin "$branch"
gh label create release \
--description "Release preparation" \
--color ededed \
--force
gh pr create \
--base "$BASE_BRANCH" \
--head "$branch" \
--title "🔖 Release version ${VERSION}" \
--body "Prepare release ${VERSION}." \
--label release
+4 -6
View File
@@ -20,20 +20,18 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: with:
persist-credentials: false persist-credentials: false
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
with: with:
python-version-file: .python-version python-version-file: .python-version
- name: Setup uv - name: Setup uv
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
with: with:
# Before upgrading uv version, make sure astral-sh/setup-uv knows its checksum. version: "latest-known"
# See: https://github.com/astral-sh/setup-uv/issues/851#issuecomment-4282017837
version: "0.11.18"
cache-dependency-glob: | cache-dependency-glob: |
pyproject.toml pyproject.toml
uv.lock uv.lock
- run: uv sync --all-packages --no-dev --group github-actions - run: uv sync --all-packages --no-dev --group github-actions
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with: with:
name: coverage-html name: coverage-html
path: backend/htmlcov path: backend/htmlcov
+5 -7
View File
@@ -18,17 +18,15 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
- name: Set up Python - name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
with: with:
python-version-file: .python-version python-version-file: .python-version
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
with: with:
# Before upgrading uv version, make sure astral-sh/setup-uv knows its checksum. version: "latest-known"
# See: https://github.com/astral-sh/setup-uv/issues/851#issuecomment-4282017837
version: "0.11.18"
- run: docker compose down -v --remove-orphans - run: docker compose down -v --remove-orphans
- run: docker compose up -d db mailcatcher - run: docker compose up -d --wait db mailpit
- name: Migrate DB - name: Migrate DB
run: uv run bash scripts/prestart.sh run: uv run bash scripts/prestart.sh
working-directory: backend working-directory: backend
@@ -37,7 +35,7 @@ jobs:
working-directory: backend working-directory: backend
- run: docker compose down -v --remove-orphans - run: docker compose down -v --remove-orphans
- name: Store coverage files - name: Store coverage files
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with: with:
name: coverage-html name: coverage-html
path: backend/htmlcov path: backend/htmlcov
@@ -20,6 +20,7 @@ jobs:
persist-credentials: false persist-credentials: false
- run: docker compose build - run: docker compose build
- run: docker compose down -v --remove-orphans - run: docker compose down -v --remove-orphans
- run: docker compose run --rm backend bash scripts/prestart.sh
- run: docker compose up -d --wait backend adminer - run: docker compose up -d --wait backend adminer
- name: Test backend is up - name: Test backend is up
run: curl http://localhost:8000/api/v1/utils/health-check run: curl http://localhost:8000/api/v1/utils/health-check
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
- name: Run zizmor - name: Run zizmor
uses: zizmorcore/zizmor-action@6fc4b006235f201fdab3722e17240ab420d580e5 # v0.6.1 uses: zizmorcore/zizmor-action@53290badfa369cb0bf7ad16342cb62c2fa47ac7b # v0.8.0
with: with:
advanced-security: ${{ github.repository_owner == 'fastapi' }} advanced-security: ${{ github.repository_owner == 'fastapi' }}
annotations: ${{ github.repository_owner != 'fastapi' }} annotations: ${{ github.repository_owner != 'fastapi' }}
+4 -4
View File
@@ -13,12 +13,12 @@ repos:
exclude: | exclude: |
(?x)^( (?x)^(
frontend/src/client/.*| frontend/src/client/.*|
backend/app/email-templates/build/.* backend/app/email-templates/.*
)$ )$
- id: trailing-whitespace - id: trailing-whitespace
exclude: ^frontend/src/client/.* exclude: ^frontend/src/client/.*
- repo: https://github.com/crate-ci/typos - repo: https://github.com/crate-ci/typos
rev: bee27e3a4fd1ea2111cf90ab89cd076c870fce14 # frozen: v1.48.0 rev: 7f9d0819ca1c008ade73f4b41fa9d258d48f605b # frozen: v1.51.1
hooks: hooks:
- id: typos - id: typos
args: [--force-exclude] args: [--force-exclude]
@@ -43,7 +43,7 @@ repos:
entry: uv run ruff format --force-exclude --exit-non-zero-on-format entry: uv run ruff format --force-exclude --exit-non-zero-on-format
require_serial: true require_serial: true
language: unsupported language: unsupported
types: [python] types_or: [python, markdown]
- id: local-mypy - id: local-mypy
name: mypy check name: mypy check
@@ -64,7 +64,7 @@ repos:
entry: bash ./scripts/generate-client.sh entry: bash ./scripts/generate-client.sh
pass_filenames: false pass_filenames: false
language: unsupported language: unsupported
files: ^backend/.*$|^scripts/generate-client\.sh$ files: ^backend/app/.*\.py$|^backend/pyproject\.toml$|^uv\.lock$|^frontend/openapi-ts\.config\.ts$|^scripts/generate-client\.sh$
- id: add-release-date - id: add-release-date
language: unsupported language: unsupported
-1
View File
@@ -7,7 +7,6 @@
"charliermarsh.ruff", "charliermarsh.ruff",
"docker.docker", "docker.docker",
"github.vscode-github-actions", "github.vscode-github-actions",
"mjmlio.vscode-mjml",
"ms-playwright.playwright", "ms-playwright.playwright",
"ms-python.python", "ms-python.python",
"tombi-toml.tombi" "tombi-toml.tombi"
+29 -175
View File
@@ -1,8 +1,7 @@
# Full Stack FastAPI Template # Full Stack FastAPI Template
<a href="https://github.com/fastapi/full-stack-fastapi-template/actions?query=workflow%3A%22Test+Docker+Compose%22" target="_blank"><img src="https://github.com/fastapi/full-stack-fastapi-template/workflows/Test%20Docker%20Compose/badge.svg" alt="Test Docker Compose"></a> [![Test Docker Compose](../../actions/workflows/test-docker-compose.yml/badge.svg)](../../actions/workflows/test-docker-compose.yml)
<a href="https://github.com/fastapi/full-stack-fastapi-template/actions?query=workflow%3A%22Test+Backend%22" target="_blank"><img src="https://github.com/fastapi/full-stack-fastapi-template/workflows/Test%20Backend/badge.svg" alt="Test Backend"></a> [![Test Backend](../../actions/workflows/test-backend.yml/badge.svg)](../../actions/workflows/test-backend.yml)
<a href="https://coverage-badge.samuelcolvin.workers.dev/redirect/fastapi/full-stack-fastapi-template" target="_blank"><img src="https://coverage-badge.samuelcolvin.workers.dev/fastapi/full-stack-fastapi-template.svg" alt="Coverage"></a>
## Technology Stack and Features ## Technology Stack and Features
@@ -11,201 +10,54 @@
- 🔍 [Pydantic](https://docs.pydantic.dev), used by FastAPI, for the data validation and settings management. - 🔍 [Pydantic](https://docs.pydantic.dev), used by FastAPI, for the data validation and settings management.
- 💾 [PostgreSQL](https://www.postgresql.org) as the SQL database. - 💾 [PostgreSQL](https://www.postgresql.org) as the SQL database.
- 🚀 [React](https://react.dev) for the frontend. - 🚀 [React](https://react.dev) for the frontend.
- 🧩 Built into the backend image and served by FastAPI on the same domain as the API. - 🧩 Built into the backend application and served by FastAPI on the same domain as the API.
- 💃 Using TypeScript, hooks, [Vite](https://vitejs.dev), and other parts of a modern frontend stack. - 💃 Using TypeScript, hooks, [Vite](https://vitejs.dev), and other parts of a modern frontend stack.
- 🎨 [Tailwind CSS](https://tailwindcss.com) and [shadcn/ui](https://ui.shadcn.com) for the frontend components. - 🎨 [Tailwind CSS](https://tailwindcss.com) and [shadcn/ui](https://ui.shadcn.com) for the frontend components.
- 🤖 An automatically generated frontend client. - 🤖 An automatically generated frontend client.
- 🧪 [Playwright](https://playwright.dev) for End-to-End testing. - 🧪 [Playwright](https://playwright.dev) for end-to-end testing.
- 🦇 Dark mode support. - 🦇 Dark mode support.
- 🐋 [Docker Compose](https://www.docker.com) for development and production. - ☁️ [FastAPI Cloud](https://fastapicloud.com) for deployment.
- 🐋 [Docker Compose](https://www.docker.com) for local services and self-hosted deployment.
- 📞 [Traefik](https://traefik.io) as a reverse proxy with automatic HTTPS.
- 🔒 Secure password hashing by default. - 🔒 Secure password hashing by default.
- 🔑 JWT (JSON Web Token) authentication. - 🔑 JWT (JSON Web Token) authentication.
- 📫 Email based password recovery. - 📫 Email-based password recovery.
- 📬 [Mailcatcher](https://mailcatcher.me) for local email testing during development. - ✉️ [React Email](https://react.email) for email templates.
- 📬 [Mailpit](https://mailpit.axllent.org) for local email testing during development.
- ✅ Tests with [Pytest](https://pytest.org). - ✅ Tests with [Pytest](https://pytest.org).
- 📞 [Traefik](https://traefik.io) as a reverse proxy / load balancer.
- 🚢 Deployment instructions using Docker Compose, including how to set up Traefik to handle automatic HTTPS certificates.
- 🏭 CI (continuous integration) and CD (continuous deployment) based on GitHub Actions. - 🏭 CI (continuous integration) and CD (continuous deployment) based on GitHub Actions.
### Dashboard Login ### Dashboard Login
[![Dashboard login screenshot](img/login.png)](https://github.com/fastapi/full-stack-fastapi-template) ![Dashboard login screenshot](img/login.png)
### Dashboard - Admin ### Dashboard - Admin
[![Admin dashboard screenshot](img/dashboard.png)](https://github.com/fastapi/full-stack-fastapi-template) ![Admin dashboard screenshot](img/dashboard.png)
### Dashboard - Items ### Dashboard - Items
[![Items dashboard screenshot](img/dashboard-items.png)](https://github.com/fastapi/full-stack-fastapi-template) ![Items dashboard screenshot](img/dashboard-items.png)
### Dashboard - Dark Mode ### Dashboard - Dark Mode
[![Dark mode dashboard screenshot](img/dashboard-dark.png)](https://github.com/fastapi/full-stack-fastapi-template) ![Dark mode dashboard screenshot](img/dashboard-dark.png)
### React Email Templates
![Email templates screenshot](img/react-email.png)
### Mailpit - Local Email Testing
![Mailpit screenshot](img/mailpit.png)
### Interactive API Documentation ### Interactive API Documentation
[![API docs](img/docs.png)](https://github.com/fastapi/full-stack-fastapi-template) ![API docs](img/docs.png)
## How To Use It ## How to Use It
You can **just fork or clone** this repository and use it as is. Click the **Use this template** button at the top of this page to create a new repository.
✨ It just works. ✨
### How to Use a Private Repository
If you want to have a private repository, GitHub won't allow you to simply fork it as it doesn't allow changing the visibility of forks.
But you can do the following:
- Create a new GitHub repo, for example `my-full-stack`.
- Clone this repository manually, set the name with the name of the project you want to use, for example `my-full-stack`:
```bash
git clone git@github.com:fastapi/full-stack-fastapi-template.git my-full-stack
```
- Enter into the new directory:
```bash
cd my-full-stack
```
- Set the new origin to your new repository, copy it from the GitHub interface, for example:
```bash
git remote set-url origin git@github.com:octocat/my-full-stack.git
```
- Add this repo as another "remote" to allow you to get updates later:
```bash
git remote add upstream git@github.com:fastapi/full-stack-fastapi-template.git
```
- Push the code to your new repository:
```bash
git push -u origin master
```
### Update From the Original Template
After cloning the repository, and after doing changes, you might want to get the latest changes from this original template.
- Make sure you added the original repository as a remote, you can check it with:
```bash
git remote -v
origin git@github.com:octocat/my-full-stack.git (fetch)
origin git@github.com:octocat/my-full-stack.git (push)
upstream git@github.com:fastapi/full-stack-fastapi-template.git (fetch)
upstream git@github.com:fastapi/full-stack-fastapi-template.git (push)
```
- Pull the latest changes without merging:
```bash
git pull --no-commit upstream master
```
This will download the latest changes from this template without committing them, that way you can check everything is right before committing.
- If there are conflicts, solve them in your editor.
- Once you are done, commit the changes:
```bash
git merge --continue
```
### Configure
You can then update configs in the `.env` files to customize your configurations.
Before deploying it, make sure you change at least the values for:
- `SECRET_KEY`
- `FIRST_SUPERUSER_PASSWORD`
- `POSTGRES_PASSWORD`
You can (and should) pass these as environment variables from secrets.
Read the [deployment.md](./deployment.md) docs for more details.
### Generate Secret Keys
Some environment variables in the `.env` file have a default value of `changethis`.
You have to change them with a secret key, to generate secret keys you can run the following command:
```bash
python -c "import secrets; print(secrets.token_urlsafe(32))"
```
Copy the content and use that as password / secret key. And run that again to generate another secure key.
## How To Use It - Alternative With Copier
This repository also supports generating a new project using [Copier](https://copier.readthedocs.io).
It will copy all the files, ask you configuration questions, and update the `.env` files with your answers.
### Install Copier
You can install Copier with:
```bash
pip install copier
```
Or better, if you have [`pipx`](https://pipx.pypa.io/), you can run it with:
```bash
pipx install copier
```
**Note**: If you have `pipx`, installing copier is optional, you could run it directly.
### Generate a Project With Copier
Decide a name for your new project's directory, you will use it below. For example, `my-awesome-project`.
Go to the directory that will be the parent of your project, and run the command with your project's name:
```bash
copier copy https://github.com/fastapi/full-stack-fastapi-template my-awesome-project --trust
```
If you have `pipx` and you didn't install `copier`, you can run it directly:
```bash
pipx run copier copy https://github.com/fastapi/full-stack-fastapi-template my-awesome-project --trust
```
**Note** the `--trust` option is necessary to be able to execute a [post-creation script](https://github.com/fastapi/full-stack-fastapi-template/blob/master/.copier/update_dotenv.py) that updates your `.env` files.
### Input Variables
Copier will ask you for some data, you might want to have at hand before generating the project.
But don't worry, you can just update any of that in the `.env` files afterwards.
The input variables, with their default values (some auto generated) are:
- `project_name`: (default: `"FastAPI Project"`) The name of the project, shown to API users (in .env).
- `stack_name`: (default: `"fastapi-project"`) The name of the stack used for Docker Compose labels and project name (no spaces, no periods) (in .env).
- `secret_key`: (default: `"changethis"`) The secret key for the project, used for security, stored in .env, you can generate one with the method above.
- `first_superuser`: (default: `"admin@example.com"`) The email of the first superuser (in .env).
- `first_superuser_password`: (default: `"changethis"`) The password of the first superuser (in .env).
- `smtp_host`: (default: "") The SMTP server host to send emails, you can set it later in .env.
- `smtp_user`: (default: "") The SMTP server user to send emails, you can set it later in .env.
- `smtp_password`: (default: "") The SMTP server password to send emails, you can set it later in .env.
- `emails_from_email`: (default: `"info@example.com"`) The email account to send emails from, you can set it later in .env.
- `postgres_password`: (default: `"changethis"`) The password for the PostgreSQL database, stored in .env, you can generate one with the method above.
- `sentry_dsn`: (default: "") The DSN for Sentry, if you are using it, you can set it later in .env.
## Backend Development ## Backend Development
@@ -217,13 +69,15 @@ Frontend docs: [frontend/README.md](./frontend/README.md).
## Deployment ## Deployment
Deployment docs: [deployment.md](./deployment.md). FastAPI Cloud deployment: [deployment.md](./deployment.md).
Self-hosted deployment with Docker Compose: [deployment-docker-compose.md](./deployment-docker-compose.md).
## Development ## Development
General development docs: [development.md](./development.md). General development docs: [development.md](./development.md).
This includes using Docker Compose, custom local domains, `.env` configurations, etc. This includes the local FastAPI and Vite workflow, Docker Compose services, `.env` configuration, and more.
## Release Notes ## Release Notes
+47 -74
View File
@@ -5,27 +5,29 @@
* [Docker](https://www.docker.com/). * [Docker](https://www.docker.com/).
* [uv](https://docs.astral.sh/uv/) for Python package and environment management. * [uv](https://docs.astral.sh/uv/) for Python package and environment management.
## Docker Compose ## Local Development
Start the local development environment with Docker Compose following the guide in [../development.md](../development.md). Run the backend locally and connect it to PostgreSQL in Docker Compose.
## General Workflow From the project root, start PostgreSQL and Mailpit:
By default, the dependencies are managed with [uv](https://docs.astral.sh/uv/), go there and install it. ```console
$ docker compose up -d db mailpit
```
From `./backend/` you can install all the dependencies with: Then, from `./backend/`, install the dependencies, prepare the database, and start the development server:
```console ```console
$ uv sync $ uv sync
$ uv run bash scripts/prestart.sh
$ uv run fastapi dev
``` ```
Then you can activate the virtual environment with: The API is available at `http://localhost:8000`, with automatic interactive docs at `http://localhost:8000/docs`.
```console ## General Workflow
$ source .venv/bin/activate
```
Make sure your editor is using the correct Python virtual environment, with the interpreter at `backend/.venv/bin/python`. Run backend commands from `./backend/` with `uv run`. Make sure your editor uses the Python interpreter at `.venv/bin/python` in the project root.
Modify or add SQLModel models for data and SQL tables in `./backend/app/models.py`, API endpoints in `./backend/app/api/`, CRUD (Create, Read, Update, Delete) utils in `./backend/app/crud.py`. Modify or add SQLModel models for data and SQL tables in `./backend/app/models.py`, API endpoints in `./backend/app/api/`, CRUD (Create, Read, Update, Delete) utils in `./backend/app/crud.py`.
@@ -35,73 +37,40 @@ There are already configurations in place to run the backend through the VS Code
The setup is also already configured so you can run the tests through the VS Code Python tests tab. The setup is also already configured so you can run the tests through the VS Code Python tests tab.
## Docker Compose Override ## Full Stack with Docker Compose
During development, you can change Docker Compose settings that will only affect the local development environment in the file `compose.override.yml`. To run the backend and built frontend in Docker Compose:
The changes to that file only affect the local development environment, not the production environment. So, you can add "temporary" changes that help the development workflow.
For example, the directory with the backend code is synchronized in the Docker container, copying the code you change live to the directory inside the container. That allows you to test your changes right away, without having to build the Docker image again. It should only be done during development, for production, you should build the Docker image with a recent version of the backend code. But during development, it allows you to iterate very fast.
There is also a command override that runs `fastapi run --reload` instead of the default `fastapi run`. It starts a single server process (instead of multiple, as would be for production) and reloads the process whenever the code changes. Have in mind that if you have a syntax error and save the Python file, it will break and exit, and the container will stop. After that, you can restart the container by fixing the error and running again:
```console ```console
$ docker compose run --rm backend bash scripts/prestart.sh
$ docker compose watch $ docker compose watch
``` ```
There is also a commented out `command` override, you can uncomment it and comment the default one. It makes the backend container run a process that does "nothing", but keeps the container alive. That allows you to get inside your running container and execute commands inside, for example a Python interpreter to test installed dependencies, or start the development server that reloads when it detects changes. The application is available at `http://localhost:8000`.
To get inside the container with a `bash` session you can start the stack with: ### Docker Compose Override
```console The `compose.override.yml` file contains local settings for published ports, source synchronization, automatic image rebuilds, and backend reloads. Docker Compose applies it automatically when you run `docker compose` without an explicit file list.
$ docker compose watch
```
and then in another terminal, `exec` inside the running container: To open a shell in the backend container:
```console ```console
$ docker compose exec backend bash $ docker compose exec backend bash
``` ```
You should see an output like: ## Backend Tests
To test the backend from the `backend` directory, run:
```console ```console
root@7f2607af31c3:/app# $ uv run bash scripts/test.sh
``` ```
that means that you are in a `bash` session inside your container, as a `root` user, under the `/app` directory, this directory has another directory called "app" inside, that's where your code lives inside the container: `/app/app`. The tests run with Pytest. Modify existing tests or add new ones in `./backend/tests/`.
There you can use the `fastapi run --reload` command to run the debug live reloading server. If you use GitHub Actions, the tests will run automatically.
```console ### Test a Running Stack
$ fastapi run --reload app/main.py
```
...it will look like:
```console
root@7f2607af31c3:/app# fastapi run --reload app/main.py
```
and then hit enter. That runs the live reloading server that auto reloads when it detects code changes.
Nevertheless, if it doesn't detect a change but a syntax error, it will just stop with an error. But as the container is still alive and you are in a Bash session, you can quickly restart it after fixing the error, running the same command ("up arrow" and "Enter").
...this previous detail is what makes it useful to have the container alive doing nothing and then, in a Bash session, make it run the live reload server.
## Backend tests
To test the backend run:
```console
$ bash ./scripts/test.sh
```
The tests run with Pytest, modify and add tests to `./backend/tests/`.
If you use GitHub Actions the tests will run automatically.
### Test running stack
If your stack is already up and you just want to run the tests, you can use: If your stack is already up and you just want to run the tests, you can use:
@@ -109,7 +78,7 @@ If your stack is already up and you just want to run the tests, you can use:
docker compose exec backend bash scripts/tests-start.sh docker compose exec backend bash scripts/tests-start.sh
``` ```
That `/app/scripts/tests-start.sh` script just calls `pytest` after making sure that the rest of the stack is running. If you need to pass extra arguments to `pytest`, you can pass them to that command and they will be forwarded. The `/app/backend/scripts/tests-start.sh` script calls `pytest`. If you need to pass extra arguments to `pytest`, you can pass them to that command and they will be forwarded.
For example, to stop on first error: For example, to stop on first error:
@@ -119,26 +88,18 @@ docker compose exec backend bash scripts/tests-start.sh -x
### Test Coverage ### Test Coverage
When the tests are run, a file `htmlcov/index.html` is generated, you can open it in your browser to see the coverage of the tests. When the tests run, they generate `htmlcov/index.html`. Open it in your browser to inspect the test coverage.
## Migrations ## Migrations
As during local development your app directory is mounted as a volume inside the container, you can also run the migrations with `alembic` commands inside the container and the migration code will be in your app directory (instead of being only inside the container). So you can add it to your git repository. Make sure you create a revision of your models and upgrade the database with that revision every time you change them. From the `backend` directory, use `uv` to run Alembic against the PostgreSQL container:
Make sure you create a "revision" of your models and that you "upgrade" your database with that revision every time you change them. As this is what will update the tables in your database. Otherwise, your application will have errors.
* Start an interactive session in the backend container:
```console
$ docker compose exec backend bash
```
* Alembic is already configured to import your SQLModel models from `./backend/app/models.py`. * Alembic is already configured to import your SQLModel models from `./backend/app/models.py`.
* After changing a model (for example, adding a column), inside the container, create a revision, e.g.: * After changing a model (for example, adding a column), create a revision:
```console ```console
$ alembic revision --autogenerate -m "Add column last_name to User model" $ uv run alembic revision --autogenerate -m "Add column last_name to User model"
``` ```
* Commit to the git repository the files generated in the alembic directory. * Commit to the git repository the files generated in the alembic directory.
@@ -146,7 +107,7 @@ $ alembic revision --autogenerate -m "Add column last_name to User model"
* After creating the revision, run the migration in the database (this is what will actually change the database): * After creating the revision, run the migration in the database (this is what will actually change the database):
```console ```console
$ alembic upgrade head $ uv run alembic upgrade head
``` ```
If you don't want to use migrations at all, uncomment the lines in the file at `./backend/app/core/db.py` that end in: If you don't want to use migrations at all, uncomment the lines in the file at `./backend/app/core/db.py` that end in:
@@ -165,8 +126,20 @@ If you don't want to start with the default models and want to remove them / mod
## Email Templates ## Email Templates
The email templates are in `./backend/app/email-templates/`. Here, there are two directories: `build` and `src`. The `src` directory contains the source files that are used to build the final email templates. The `build` directory contains the final email templates that are used by the application. The email templates are written with [React Email](https://react.email) in `./packages/react-email/`. The `emails` directory holds one component per email and the `ui` directory holds the shared components (layout, heading, button, link, callout).
Before continuing, ensure you have the [MJML extension](https://github.com/mjmlio/vscode-mjml) installed in your VS Code. The rendered HTML in `./backend/app/email-templates/` is generated from those components. It is what the application sends and should not be edited by hand.
Once you have the MJML extension installed, you can create a new email template in the `src` directory. After creating the new email template and with the `.mjml` file open in your editor, open the command palette with `Ctrl+Shift+P` and search for `MJML: Export to HTML`. This will convert the `.mjml` file to a `.html` file and now you can save it in the build directory. To preview the emails while editing them, start the dev server from the root of the project:
```console
$ bun run email:dev
```
Values coming from the backend are declared as Jinja placeholders in the component props, for example `username = "{{ username }}"`. The context for each email is built in `generate_*_email()` in `./backend/app/utils.py`, so a new placeholder needs to be added there too.
Once you are done, regenerate the templates used by the application:
```console
$ bun run email:export
```
+1 -1
View File
@@ -31,7 +31,7 @@ target_metadata = SQLModel.metadata
def get_url(): def get_url():
return str(settings.SQLALCHEMY_DATABASE_URI) return str(settings.DATABASE_URL)
def run_migrations_offline(): def run_migrations_offline():
+1 -1
View File
@@ -10,5 +10,5 @@ api_router.include_router(utils.router)
api_router.include_router(items.router) api_router.include_router(items.router)
if settings.ENVIRONMENT == "local": if settings.FASTAPI_ENV == "development":
api_router.include_router(private.router) api_router.include_router(private.router)
-39
View File
@@ -1,39 +0,0 @@
import logging
from sqlalchemy import Engine
from sqlmodel import Session, select
from tenacity import after_log, before_log, retry, stop_after_attempt, wait_fixed
from app.core.db import engine
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
max_tries = 60 * 5 # 5 minutes
wait_seconds = 1
@retry(
stop=stop_after_attempt(max_tries),
wait=wait_fixed(wait_seconds),
before=before_log(logger, logging.INFO),
after=after_log(logger, logging.WARN),
)
def init(db_engine: Engine) -> None:
try:
with Session(db_engine) as session:
# Try to create session to check if DB is awake
session.exec(select(1))
except Exception as e:
logger.error(e)
raise e
def main() -> None:
logger.info("Initializing service")
init(engine)
logger.info("Service finished initializing")
if __name__ == "__main__":
main()
+17 -44
View File
@@ -1,27 +1,17 @@
import secrets
import warnings import warnings
from typing import Annotated, Any, Literal, Self from typing import Literal, Self
from pydantic import ( from pydantic import (
AnyUrl,
BeforeValidator,
EmailStr, EmailStr,
HttpUrl, HttpUrl,
PostgresDsn, PostgresDsn,
computed_field, computed_field,
field_validator,
model_validator, model_validator,
) )
from pydantic_settings import BaseSettings, SettingsConfigDict from pydantic_settings import BaseSettings, SettingsConfigDict
def parse_cors(v: Any) -> list[str] | str:
if isinstance(v, str) and not v.startswith("["):
return [i.strip() for i in v.split(",") if i.strip()]
elif isinstance(v, list | str):
return v
raise ValueError(v)
class Settings(BaseSettings): class Settings(BaseSettings):
model_config = SettingsConfigDict( model_config = SettingsConfigDict(
# Use top level .env file (one level above ./backend/) # Use top level .env file (one level above ./backend/)
@@ -30,42 +20,24 @@ class Settings(BaseSettings):
extra="ignore", extra="ignore",
) )
API_V1_STR: str = "/api/v1" API_V1_STR: str = "/api/v1"
SECRET_KEY: str = secrets.token_urlsafe(32) SECRET_KEY: str
# 60 minutes * 24 hours * 8 days = 8 days # 60 minutes * 24 hours * 8 days = 8 days
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8 ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8
FRONTEND_HOST: str = "http://localhost:5173" FRONTEND_HOST: str = "http://localhost:5173"
ENVIRONMENT: Literal["local", "staging", "production"] = "local" FASTAPI_ENV: Literal["development"] | None = None
BACKEND_CORS_ORIGINS: Annotated[
list[AnyUrl] | str, BeforeValidator(parse_cors)
] = []
@computed_field # type: ignore[prop-decorator]
@property
def all_cors_origins(self) -> list[str]:
return [str(origin).rstrip("/") for origin in self.BACKEND_CORS_ORIGINS] + [
self.FRONTEND_HOST
]
PROJECT_NAME: str PROJECT_NAME: str
SENTRY_DSN: HttpUrl | None = None SENTRY_DSN: HttpUrl | None = None
POSTGRES_SERVER: str DATABASE_URL: PostgresDsn
POSTGRES_PORT: int = 5432
POSTGRES_USER: str
POSTGRES_PASSWORD: str = ""
POSTGRES_DB: str = ""
@computed_field # type: ignore[prop-decorator] @field_validator("DATABASE_URL", mode="before")
@property @classmethod
def SQLALCHEMY_DATABASE_URI(self) -> PostgresDsn: def _use_psycopg_driver(cls, value: str | PostgresDsn) -> str:
return PostgresDsn.build( database_url = str(value)
scheme="postgresql+psycopg", for scheme in ("postgres://", "postgresql://"):
username=self.POSTGRES_USER, if database_url.startswith(scheme):
password=self.POSTGRES_PASSWORD, return database_url.replace(scheme, "postgresql+psycopg://", 1)
host=self.POSTGRES_SERVER, return database_url
port=self.POSTGRES_PORT,
path=self.POSTGRES_DB,
)
SMTP_TLS: bool = True SMTP_TLS: bool = True
SMTP_SSL: bool = False SMTP_SSL: bool = False
@@ -99,7 +71,7 @@ class Settings(BaseSettings):
f'The value of {var_name} is "changethis", ' f'The value of {var_name} is "changethis", '
"for security, please change it, at least for deployments." "for security, please change it, at least for deployments."
) )
if self.ENVIRONMENT == "local": if self.FASTAPI_ENV == "development":
warnings.warn(message, stacklevel=1) warnings.warn(message, stacklevel=1)
else: else:
raise ValueError(message) raise ValueError(message)
@@ -107,7 +79,8 @@ class Settings(BaseSettings):
@model_validator(mode="after") @model_validator(mode="after")
def _enforce_non_default_secrets(self) -> Self: def _enforce_non_default_secrets(self) -> Self:
self._check_default_secret("SECRET_KEY", self.SECRET_KEY) self._check_default_secret("SECRET_KEY", self.SECRET_KEY)
self._check_default_secret("POSTGRES_PASSWORD", self.POSTGRES_PASSWORD) for host in self.DATABASE_URL.hosts():
self._check_default_secret("DATABASE_URL password", host["password"])
self._check_default_secret( self._check_default_secret(
"FIRST_SUPERUSER_PASSWORD", self.FIRST_SUPERUSER_PASSWORD "FIRST_SUPERUSER_PASSWORD", self.FIRST_SUPERUSER_PASSWORD
) )
@@ -115,4 +88,4 @@ class Settings(BaseSettings):
return self return self
settings = Settings() # type: ignore settings = Settings() # type: ignore # ty: ignore[unused-ignore-comment]
+1 -1
View File
@@ -4,7 +4,7 @@ from app import crud
from app.core.config import settings from app.core.config import settings
from app.models import User, UserCreate from app.models import User, UserCreate
engine = create_engine(str(settings.SQLALCHEMY_DATABASE_URI)) engine = create_engine(str(settings.DATABASE_URL), pool_pre_ping=True)
# make sure all SQLModel models are imported (app.models) before initializing DB # make sure all SQLModel models are imported (app.models) before initializing DB
@@ -1,25 +0,0 @@
<!doctype html><html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office"><head><title></title><!--[if !mso]><!-- --><meta http-equiv="X-UA-Compatible" content="IE=edge"><!--<![endif]--><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><style type="text/css">#outlook a { padding:0; }
.ReadMsgBody { width:100%; }
.ExternalClass { width:100%; }
.ExternalClass * { line-height:100%; }
body { margin:0;padding:0;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%; }
table, td { border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt; }
img { border:0;height:auto;line-height:100%; outline:none;text-decoration:none;-ms-interpolation-mode:bicubic; }
p { display:block;margin:13px 0; }</style><!--[if !mso]><!--><style type="text/css">@media only screen and (max-width:480px) {
@-ms-viewport { width:320px; }
@viewport { width:320px; }
}</style><!--<![endif]--><!--[if mso]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]--><!--[if lte mso 11]>
<style type="text/css">
.outlook-group-fix { width:100% !important; }
</style>
<![endif]--><!--[if !mso]><!--><link href="https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700" rel="stylesheet" type="text/css"><style type="text/css">@import url(https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700);</style><!--<![endif]--><style type="text/css">@media only screen and (min-width:480px) {
.mj-column-per-100 { width:100% !important; max-width: 100%; }
}</style><style type="text/css"></style></head><body style="background-color:#fafbfc;"><div style="background-color:#fafbfc;"><!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]--><div style="background:#ffffff;background-color:#ffffff;Margin:0px auto;max-width:600px;"><table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#ffffff;background-color:#ffffff;width:100%;"><tbody><tr><td style="direction:ltr;font-size:0px;padding:40px 20px;text-align:center;vertical-align:top;"><!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:middle;width:560px;" ><![endif]--><div class="mj-column-per-100 outlook-group-fix" style="font-size:13px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:100%;"><table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:middle;" width="100%"><tr><td align="center" style="font-size:0px;padding:35px;word-break:break-word;"><div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:20px;line-height:1;text-align:center;color:#333333;">{{ project_name }} - New Account</div></td></tr><tr><td align="center" style="font-size:0px;padding:10px 25px;padding-right:25px;padding-left:25px;word-break:break-word;"><div style="font-family:Arial, Helvetica, sans-serif;font-size:16px;line-height:1;text-align:center;color:#555555;"><span>Welcome to your new account!</span></div></td></tr><tr><td align="center" style="font-size:0px;padding:10px 25px;padding-right:25px;padding-left:25px;word-break:break-word;"><div style="font-family:Arial, Helvetica, sans-serif;font-size:16px;line-height:1;text-align:center;color:#555555;">Here are your account details:</div></td></tr><tr><td align="center" style="font-size:0px;padding:10px 25px;padding-right:25px;padding-left:25px;word-break:break-word;"><div style="font-family:Arial, Helvetica, sans-serif;font-size:16px;line-height:1;text-align:center;color:#555555;">Username: {{ username }}</div></td></tr><tr><td align="center" style="font-size:0px;padding:10px 25px;padding-right:25px;padding-left:25px;word-break:break-word;"><div style="font-family:Arial, Helvetica, sans-serif;font-size:16px;line-height:1;text-align:center;color:#555555;">Password: {{ password }}</div></td></tr><tr><td align="center" vertical-align="middle" style="font-size:0px;padding:15px 30px;word-break:break-word;"><table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;"><tr><td align="center" bgcolor="#009688" role="presentation" style="border:none;border-radius:8px;cursor:auto;padding:10px 25px;background:#009688;" valign="middle"><a href="{{ link }}" style="background:#009688;color:#ffffff;font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:18px;font-weight:normal;line-height:120%;Margin:0;text-decoration:none;text-transform:none;" target="_blank">Go to Dashboard</a></td></tr></table></td></tr><tr><td style="font-size:0px;padding:10px 25px;word-break:break-word;"><p style="border-top:solid 2px #cccccc;font-size:1;margin:0px auto;width:100%;"></p><!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 2px #cccccc;font-size:1;margin:0px auto;width:510px;" role="presentation" width="510px" ><tr><td style="height:0;line-height:0;"> &nbsp;
</td></tr></table><![endif]--></td></tr></table></div><!--[if mso | IE]></td></tr></table><![endif]--></td></tr></tbody></table></div><!--[if mso | IE]></td></tr></table><![endif]--></div></body></html>
@@ -1,25 +0,0 @@
<!doctype html><html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office"><head><title></title><!--[if !mso]><!-- --><meta http-equiv="X-UA-Compatible" content="IE=edge"><!--<![endif]--><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><style type="text/css">#outlook a { padding:0; }
.ReadMsgBody { width:100%; }
.ExternalClass { width:100%; }
.ExternalClass * { line-height:100%; }
body { margin:0;padding:0;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%; }
table, td { border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt; }
img { border:0;height:auto;line-height:100%; outline:none;text-decoration:none;-ms-interpolation-mode:bicubic; }
p { display:block;margin:13px 0; }</style><!--[if !mso]><!--><style type="text/css">@media only screen and (max-width:480px) {
@-ms-viewport { width:320px; }
@viewport { width:320px; }
}</style><!--<![endif]--><!--[if mso]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]--><!--[if lte mso 11]>
<style type="text/css">
.outlook-group-fix { width:100% !important; }
</style>
<![endif]--><!--[if !mso]><!--><link href="https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700" rel="stylesheet" type="text/css"><style type="text/css">@import url(https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700);</style><!--<![endif]--><style type="text/css">@media only screen and (min-width:480px) {
.mj-column-per-100 { width:100% !important; max-width: 100%; }
}</style><style type="text/css"></style></head><body style="background-color:#fafbfc;"><div style="background-color:#fafbfc;"><!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]--><div style="background:#ffffff;background-color:#ffffff;Margin:0px auto;max-width:600px;"><table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#ffffff;background-color:#ffffff;width:100%;"><tbody><tr><td style="direction:ltr;font-size:0px;padding:40px 20px;text-align:center;vertical-align:top;"><!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:middle;width:560px;" ><![endif]--><div class="mj-column-per-100 outlook-group-fix" style="font-size:13px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:100%;"><table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:middle;" width="100%"><tr><td align="center" style="font-size:0px;padding:35px;word-break:break-word;"><div style="font-family:Arial, Helvetica, sans-serif;font-size:20px;line-height:1;text-align:center;color:#333333;">{{ project_name }} - Password Recovery</div></td></tr><tr><td align="center" style="font-size:0px;padding:10px 25px;padding-right:25px;padding-left:25px;word-break:break-word;"><div style="font-family:Arial, Helvetica, sans-serif;font-size:16px;line-height:1;text-align:center;color:#555555;"><span>Hello {{ username }}</span></div></td></tr><tr><td align="center" style="font-size:0px;padding:10px 25px;padding-right:25px;padding-left:25px;word-break:break-word;"><div style="font-family:Arial, Helvetica, sans-serif;font-size:16px;line-height:1;text-align:center;color:#555555;">We've received a request to reset your password. You can do it by clicking the button below:</div></td></tr><tr><td align="center" vertical-align="middle" style="font-size:0px;padding:15px 30px;word-break:break-word;"><table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;"><tr><td align="center" bgcolor="#009688" role="presentation" style="border:none;border-radius:8px;cursor:auto;padding:10px 25px;background:#009688;" valign="middle"><a href="{{ link }}" style="background:#009688;color:#ffffff;font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:18px;font-weight:normal;line-height:120%;Margin:0;text-decoration:none;text-transform:none;" target="_blank">Reset password</a></td></tr></table></td></tr><tr><td align="center" style="font-size:0px;padding:10px 25px;padding-right:25px;padding-left:25px;word-break:break-word;"><div style="font-family:Arial, Helvetica, sans-serif;font-size:16px;line-height:1;text-align:center;color:#555555;">Or copy and paste the following link into your browser:</div></td></tr><tr><td align="center" style="font-size:0px;padding:10px 25px;padding-right:25px;padding-left:25px;word-break:break-word;"><div style="font-family:Arial, Helvetica, sans-serif;font-size:16px;line-height:1;text-align:center;color:#555555;"><a href="{{ link }}">{{ link }}</a></div></td></tr><tr><td align="center" style="font-size:0px;padding:10px 25px;padding-right:25px;padding-left:25px;word-break:break-word;"><div style="font-family:Arial, Helvetica, sans-serif;font-size:16px;line-height:1;text-align:center;color:#555555;">This password will expire in {{ valid_hours }} hours.</div></td></tr><tr><td style="font-size:0px;padding:10px 25px;word-break:break-word;"><p style="border-top:solid 2px #cccccc;font-size:1;margin:0px auto;width:100%;"></p><!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 2px #cccccc;font-size:1;margin:0px auto;width:510px;" role="presentation" width="510px" ><tr><td style="height:0;line-height:0;"> &nbsp;
</td></tr></table><![endif]--></td></tr><tr><td align="center" style="font-size:0px;padding:10px 25px;padding-right:25px;padding-left:25px;word-break:break-word;"><div style="font-family:Arial, Helvetica, sans-serif;font-size:14px;line-height:1;text-align:center;color:#555555;">If you didn't request a password recovery you can disregard this email.</div></td></tr></table></div><!--[if mso | IE]></td></tr></table><![endif]--></td></tr></tbody></table></div><!--[if mso | IE]></td></tr></table><![endif]--></div></body></html>
@@ -1,25 +0,0 @@
<!doctype html><html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office"><head><title></title><!--[if !mso]><!-- --><meta http-equiv="X-UA-Compatible" content="IE=edge"><!--<![endif]--><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><style type="text/css">#outlook a { padding:0; }
.ReadMsgBody { width:100%; }
.ExternalClass { width:100%; }
.ExternalClass * { line-height:100%; }
body { margin:0;padding:0;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%; }
table, td { border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt; }
img { border:0;height:auto;line-height:100%; outline:none;text-decoration:none;-ms-interpolation-mode:bicubic; }
p { display:block;margin:13px 0; }</style><!--[if !mso]><!--><style type="text/css">@media only screen and (max-width:480px) {
@-ms-viewport { width:320px; }
@viewport { width:320px; }
}</style><!--<![endif]--><!--[if mso]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]--><!--[if lte mso 11]>
<style type="text/css">
.outlook-group-fix { width:100% !important; }
</style>
<![endif]--><style type="text/css">@media only screen and (min-width:480px) {
.mj-column-per-100 { width:100% !important; max-width: 100%; }
}</style><style type="text/css"></style></head><body style="background-color:#fafbfc;"><div style="background-color:#fafbfc;"><!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]--><div style="background:#ffffff;background-color:#ffffff;Margin:0px auto;max-width:600px;"><table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#ffffff;background-color:#ffffff;width:100%;"><tbody><tr><td style="direction:ltr;font-size:0px;padding:40px 20px;text-align:center;vertical-align:top;"><!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:middle;width:560px;" ><![endif]--><div class="mj-column-per-100 outlook-group-fix" style="font-size:13px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:100%;"><table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:middle;" width="100%"><tr><td align="center" style="font-size:0px;padding:35px;word-break:break-word;"><div style="font-family:Arial, Helvetica, sans-serif;font-size:20px;line-height:1;text-align:center;color:#333333;">{{ project_name }}</div></td></tr><tr><td align="center" style="font-size:0px;padding:10px 25px;padding-right:25px;padding-left:25px;word-break:break-word;"><div style="font-family:, sans-serif;font-size:16px;line-height:1;text-align:center;color:#555555;"><span>Test email for: {{ email }}</span></div></td></tr><tr><td style="font-size:0px;padding:10px 25px;word-break:break-word;"><p style="border-top:solid 2px #cccccc;font-size:1;margin:0px auto;width:100%;"></p><!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 2px #cccccc;font-size:1;margin:0px auto;width:510px;" role="presentation" width="510px" ><tr><td style="height:0;line-height:0;"> &nbsp;
</td></tr></table><![endif]--></td></tr></table></div><!--[if mso | IE]></td></tr></table><![endif]--></td></tr></tbody></table></div><!--[if mso | IE]></td></tr></table><![endif]--></div></body></html>
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,15 +0,0 @@
<mjml>
<mj-body background-color="#fafbfc">
<mj-section background-color="#fff" padding="40px 20px">
<mj-column vertical-align="middle" width="100%">
<mj-text align="center" padding="35px" font-size="20px" color="#333">{{ project_name }} - New Account</mj-text>
<mj-text align="center" font-size="16px" padding-left="25px" padding-right="25px" font-family="Arial, Helvetica, sans-serif" color="#555"><span>Welcome to your new account!</span></mj-text>
<mj-text align="center" font-size="16px" padding-left="25px" padding-right="25px" font-family="Arial, Helvetica, sans-serif" color="#555">Here are your account details:</mj-text>
<mj-text align="center" font-size="16px" padding-left="25px" padding-right="25px" font-family="Arial, Helvetica, sans-serif" color="#555">Username: {{ username }}</mj-text>
<mj-text align="center" font-size="16px" padding-left="25px" padding-right="25px" font-family="Arial, Helvetica, sans-serif" color="#555">Password: {{ password }}</mj-text>
<mj-button align="center" font-size="18px" background-color="#009688" border-radius="8px" color="#fff" href="{{ link }}" padding="15px 30px">Go to Dashboard</mj-button>
<mj-divider border-color="#ccc" border-width="2px"></mj-divider>
</mj-column>
</mj-section>
</mj-body>
</mjml>
@@ -1,17 +0,0 @@
<mjml>
<mj-body background-color="#fafbfc">
<mj-section background-color="#fff" padding="40px 20px">
<mj-column vertical-align="middle" width="100%">
<mj-text align="center" padding="35px" font-size="20px" font-family="Arial, Helvetica, sans-serif" color="#333">{{ project_name }} - Password Recovery</mj-text>
<mj-text align="center" font-size="16px" padding-left="25px" padding-right="25px" font-family="Arial, Helvetica, sans-serif" color="#555"><span>Hello {{ username }}</span></mj-text>
<mj-text align="center" font-size="16px" padding-left="25px" padding-right="25px" font-family="Arial, Helvetica, sans-serif" color="#555">We've received a request to reset your password. You can do it by clicking the button below:</mj-text>
<mj-button align="center" font-size="18px" background-color="#009688" border-radius="8px" color="#fff" href="{{ link }}" padding="15px 30px">Reset password</mj-button>
<mj-text align="center" font-size="16px" padding-left="25px" padding-right="25px" font-family="Arial, Helvetica, sans-serif" color="#555">Or copy and paste the following link into your browser:</mj-text>
<mj-text align="center" font-size="16px" padding-left="25px" padding-right="25px" font-family="Arial, Helvetica, sans-serif" color="#555"><a href="{{ link }}">{{ link }}</a></mj-text>
<mj-text align="center" font-size="16px" padding-left="25px" padding-right="25px" font-family="Arial, Helvetica, sans-serif" color="#555">This password will expire in {{ valid_hours }} hours.</mj-text>
<mj-divider border-color="#ccc" border-width="2px"></mj-divider>
<mj-text align="center" font-size="14px" padding-left="25px" padding-right="25px" font-family="Arial, Helvetica, sans-serif" color="#555">If you didn't request a password recovery you can disregard this email.</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>
@@ -1,11 +0,0 @@
<mjml>
<mj-body background-color="#fafbfc">
<mj-section background-color="#fff" padding="40px 20px">
<mj-column vertical-align="middle" width="100%">
<mj-text align="center" padding="35px" font-size="20px" font-family="Arial, Helvetica, sans-serif" color="#333">{{ project_name }}</mj-text>
<mj-text align="center" font-size="16px" padding-left="25px" padding-right="25px" font-family=", sans-serif" color="#555"><span>Test email for: {{ email }}</span></mj-text>
<mj-divider border-color="#ccc" border-width="2px"></mj-divider>
</mj-column>
</mj-section>
</mj-body>
</mjml>
File diff suppressed because one or more lines are too long
+8 -10
View File
@@ -15,7 +15,7 @@ def custom_generate_unique_id(route: APIRoute) -> str:
return f"{route.tags[0]}-{route.name}" return f"{route.tags[0]}-{route.name}"
if settings.SENTRY_DSN and settings.ENVIRONMENT != "local": if settings.SENTRY_DSN and settings.FASTAPI_ENV != "development":
sentry_sdk.init(dsn=str(settings.SENTRY_DSN), enable_tracing=True) sentry_sdk.init(dsn=str(settings.SENTRY_DSN), enable_tracing=True)
app = FastAPI( app = FastAPI(
@@ -24,15 +24,13 @@ app = FastAPI(
generate_unique_id_function=custom_generate_unique_id, generate_unique_id_function=custom_generate_unique_id,
) )
# Set all CORS enabled origins app.add_middleware(
if settings.all_cors_origins: CORSMiddleware,
app.add_middleware( allow_origins=[settings.FRONTEND_HOST],
CORSMiddleware, allow_credentials=True,
allow_origins=settings.all_cors_origins, allow_methods=["*"],
allow_credentials=True, allow_headers=["*"],
allow_methods=["*"], )
allow_headers=["*"],
)
app.include_router(api_router, prefix=settings.API_V1_STR) app.include_router(api_router, prefix=settings.API_V1_STR)
app.frontend("/", directory=FRONTEND_DIR) app.frontend("/", directory=FRONTEND_DIR)
+2 -9
View File
@@ -2,7 +2,6 @@ import uuid
from datetime import UTC, datetime from datetime import UTC, datetime
from pydantic import EmailStr from pydantic import EmailStr
from sqlalchemy import DateTime
from sqlmodel import Field, Relationship, SQLModel from sqlmodel import Field, Relationship, SQLModel
@@ -52,10 +51,7 @@ class UpdatePassword(SQLModel):
class User(UserBase, table=True): class User(UserBase, table=True):
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True) id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
hashed_password: str hashed_password: str
created_at: datetime | None = Field( created_at: datetime | None = Field(default_factory=get_datetime_utc)
default_factory=get_datetime_utc,
sa_type=DateTime(timezone=True), # type: ignore
)
items: list[Item] = Relationship(back_populates="owner", cascade_delete=True) items: list[Item] = Relationship(back_populates="owner", cascade_delete=True)
@@ -90,10 +86,7 @@ class ItemUpdate(SQLModel):
# Database model, database table inferred from class name # Database model, database table inferred from class name
class Item(ItemBase, table=True): class Item(ItemBase, table=True):
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True) id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
created_at: datetime | None = Field( created_at: datetime | None = Field(default_factory=get_datetime_utc)
default_factory=get_datetime_utc,
sa_type=DateTime(timezone=True), # type: ignore
)
owner_id: uuid.UUID = Field( owner_id: uuid.UUID = Field(
foreign_key="user.id", nullable=False, ondelete="CASCADE" foreign_key="user.id", nullable=False, ondelete="CASCADE"
) )
-39
View File
@@ -1,39 +0,0 @@
import logging
from sqlalchemy import Engine
from sqlmodel import Session, select
from tenacity import after_log, before_log, retry, stop_after_attempt, wait_fixed
from app.core.db import engine
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
max_tries = 60 * 5 # 5 minutes
wait_seconds = 1
@retry(
stop=stop_after_attempt(max_tries),
wait=wait_fixed(wait_seconds),
before=before_log(logger, logging.INFO),
after=after_log(logger, logging.WARN),
)
def init(db_engine: Engine) -> None:
try:
# Try to create session to check if DB is awake
with Session(db_engine) as session:
session.exec(select(1))
except Exception as e:
logger.error(e)
raise e
def main() -> None:
logger.info("Initializing service")
init(engine)
logger.info("Service finished initializing")
if __name__ == "__main__":
main()
+3 -2
View File
@@ -24,9 +24,10 @@ class EmailData:
def render_email_template(*, template_name: str, context: dict[str, Any]) -> str: def render_email_template(*, template_name: str, context: dict[str, Any]) -> str:
template_str = ( template_str = (
Path(__file__).parent / "email-templates" / "build" / template_name Path(__file__).parent / "email-templates" / template_name
).read_text() ).read_text()
html_content = Template(template_str).render(context) template: Template = Template(template_str)
html_content = template.render(context)
return html_content return html_content
+8 -9
View File
@@ -7,18 +7,17 @@ dependencies = [
"fastapi[standard]>=0.141.1,<1.0.0", "fastapi[standard]>=0.141.1,<1.0.0",
"python-multipart<1.0.0,>=0.0.27", "python-multipart<1.0.0,>=0.0.27",
"email-validator<3.0.0.0,>=2.1.0.post1", "email-validator<3.0.0.0,>=2.1.0.post1",
"tenacity<10.0.0,>=8.2.3", "pydantic>=2.13.5",
"pydantic>2.0",
"emails>=1.1.2,<2.0", "emails>=1.1.2,<2.0",
"jinja2<4.0.0,>=3.1.4", "jinja2<4.0.0,>=3.1.4",
"alembic<2.0.0,>=1.12.1", "alembic>=1.19.1,<2.0.0",
"httpx<1.0.0,>=0.25.1", "httpx<1.0.0,>=0.25.1",
"psycopg[binary]>=3.3.4,<4.0.0", "psycopg[binary]>=3.3.6,<4.0.0",
"sqlmodel>=0.0.39,<1.0.0", "sqlmodel>=0.0.46,<1.0.0",
"pydantic-settings<3.0.0,>=2.2.1", "pydantic-settings<3.0.0,>=2.2.1",
"sentry-sdk[fastapi]>=2.66.1,<3.0.0", "sentry-sdk[fastapi]>=2.70.0,<3.0.0",
"pyjwt<3.0.0,>=2.13.0", "pyjwt>=2.15.0,<3.0.0",
"pwdlib[argon2,bcrypt]>=0.3.0", "pwdlib[argon2,bcrypt]>=0.3.1",
] ]
[dependency-groups] [dependency-groups]
@@ -26,7 +25,7 @@ dev = [
"pytest<10.0.0,>=7.4.3", "pytest<10.0.0,>=7.4.3",
"mypy<3.0.0,>=1.8.0", "mypy<3.0.0,>=1.8.0",
"ty>=0.0.25", "ty>=0.0.25",
"ruff<1.0.0,>=0.2.2", "ruff<1.0.0,>=0.16.0",
"coverage<8.0.0,>=7.4.3", "coverage<8.0.0,>=7.4.3",
] ]
-3
View File
@@ -3,9 +3,6 @@
set -e set -e
set -x set -x
# Let the DB start
python app/backend_pre_start.py
# Run migrations # Run migrations
alembic upgrade head alembic upgrade head
-2
View File
@@ -2,6 +2,4 @@
set -e set -e
set -x set -x
python app/tests_pre_start.py
bash scripts/test.sh "$@" bash scripts/test.sh "$@"
@@ -1,33 +0,0 @@
from unittest.mock import MagicMock, patch
from sqlmodel import select
from app.backend_pre_start import init, logger
def test_init_successful_connection() -> None:
engine_mock = MagicMock()
session_mock = MagicMock()
session_mock.__enter__.return_value = session_mock
select1 = select(1)
with (
patch("app.backend_pre_start.Session", return_value=session_mock),
patch("app.backend_pre_start.select", return_value=select1),
patch.object(logger, "info"),
patch.object(logger, "error"),
patch.object(logger, "warn"),
):
try:
init(engine_mock)
connection_successful = True
except Exception:
connection_successful = False
assert connection_successful, (
"The database connection should be successful and not raise an exception."
)
session_mock.exec.assert_called_once_with(select1)
@@ -1,33 +0,0 @@
from unittest.mock import MagicMock, patch
from sqlmodel import select
from app.tests_pre_start import init, logger
def test_init_successful_connection() -> None:
engine_mock = MagicMock()
session_mock = MagicMock()
session_mock.__enter__.return_value = session_mock
select1 = select(1)
with (
patch("app.tests_pre_start.Session", return_value=session_mock),
patch("app.tests_pre_start.select", return_value=select1),
patch.object(logger, "info"),
patch.object(logger, "error"),
patch.object(logger, "warn"),
):
try:
init(engine_mock)
connection_successful = True
except Exception:
connection_successful = False
assert connection_successful, (
"The database connection should be successful and not raise an exception."
)
session_mock.exec.assert_called_once_with(select1)
+814 -257
View File
File diff suppressed because it is too large Load Diff
+61
View File
@@ -0,0 +1,61 @@
services:
proxy:
restart: always
ports:
# Listen on port 80, default for HTTP, necessary to redirect to HTTPS
- "80:80"
# Listen on port 443, default for HTTPS
- "443:443"
volumes:
# Mount the volume to store the certificates
- traefik-certificates:/certificates
command:
# Enable Docker in Traefik, so that it reads labels from Docker services
- --providers.docker
# Do not expose all Docker services, only the ones explicitly exposed
- --providers.docker.exposedbydefault=false
# Create an entrypoint "http" listening on port 80
- --entrypoints.http.address=:80
# Redirect all HTTP traffic to HTTPS
- --entrypoints.http.http.redirections.entrypoint.to=https
- --entrypoints.http.http.redirections.entrypoint.scheme=https
# Create an entrypoint "https" listening on port 443
- --entrypoints.https.address=:443
# Store the Let's Encrypt certificates in the mounted volume
- --certificatesresolvers.le.acme.storage=/certificates/acme.json
# Use the TLS Challenge for Let's Encrypt
- --certificatesresolvers.le.acme.tlschallenge=true
# Enable the access log, with HTTP requests
- --accesslog
# Enable the Traefik log, for configurations and errors
- --log
db:
restart: always
adminer:
restart: always
labels:
# Route HTTPS traffic for the Adminer subdomain
- traefik.http.routers.adminer-https.rule=Host(`adminer.${DOMAIN:?Variable not set}`)
- traefik.http.routers.adminer-https.entrypoints=https
- traefik.http.routers.adminer-https.tls=true
# Use the Let's Encrypt resolver
- traefik.http.routers.adminer-https.tls.certresolver=le
backend:
restart: always
environment:
FRONTEND_HOST: https://${DOMAIN:?Variable not set}
labels:
# Route HTTPS traffic for this domain
- traefik.http.routers.backend-https.rule=Host(`${DOMAIN:?Variable not set}`)
- traefik.http.routers.backend-https.entrypoints=https
- traefik.http.routers.backend-https.tls=true
# Use the Let's Encrypt resolver
- traefik.http.routers.backend-https.tls.certresolver=le
volumes:
# Create a volume to store the certificates, even if the container is recreated
traefik-certificates:
+14 -44
View File
@@ -1,13 +1,7 @@
services: services:
# Local services are available on their ports, but also available on:
# http://localhost.tiangolo.com: application
# etc. To enable it, update .env, set:
# DOMAIN=localhost.tiangolo.com
proxy: proxy:
image: traefik:3.6 image: traefik:v3.7
volumes:
- /var/run/docker.sock:/var/run/docker.sock
ports: ports:
- "80:80" - "80:80"
- "8090:8080" - "8090:8080"
@@ -15,14 +9,10 @@ services:
command: command:
# Enable Docker in Traefik, so that it reads labels from Docker services # Enable Docker in Traefik, so that it reads labels from Docker services
- --providers.docker - --providers.docker
# Add a constraint to only use services with the label for this stack
- --providers.docker.constraints=Label(`traefik.constraint-label`, `traefik-public`)
# Do not expose all Docker services, only the ones explicitly exposed # Do not expose all Docker services, only the ones explicitly exposed
- --providers.docker.exposedbydefault=false - --providers.docker.exposedbydefault=false
# Create an entrypoint "http" listening on port 80 # Create an entrypoint "http" listening on port 80
- --entrypoints.http.address=:80 - --entrypoints.http.address=:80
# Create an entrypoint "https" listening on port 443
- --entrypoints.https.address=:443
# Enable the access log, with HTTP requests # Enable the access log, with HTTP requests
- --accesslog - --accesslog
# Enable the Traefik log, for configurations and errors # Enable the Traefik log, for configurations and errors
@@ -33,29 +23,15 @@ services:
- --api - --api
# Enable the Dashboard and API in insecure mode for local development # Enable the Dashboard and API in insecure mode for local development
- --api.insecure=true - --api.insecure=true
labels:
# Enable Traefik for this service, to make it available in the public network
- traefik.enable=true
- traefik.constraint-label=traefik-public
# Dummy https-redirect middleware that doesn't really redirect, only to
# allow running it locally
- traefik.http.middlewares.https-redirect.contenttype.autodetect=false
networks:
- traefik-public
- default
db: db:
restart: "no"
ports: ports:
- "5432:5432" - "5432:5432"
adminer: adminer:
restart: "no"
ports: ports:
- "8080:8080" - "8080:8080"
backend: backend:
restart: "no"
ports: ports:
- "8000:8000" - "8000:8000"
build: build:
@@ -64,16 +40,15 @@ services:
# command: sleep infinity # Infinite loop to keep container alive doing nothing # command: sleep infinity # Infinite loop to keep container alive doing nothing
command: command:
- fastapi - fastapi
- run - dev
- --reload - --host
- "app/main.py" - "0.0.0.0"
develop: develop:
watch: watch:
- path: ./backend - path: ./backend
action: sync action: sync
target: /app/backend target: /app/backend
ignore: ignore:
- ./backend/.venv
- .venv - .venv
- path: ./backend/pyproject.toml - path: ./backend/pyproject.toml
action: rebuild action: rebuild
@@ -88,15 +63,15 @@ services:
volumes: volumes:
- ./backend/htmlcov:/app/backend/htmlcov - ./backend/htmlcov:/app/backend/htmlcov
environment: environment:
SMTP_HOST: "mailcatcher" FASTAPI_ENV: "development"
SMTP_HOST: "mailpit"
SMTP_PORT: "1025" SMTP_PORT: "1025"
SMTP_TLS: "false" SMTP_TLS: "false"
EMAILS_FROM_EMAIL: "noreply@example.com"
mailcatcher: mailpit:
image: schickling/mailcatcher image: axllent/mailpit
ports: ports:
- "1080:1080" - "8025:8025"
- "1025:1025" - "1025:1025"
playwright: playwright:
@@ -108,23 +83,18 @@ services:
ipc: host ipc: host
depends_on: depends_on:
- backend - backend
- mailcatcher - mailpit
env_file:
- .env
environment: environment:
- FIRST_SUPERUSER=${FIRST_SUPERUSER:?Variable not set}
- FIRST_SUPERUSER_PASSWORD=${FIRST_SUPERUSER_PASSWORD:?Variable not set}
- PLAYWRIGHT_BASE_URL=http://backend:8000 - PLAYWRIGHT_BASE_URL=http://backend:8000
- VITE_API_URL=http://backend:8000 - VITE_API_URL=http://backend:8000
- MAILCATCHER_HOST=http://mailcatcher:1080 - MAILPIT_HOST=http://mailpit:8025
# For the reports when run locally # For the reports when run locally
- PLAYWRIGHT_HTML_HOST=0.0.0.0 - PLAYWRIGHT_HTML_HOST=0.0.0.0
- CI=${CI} - CI=${CI:-}
volumes: volumes:
- ./frontend/blob-report:/app/frontend/blob-report - ./frontend/blob-report:/app/frontend/blob-report
- ./frontend/test-results:/app/frontend/test-results - ./frontend/test-results:/app/frontend/test-results
ports: ports:
- 9323:9323 - 9323:9323
networks:
traefik-public:
# For local dev, don't expect an external Traefik network
external: false
-77
View File
@@ -1,77 +0,0 @@
services:
traefik:
image: traefik:3.6
ports:
# Listen on port 80, default for HTTP, necessary to redirect to HTTPS
- 80:80
# Listen on port 443, default for HTTPS
- 443:443
restart: always
labels:
# Enable Traefik for this service, to make it available in the public network
- traefik.enable=true
# Use the traefik-public network (declared below)
- traefik.docker.network=traefik-public
# Define the port inside of the Docker service to use
- traefik.http.services.traefik-dashboard.loadbalancer.server.port=8080
# Make Traefik use this domain (from an environment variable) in HTTP
- traefik.http.routers.traefik-dashboard-http.entrypoints=http
- traefik.http.routers.traefik-dashboard-http.rule=Host(`traefik.${DOMAIN?Variable not set}`)
# traefik-https the actual router using HTTPS
- traefik.http.routers.traefik-dashboard-https.entrypoints=https
- traefik.http.routers.traefik-dashboard-https.rule=Host(`traefik.${DOMAIN?Variable not set}`)
- traefik.http.routers.traefik-dashboard-https.tls=true
# Use the "le" (Let's Encrypt) resolver created below
- traefik.http.routers.traefik-dashboard-https.tls.certresolver=le
# Use the special Traefik service api@internal with the web UI/Dashboard
- traefik.http.routers.traefik-dashboard-https.service=api@internal
# https-redirect middleware to redirect HTTP to HTTPS
- traefik.http.middlewares.https-redirect.redirectscheme.scheme=https
- traefik.http.middlewares.https-redirect.redirectscheme.permanent=true
# traefik-http set up only to use the middleware to redirect to https
- traefik.http.routers.traefik-dashboard-http.middlewares=https-redirect
# admin-auth middleware with HTTP Basic auth
# Using the environment variables USERNAME and HASHED_PASSWORD
- traefik.http.middlewares.admin-auth.basicauth.users=${USERNAME?Variable not set}:${HASHED_PASSWORD?Variable not set}
# Enable HTTP Basic auth, using the middleware created above
- traefik.http.routers.traefik-dashboard-https.middlewares=admin-auth
volumes:
# Add Docker as a mounted volume, so that Traefik can read the labels of other services
- /var/run/docker.sock:/var/run/docker.sock:ro
# Mount the volume to store the certificates
- traefik-public-certificates:/certificates
command:
# Enable Docker in Traefik, so that it reads labels from Docker services
- --providers.docker
# Do not expose all Docker services, only the ones explicitly exposed
- --providers.docker.exposedbydefault=false
# Create an entrypoint "http" listening on port 80
- --entrypoints.http.address=:80
# Create an entrypoint "https" listening on port 443
- --entrypoints.https.address=:443
# Create the certificate resolver "le" for Let's Encrypt, uses the environment variable EMAIL
- --certificatesresolvers.le.acme.email=${EMAIL?Variable not set}
# Store the Let's Encrypt certificates in the mounted volume
- --certificatesresolvers.le.acme.storage=/certificates/acme.json
# Use the TLS Challenge for Let's Encrypt
- --certificatesresolvers.le.acme.tlschallenge=true
# Enable the access log, with HTTP requests
- --accesslog
# Enable the Traefik log, for configurations and errors
- --log
# Enable the Dashboard and API
- --api
networks:
# Use the public network created to be shared between Traefik and
# any other service that needs to be publicly available with HTTPS
- traefik-public
volumes:
# Create a volume to store the certificates, even if the container is recreated
traefik-public-certificates:
networks:
# Use the previously created public network "traefik-public", shared with other
# services that need to be publicly available via this Traefik
traefik-public:
external: true
+43 -98
View File
@@ -1,112 +1,68 @@
services: services:
proxy:
image: traefik:v3.7
volumes:
# Add Docker as a mounted volume, so that Traefik can read the labels of other services
- /var/run/docker.sock:/var/run/docker.sock:ro
command:
# Enable Docker in Traefik, so that it reads labels from Docker services
- --providers.docker
# Do not expose all Docker services, only the ones explicitly exposed
- --providers.docker.exposedbydefault=false
# Create an entrypoint "http" listening on port 80
- --entrypoints.http.address=:80
# Enable the access log, with HTTP requests
- --accesslog
# Enable the Traefik log, for configurations and errors
- --log
db: db:
image: postgres:18 image: postgres:18
restart: always
healthcheck: healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] test: ["CMD-SHELL", "pg_isready -U postgres -d app"]
interval: 10s interval: 10s
retries: 5 retries: 5
start_period: 30s start_period: 30s
timeout: 10s timeout: 10s
volumes: volumes:
- app-db-data:/var/lib/postgresql - app-db-data:/var/lib/postgresql
env_file:
- .env
environment: environment:
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD?Variable not set} - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:?Variable not set}
- POSTGRES_USER=${POSTGRES_USER?Variable not set} - POSTGRES_DB=app
- POSTGRES_DB=${POSTGRES_DB?Variable not set}
adminer: adminer:
image: adminer image: adminer
restart: always
networks:
- traefik-public
- default
depends_on: depends_on:
- db - db
environment: environment:
- ADMINER_DESIGN=pepa-linha-dark - ADMINER_DESIGN=pepa-linha-dark
labels: labels:
# Enable Traefik for this service
- traefik.enable=true - traefik.enable=true
- traefik.docker.network=traefik-public # Route HTTP traffic for the Adminer subdomain
- traefik.constraint-label=traefik-public - traefik.http.routers.adminer-http.rule=Host(`adminer.${DOMAIN:-localhost}`)
- traefik.http.routers.${STACK_NAME?Variable not set}-adminer-http.rule=Host(`adminer.${DOMAIN?Variable not set}`) - traefik.http.routers.adminer-http.entrypoints=http
- traefik.http.routers.${STACK_NAME?Variable not set}-adminer-http.entrypoints=http # Define the port inside of the Docker service to use
- traefik.http.routers.${STACK_NAME?Variable not set}-adminer-http.middlewares=https-redirect - traefik.http.services.adminer.loadbalancer.server.port=8080
- traefik.http.routers.${STACK_NAME?Variable not set}-adminer-https.rule=Host(`adminer.${DOMAIN?Variable not set}`)
- traefik.http.routers.${STACK_NAME?Variable not set}-adminer-https.entrypoints=https
- traefik.http.routers.${STACK_NAME?Variable not set}-adminer-https.tls=true
- traefik.http.routers.${STACK_NAME?Variable not set}-adminer-https.tls.certresolver=le
- traefik.http.services.${STACK_NAME?Variable not set}-adminer.loadbalancer.server.port=8080
prestart:
image: '${DOCKER_IMAGE_BACKEND?Variable not set}:${TAG-latest}'
build:
context: .
dockerfile: backend/Dockerfile
networks:
- traefik-public
- default
depends_on:
db:
condition: service_healthy
restart: true
command: bash scripts/prestart.sh
env_file:
- .env
environment:
- DOMAIN=${DOMAIN}
- FRONTEND_HOST=${FRONTEND_HOST?Variable not set}
- ENVIRONMENT=${ENVIRONMENT}
- BACKEND_CORS_ORIGINS=${BACKEND_CORS_ORIGINS}
- SECRET_KEY=${SECRET_KEY?Variable not set}
- FIRST_SUPERUSER=${FIRST_SUPERUSER?Variable not set}
- FIRST_SUPERUSER_PASSWORD=${FIRST_SUPERUSER_PASSWORD?Variable not set}
- SMTP_HOST=${SMTP_HOST}
- SMTP_USER=${SMTP_USER}
- SMTP_PASSWORD=${SMTP_PASSWORD}
- EMAILS_FROM_EMAIL=${EMAILS_FROM_EMAIL}
- POSTGRES_SERVER=db
- POSTGRES_PORT=${POSTGRES_PORT}
- POSTGRES_DB=${POSTGRES_DB}
- POSTGRES_USER=${POSTGRES_USER?Variable not set}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD?Variable not set}
- SENTRY_DSN=${SENTRY_DSN}
backend: backend:
image: '${DOCKER_IMAGE_BACKEND?Variable not set}:${TAG-latest}' image: backend:latest
restart: always
networks:
- traefik-public
- default
depends_on: depends_on:
db: db:
condition: service_healthy condition: service_healthy
restart: true restart: true
prestart:
condition: service_completed_successfully
env_file:
- .env
environment: environment:
- DOMAIN=${DOMAIN} PROJECT_NAME: ${PROJECT_NAME:?Variable not set}
- FRONTEND_HOST=${FRONTEND_HOST?Variable not set} SECRET_KEY: ${SECRET_KEY:?Variable not set}
- ENVIRONMENT=${ENVIRONMENT} FIRST_SUPERUSER: ${FIRST_SUPERUSER:?Variable not set}
- BACKEND_CORS_ORIGINS=${BACKEND_CORS_ORIGINS} FIRST_SUPERUSER_PASSWORD: ${FIRST_SUPERUSER_PASSWORD:?Variable not set}
- SECRET_KEY=${SECRET_KEY?Variable not set} SMTP_HOST: ${SMTP_HOST}
- FIRST_SUPERUSER=${FIRST_SUPERUSER?Variable not set} SMTP_USER: ${SMTP_USER:-}
- FIRST_SUPERUSER_PASSWORD=${FIRST_SUPERUSER_PASSWORD?Variable not set} SMTP_PASSWORD: ${SMTP_PASSWORD:-}
- SMTP_HOST=${SMTP_HOST} EMAILS_FROM_EMAIL: ${EMAILS_FROM_EMAIL}
- SMTP_USER=${SMTP_USER} DATABASE_URL: postgresql://postgres:${POSTGRES_PASSWORD:?Variable not set}@db:5432/app
- SMTP_PASSWORD=${SMTP_PASSWORD} SENTRY_DSN: ${SENTRY_DSN:-}
- EMAILS_FROM_EMAIL=${EMAILS_FROM_EMAIL}
- POSTGRES_SERVER=db
- POSTGRES_PORT=${POSTGRES_PORT}
- POSTGRES_DB=${POSTGRES_DB}
- POSTGRES_USER=${POSTGRES_USER?Variable not set}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD?Variable not set}
- SENTRY_DSN=${SENTRY_DSN}
healthcheck: healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/api/v1/utils/health-check/"] test: ["CMD", "curl", "-f", "http://localhost:8000/api/v1/utils/health-check/"]
@@ -118,26 +74,15 @@ services:
context: . context: .
dockerfile: backend/Dockerfile dockerfile: backend/Dockerfile
labels: labels:
# Enable Traefik for this service
- traefik.enable=true - traefik.enable=true
- traefik.docker.network=traefik-public
- traefik.constraint-label=traefik-public
- traefik.http.services.${STACK_NAME?Variable not set}-backend.loadbalancer.server.port=8000 # Define the port inside of the Docker service to use
- traefik.http.services.backend.loadbalancer.server.port=8000
- traefik.http.routers.${STACK_NAME?Variable not set}-backend-http.rule=Host(`${DOMAIN?Variable not set}`) # Route HTTP traffic for this domain
- traefik.http.routers.${STACK_NAME?Variable not set}-backend-http.entrypoints=http - traefik.http.routers.backend-http.rule=Host(`${DOMAIN:-localhost}`)
- traefik.http.routers.backend-http.entrypoints=http
- traefik.http.routers.${STACK_NAME?Variable not set}-backend-https.rule=Host(`${DOMAIN?Variable not set}`)
- traefik.http.routers.${STACK_NAME?Variable not set}-backend-https.entrypoints=https
- traefik.http.routers.${STACK_NAME?Variable not set}-backend-https.tls=true
- traefik.http.routers.${STACK_NAME?Variable not set}-backend-https.tls.certresolver=le
# Enable redirection for HTTP and HTTPS
- traefik.http.routers.${STACK_NAME?Variable not set}-backend-http.middlewares=https-redirect
volumes: volumes:
app-db-data: app-db-data:
networks:
traefik-public:
# Allow setting it to false for testing
external: true
-100
View File
@@ -1,100 +0,0 @@
project_name:
type: str
help: The name of the project, shown to API users (in .env)
default: FastAPI Project
stack_name:
type: str
help: The name of the stack used for Docker Compose labels (no spaces) (in .env)
default: fastapi-project
secret_key:
type: str
help: |
'The secret key for the project, used for security,
stored in .env, you can generate one with:
python -c "import secrets; print(secrets.token_urlsafe(32))"'
default: changethis
first_superuser:
type: str
help: The email of the first superuser (in .env)
default: admin@example.com
first_superuser_password:
type: str
help: The password of the first superuser (in .env)
default: changethis
smtp_host:
type: str
help: The SMTP server host to send emails, you can set it later in .env
default: ""
smtp_user:
type: str
help: The SMTP server user to send emails, you can set it later in .env
default: ""
smtp_password:
type: str
help: The SMTP server password to send emails, you can set it later in .env
default: ""
emails_from_email:
type: str
help: The email account to send emails from, you can set it later in .env
default: info@example.com
postgres_password:
type: str
help: |
'The password for the PostgreSQL database, stored in .env,
you can generate one with:
python -c "import secrets; print(secrets.token_urlsafe(32))"'
default: changethis
sentry_dsn:
type: str
help: The DSN for Sentry, if you are using it, you can set it later in .env
default: ""
_exclude:
# Global
- .vscode
- .mypy_cache
# Python
- __pycache__
- app.egg-info
- "*.pyc"
- .mypy_cache
- .coverage
- htmlcov
- .cache
- .venv
# Frontend
# Logs
- logs
- "*.log"
- npm-debug.log*
- yarn-debug.log*
- yarn-error.log*
- pnpm-debug.log*
- lerna-debug.log*
- node_modules
- dist
- dist-ssr
- "*.local"
# Editor directories and files
- .idea
- .DS_Store
- "*.suo"
- "*.ntvs*"
- "*.njsproj"
- "*.sln"
- "*.sw?"
_answers_file: .copier/.copier-answers.yml
_tasks:
- ["{{ _copier_python }}", .copier/update_dotenv.py]
+129
View File
@@ -0,0 +1,129 @@
# FastAPI Project - Docker Compose Deployment
You can deploy the project to your own remote server with Docker Compose. The deployment configuration includes Traefik to handle HTTPS and route incoming traffic to the application.
## Preparation
* Have a remote server ready and available.
* Configure DNS records pointing to the server for the application domain and any supporting service subdomains you want to expose, such as `fastapi-project.example.com` and `adminer.fastapi-project.example.com`.
* Install and configure [Docker](https://docs.docker.com/engine/install/) on the remote server (Docker Engine, not Docker Desktop).
## Copy the Code
```bash
rsync -av --exclude=".git/" --filter=":- .gitignore" ./ root@your-server.example.com:/root/code/app/
```
The `--filter=":- .gitignore"` option tells `rsync` to use the same ignore rules as Git, excluding files such as the Python virtual environment.
## Configure the Application
### Environment Variables
Set the application domain, project name, and first superuser email:
```bash
export DOMAIN=fastapi-project.example.com
export PROJECT_NAME="Full Stack FastAPI Project"
export FIRST_SUPERUSER=admin@example.com
```
You can also configure these environment variables as needed:
* `SMTP_HOST`: The SMTP server host from your email provider.
* `SMTP_USER`: The SMTP server user.
* `EMAILS_FROM_EMAIL`: The email account used to send emails.
* `SENTRY_DSN`: The DSN for Sentry.
### Secrets
Generate and set secure values for the database password, token signing key, and first superuser password:
```bash
export POSTGRES_PASSWORD="$(python -c 'import secrets; print(secrets.token_urlsafe(32))')"
export SECRET_KEY="$(python -c 'import secrets; print(secrets.token_urlsafe(32))')"
export FIRST_SUPERUSER_PASSWORD="$(python -c 'import secrets; print(secrets.token_urlsafe(32))')"
```
To use an authenticated email provider, also set `SMTP_PASSWORD`.
## Deploy
```bash
cd /root/code/app/
docker compose -f compose.yml -f compose.deploy.yml build
docker compose -f compose.yml -f compose.deploy.yml run --rm backend bash scripts/prestart.sh
docker compose -f compose.yml -f compose.deploy.yml up -d
```
The `compose.deploy.yml` file adds HTTPS and automatic certificate handling to the shared `compose.yml` configuration. Explicitly listing both files excludes the local settings from `compose.override.yml`.
The backend Docker image builds the frontend, so the server does not need Bun or prebuilt frontend files.
## Deploy with GitHub Actions
The included `.github/workflows/deploy-docker-compose.yml` workflow runs the deployment commands on the server when manually triggered from GitHub Actions.
Use a self-hosted runner only for a repository whose contributors and workflow code you trust. GitHub recommends using self-hosted runners with private repositories because workflows execute directly on the runner machine.
### Configure Repository Variables and Secrets
In the repository, go to **Settings** > **Secrets and variables** > **Actions** and add these repository variables:
* `DOMAIN`
* `PROJECT_NAME`
* `FIRST_SUPERUSER`
To enable emails, add these optional repository variables:
* `SMTP_HOST`
* `SMTP_USER`
* `EMAILS_FROM_EMAIL`
To enable Sentry, add the optional `SENTRY_DSN` repository variable.
Add these repository secrets:
* `POSTGRES_PASSWORD`
* `SECRET_KEY`
* `FIRST_SUPERUSER_PASSWORD`
To use an authenticated email provider, add the optional `SMTP_PASSWORD` repository secret.
### Install a Self-Hosted Runner
On the server, create a dedicated user and grant it access to Docker:
```bash
sudo adduser github
sudo usermod -aG docker github
sudo su - github
```
In the GitHub repository, go to **Settings** > **Actions** > **Runners**, select **New self-hosted runner**, choose Linux, and follow the commands GitHub provides to download, configure, and register the runner. Install it in `/home/github/actions-runner`.
After registering the runner, exit the `github` user session and install the runner as a system service:
```bash
exit
cd /home/github/actions-runner
sudo ./svc.sh install github
sudo ./svc.sh start
sudo ./svc.sh status
```
See GitHub's guides for [adding a self-hosted runner](https://docs.github.com/en/actions/how-tos/manage-runners/self-hosted-runners/add-runners) and [configuring the runner as a service](https://docs.github.com/en/actions/how-tos/manage-runners/self-hosted-runners/configure-the-application?platform=linux).
### Run the Deployment
When the runner is online, open the repository's **Actions** tab, select **Deploy with Docker Compose**, and select **Run workflow**.
## URLs
Replace `fastapi-project.example.com` with your domain.
Application (frontend and API): `https://fastapi-project.example.com`
Interactive API docs: `https://fastapi-project.example.com/docs`
Adminer: `https://adminer.fastapi-project.example.com`
+51 -301
View File
@@ -1,347 +1,97 @@
# FastAPI Project - Deployment # FastAPI Project - Deployment
You can deploy the project using Docker Compose to a remote server. Deploy the project to [FastAPI Cloud](https://fastapicloud.com) with the included GitHub Actions workflow.
This project expects you to have a Traefik proxy handling communication to the outside world and HTTPS certificates. ## Create the FastAPI Cloud Application
You can use CI/CD (continuous integration and continuous deployment) systems to deploy automatically, there are already configurations to do it with GitHub Actions. Create an application in FastAPI Cloud and set its [Application Directory](https://fastapicloud.com/docs/builds-and-deployments/application-directory/) to `backend`.
But you have to configure a couple things first. 🤓 Connect a PostgreSQL database using the [Neon](https://fastapicloud.com/docs/integrations/neon-integration/) or [Supabase](https://fastapicloud.com/docs/integrations/supabase-integration/) integration. Both integrations configure a `DATABASE_URL` secret automatically. You can also configure `DATABASE_URL` manually for another PostgreSQL provider.
## Preparation ## Configure the Application
* Have a remote server ready and available. ### Environment Variables
* Configure DNS records pointing to the server for the application domain and any supporting service subdomains you want to expose, e.g. `fastapi-project.example.com`, `traefik.fastapi-project.example.com`, and `adminer.fastapi-project.example.com`. Do the same for staging domains such as `staging.fastapi-project.example.com`.
* Install and configure [Docker](https://docs.docker.com/engine/install/) on the remote server (Docker Engine, not Docker Desktop).
## Public Traefik Add these required [environment variables](https://fastapicloud.com/docs/builds-and-deployments/environment-variables/) to the FastAPI Cloud application:
We need a Traefik proxy to handle incoming connections and HTTPS certificates. * `PROJECT_NAME`: The name of the project, used in the API documentation and emails.
* `FIRST_SUPERUSER`: The email address of the first superuser.
* `FRONTEND_HOST`: The public URL of the application, such as the generated `https://your-app.fastapicloud.dev` URL or a custom domain.
You need to do these next steps only once. To enable emails, add these optional environment variables with values from your email provider:
### Traefik Docker Compose * `SMTP_HOST`
* `SMTP_USER`
* `EMAILS_FROM_EMAIL`
* Create a remote directory to store your Traefik Docker Compose file: To enable Sentry, configure `SENTRY_DSN`.
```bash ### Secrets
mkdir -p /root/code/traefik-public/
```
Copy the Traefik Docker Compose file to your server. You could do it by running the command `rsync` in your local terminal: Add these required values and mark them as secrets:
```bash * `SECRET_KEY`: A secret key used to sign security tokens.
rsync -a compose.traefik.yml root@your-server.example.com:/root/code/traefik-public/ * `FIRST_SUPERUSER_PASSWORD`: The password of the first superuser.
``` * `DATABASE_URL`: The PostgreSQL connection URL, configured automatically when using a database integration.
### Traefik Public Network To enable emails with an authenticated provider, add `SMTP_PASSWORD` as a secret.
This Traefik will expect a Docker "public network" named `traefik-public` to communicate with your stack(s). You can generate secure values for `SECRET_KEY` and `FIRST_SUPERUSER_PASSWORD` with:
This way, there will be a single public Traefik proxy that handles the communication (HTTP and HTTPS) with the outside world, and then behind that, you could have one or more stacks with different domains, even if they are on the same single server.
To create a Docker "public network" named `traefik-public` run the following command in your remote server:
```bash
docker network create traefik-public
```
### Traefik Environment Variables
The Traefik Docker Compose file expects some environment variables to be set in your terminal before starting it. You can do it by running the following commands in your remote server.
* Create the username for HTTP Basic Auth, e.g.:
```bash
export USERNAME=admin
```
* Create an environment variable with the password for HTTP Basic Auth, e.g.:
```bash
export PASSWORD=changethis
```
* Use openssl to generate the "hashed" version of the password for HTTP Basic Auth and store it in an environment variable:
```bash
export HASHED_PASSWORD=$(openssl passwd -apr1 $PASSWORD)
```
To verify that the hashed password is correct, you can print it:
```bash
echo $HASHED_PASSWORD
```
* Create an environment variable with the domain name for your server, e.g.:
```bash
export DOMAIN=fastapi-project.example.com
```
* Create an environment variable with the email for Let's Encrypt, e.g.:
```bash
export EMAIL=admin@example.com
```
**Note**: you need to set a different email, an email `@example.com` won't work.
### Start the Traefik Docker Compose
Go to the directory where you copied the Traefik Docker Compose file in your remote server:
```bash
cd /root/code/traefik-public/
```
Now with the environment variables set and the `compose.traefik.yml` in place, you can start the Traefik Docker Compose running the following command:
```bash
docker compose -f compose.traefik.yml up -d
```
## Deploy the FastAPI Project
Now that you have Traefik in place you can deploy your FastAPI project with Docker Compose.
**Note**: You might want to jump ahead to the section about Continuous Deployment with GitHub Actions.
## Copy the Code
```bash
rsync -av --exclude=".git/" --filter=":- .gitignore" ./ root@your-server.example.com:/root/code/app/
```
Note: `--filter=":- .gitignore"` tells `rsync` to use the same rules as git, ignore files ignored by git, like the Python virtual environment.
## Environment Variables
You need to set some environment variables first.
### Generate secret keys
Some environment variables in the `.env` file have a default value of `changethis`.
You have to change them with a secret key, to generate secret keys you can run the following command:
```bash ```bash
python -c "import secrets; print(secrets.token_urlsafe(32))" python -c "import secrets; print(secrets.token_urlsafe(32))"
``` ```
Copy the content and use that as password / secret key. And run that again to generate another secure key. ## Configure Continuous Deployment
### Required Environment Variables The included `.github/workflows/deploy.yml` workflow builds the frontend, prepares the database, and deploys the application whenever changes are pushed to `master`. You can also run it manually from the **Actions** tab.
Set the `ENVIRONMENT`, by default `local` (for development), but when deploying to a server you would put something like `staging` or `production`: Log in to FastAPI Cloud and configure the [deploy token](https://fastapicloud.com/docs/advanced-features/deploy-tokens/) and application ID as GitHub repository secrets:
```bash ```bash
export ENVIRONMENT=production uv run fastapi login
uv run fastapi cloud setup-ci --secrets-only --app-id <your-app-id>
``` ```
Set the `DOMAIN`, by default `localhost` (for development), but when deploying you would use your own domain, for example: If the GitHub CLI is installed and authenticated, the command configures `FASTAPI_CLOUD_TOKEN` and `FASTAPI_CLOUD_APP_ID` automatically. Otherwise, it prints the values so you can add them in your repository under **Settings** > **Secrets and variables** > **Actions**.
```bash The workflow runs database migrations and creates the first superuser before deploying. In the repository's **Settings** > **Secrets and variables** > **Actions** page, add these repository variables:
export DOMAIN=fastapi-project.example.com
```
Set the `POSTGRES_PASSWORD` to something different than `changethis`: * `PROJECT_NAME`
```bash
export POSTGRES_PASSWORD="changethis"
```
Set the `SECRET_KEY`, used to sign tokens:
```bash
export SECRET_KEY="changethis"
```
Note: you can use the Python command above to generate a secure secret key.
Set the `FIRST_SUPER_USER_PASSWORD` to something different than `changethis`:
```bash
export FIRST_SUPERUSER_PASSWORD="changethis"
```
Set the `FRONTEND_HOST` to the application URL. It is used to generate links in emails:
```bash
export FRONTEND_HOST="https://${DOMAIN?Variable not set}"
```
You can set several other environment variables:
* `PROJECT_NAME`: The name of the project, used in the API for the docs and emails.
* `STACK_NAME`: The name of the stack used for Docker Compose labels and project name, this should be different for `staging`, `production`, etc. You could use the same domain replacing dots with dashes, e.g. `fastapi-project-example-com` and `staging-fastapi-project-example-com`.
* `BACKEND_CORS_ORIGINS`: A list of additional allowed CORS origins separated by commas. The frontend served by FastAPI uses the same origin and doesn't need to be added.
* `FIRST_SUPERUSER`: The email of the first superuser, this superuser will be the one that can create new users.
* `SMTP_HOST`: The SMTP server host to send emails, this would come from your email provider (E.g. Mailgun, Sparkpost, Sendgrid, etc).
* `SMTP_USER`: The SMTP server user to send emails.
* `SMTP_PASSWORD`: The SMTP server password to send emails.
* `EMAILS_FROM_EMAIL`: The email account to send emails from.
* `POSTGRES_SERVER`: The hostname of the PostgreSQL server. You can leave the default of `db`, provided by the same Docker Compose. You normally wouldn't need to change this unless you are using a third-party provider.
* `POSTGRES_PORT`: The port of the PostgreSQL server. You can leave the default. You normally wouldn't need to change this unless you are using a third-party provider.
* `POSTGRES_USER`: The Postgres user, you can leave the default.
* `POSTGRES_DB`: The database name to use for this application. You can leave the default of `app`.
* `SENTRY_DSN`: The DSN for Sentry, if you are using it.
## GitHub Actions Environment Variables
There are some environment variables only used by GitHub Actions that you can configure:
* `LATEST_CHANGES`: Used by the GitHub Action [latest-changes](https://github.com/tiangolo/latest-changes) to automatically add release notes based on the PRs merged. It's a personal access token, read the docs for details.
* `SMOKESHOW_AUTH_KEY`: Used to handle and publish the code coverage using [Smokeshow](https://github.com/samuelcolvin/smokeshow), follow their instructions to create a (free) Smokeshow key.
### Deploy with Docker Compose
With the environment variables in place, you can deploy with Docker Compose:
```bash
cd /root/code/app/
docker compose -f compose.yml build
docker compose -f compose.yml up -d
```
For production you wouldn't want to have the overrides in `compose.override.yml`, that's why we explicitly specify `compose.yml` as the file to use.
## Continuous Deployment (CD)
You can use GitHub Actions to deploy your project automatically. 😎
You can have multiple environment deployments.
There are already two environments configured, `staging` and `production`. 🚀
### Install GitHub Actions Runner
* On your remote server, create a user for your GitHub Actions:
```bash
sudo adduser github
```
* Add Docker permissions to the `github` user:
```bash
sudo usermod -aG docker github
```
* Temporarily switch to the `github` user:
```bash
sudo su - github
```
* Go to the `github` user's home directory:
```bash
cd
```
* [Install a GitHub Action self-hosted runner following the official guide](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/adding-self-hosted-runners#adding-a-self-hosted-runner-to-a-repository).
* When asked about labels, add a label for the environment, e.g. `production`. You can also add labels later.
After installing, the guide would tell you to run a command to start the runner. Nevertheless, it would stop once you terminate that process or if your local connection to your server is lost.
To make sure it runs on startup and continues running, you can install it as a service. To do that, exit the `github` user and go back to the `root` user:
```bash
exit
```
After you do it, you will be on the previous user again. And you will be on the previous directory, belonging to that user.
Before being able to go the `github` user directory, you need to become the `root` user (you might already be):
```bash
sudo su
```
* As the `root` user, go to the `actions-runner` directory inside of the `github` user's home directory:
```bash
cd /home/github/actions-runner
```
* Install the self-hosted runner as a service with the user `github`:
```bash
./svc.sh install github
```
* Start the service:
```bash
./svc.sh start
```
* Check the status of the service:
```bash
./svc.sh status
```
You can read more about it in the official guide: [Configuring the self-hosted runner application as a service](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/configuring-the-self-hosted-runner-application-as-a-service).
### Configure GitHub Environments
The deployment workflows use [GitHub Environments](https://docs.github.com/en/actions/how-tos/deploy/configure-and-manage-deployments/manage-environments) for `staging` and `production`. This enables environment-specific secrets, deployment protection rules (e.g. required reviewers, wait timers), and deployment status tracking.
To configure them, go to your repository's **Settings** > **Environments** and create the `staging` and `production` environments.
### Set Secrets
For each GitHub Environment (`staging` and `production`), configure the required secrets as [environment secrets](https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/use-secrets#creating-secrets-for-an-environment). Environment secrets are preferred over [repository secrets](https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/use-secrets#creating-secrets-for-a-repository) because they are scoped to the specific environment, reducing exposure and aligning with any protection rules you configure.
The current Github Actions workflows expect these secrets:
* `DOMAIN_PRODUCTION`
* `DOMAIN_STAGING`
* `STACK_NAME_PRODUCTION`
* `STACK_NAME_STAGING`
* `EMAILS_FROM_EMAIL`
* `FIRST_SUPERUSER` * `FIRST_SUPERUSER`
* `FIRST_SUPERUSER_PASSWORD`
* `POSTGRES_PASSWORD` Add these repository secrets:
* `DATABASE_URL`
* `SECRET_KEY` * `SECRET_KEY`
* `LATEST_CHANGES` * `FIRST_SUPERUSER_PASSWORD`
* `SMOKESHOW_AUTH_KEY`
## GitHub Action Deployment Workflows Use the same values configured in FastAPI Cloud. For `DATABASE_URL`, use the connection URL from your database provider. The database must be reachable from GitHub-hosted runners so the preparation step can connect to it.
There are GitHub Action workflows in the `.github/workflows` directory already configured for deploying to the environments (GitHub Actions runners with the labels): The deployment workflow performs these steps:
* `staging`: after pushing (or merging) to the branch `master`. 1. Installs and builds the frontend into `backend/app/frontend`.
* `production`: after publishing a release. 2. Runs `backend/scripts/prestart.sh` to apply database migrations and create the first superuser.
3. Deploys the project with `uv run fastapi deploy`.
Both workflows are associated with their respective GitHub Environments, so deployments will be visible in the repository's **Environments** section and will respect any protection rules you configure.
If you need to add extra environments you could use those as a starting point.
## URLs ## URLs
Replace `fastapi-project.example.com` with your domain. Replace `your-app.fastapicloud.dev` with the URL of your FastAPI Cloud application.
### Main Traefik Dashboard Application (frontend and API): `https://your-app.fastapicloud.dev`
Traefik UI: `https://traefik.fastapi-project.example.com` Interactive API docs: `https://your-app.fastapicloud.dev/docs`
### Production ## Docker Compose
Application (frontend and API): `https://fastapi-project.example.com` For deployment to your own server, see the [Docker Compose deployment guide](./deployment-docker-compose.md).
Interactive API docs: `https://fastapi-project.example.com/docs` ## GitHub Repository Automation
Adminer: `https://adminer.fastapi-project.example.com` Install the following GitHub Apps to enable the included repository automation:
### Staging * [Latest Changes](https://github.com/apps/latest-changes) updates `release-notes.md` when a pull request is merged.
* [PR Push](https://github.com/apps/pr-push) lets the pre-commit workflow push automated fixes to pull request branches.
* [PR Submit](https://github.com/apps/pr-submit) lets the **Bump pre-commit hooks** and **Prepare Release** workflows create pull requests.
Application (frontend and API): `https://staging.fastapi-project.example.com` To publish code coverage with [Smokeshow](https://github.com/samuelcolvin/smokeshow), add `SMOKESHOW_AUTH_KEY` as a repository secret.
Interactive API docs: `https://staging.fastapi-project.example.com/docs`
Adminer: `https://adminer.staging.fastapi-project.example.com`
+79 -150
View File
@@ -1,14 +1,67 @@
# FastAPI Project - Development # FastAPI Project - Development
## Docker Compose ## Local Development
* Start the local stack with Docker Compose: For local development, run PostgreSQL and Mailpit with Docker Compose, and run the FastAPI and Vite development servers locally.
Start the supporting services:
```bash ```bash
docker compose up -d db mailpit
```
Then, from the `backend` directory, install the dependencies and prepare the database:
```bash
uv sync
uv run bash scripts/prestart.sh
```
Start the FastAPI development server:
```bash
uv run fastapi dev
```
In another terminal, from the project root, install the frontend dependencies and start the Vite development server:
```bash
bun install
bun run dev
```
Now you can open these URLs:
Frontend development server: <http://localhost:5173>
Backend API: <http://localhost:8000>
Automatic interactive API documentation with Swagger UI: <http://localhost:8000/docs>
Mailpit: <http://localhost:8025>
The frontend development server uses the backend at `http://localhost:8000`, as configured in `frontend/.env`.
### Frontend Served by FastAPI
Build the frontend from the `frontend` directory:
```bash
bun run build
```
The build is written to `backend/app/frontend` and served by FastAPI at <http://localhost:8000>. Rebuild the frontend after making frontend changes.
## Full Stack with Docker Compose
To run the backend and built frontend in Docker Compose:
```bash
docker compose run --rm backend bash scripts/prestart.sh
docker compose watch docker compose watch
``` ```
* Now you can open your browser and interact with these URLs: Now you can open these URLs:
Application, with the frontend and API served by FastAPI: <http://localhost:8000> Application, with the frontend and API served by FastAPI: <http://localhost:8000>
@@ -18,96 +71,25 @@ Adminer, database web administration: <http://localhost:8080>
Traefik UI, to see how the routes are being handled by the proxy: <http://localhost:8090> Traefik UI, to see how the routes are being handled by the proxy: <http://localhost:8090>
**Note**: The first time you start your stack, it might take a minute for it to be ready. While the backend waits for the database to be ready and configures everything. You can check the logs to monitor it. Mailpit: <http://localhost:8025>
To check the logs, run (in another terminal): Stop a locally running FastAPI server before starting the Compose backend because both use port `8000`.
```bash **Note**: The first time you start the stack, it might take a minute for all the services to be ready. To monitor it, use `docker compose logs`, or `docker compose logs backend` for the backend service.
docker compose logs
```
To check the logs of a specific service, add the name of the service, e.g.: ## Mailpit
```bash [Mailpit](https://mailpit.axllent.org) captures emails sent during local development instead of delivering them. The local backend connects to it at `localhost:1025`, and the Compose backend connects to the `mailpit` service. Captured emails are available at <http://localhost:8025>.
docker compose logs backend
```
## Mailcatcher ## Docker Compose Files and Environment Variables
Mailcatcher is a simple SMTP server that catches all emails sent by the backend during local development. Instead of sending real emails, they are captured and displayed in a web interface. The main `compose.yml` file contains the configuration shared by the whole stack. Docker Compose loads it automatically.
This is useful for: The `compose.override.yml` file adds local development settings, such as mounting the source code as a volume. Docker Compose also loads it automatically and applies it on top of `compose.yml`.
* Testing email functionality during development The `compose.deploy.yml` file contains the deployment-specific settings, including HTTPS and automatic certificate handling. It is explicitly combined with `compose.yml` when deploying the application.
* Verifying email content and formatting
* Debugging email-related functionality without sending real emails
The backend is automatically configured to use Mailcatcher when running with Docker Compose locally (SMTP on port 1025). All captured emails can be viewed at <http://localhost:1080>. The backend reads local settings from the `.env` file. Docker Compose also uses it for variable interpolation and passes the settings each container needs.
## Local Development
The Docker Compose files are configured so that each supporting service is available in a different port in `localhost`.
FastAPI serves the built frontend and the API as one application at `http://localhost:8000`. The API routes live under `/api`.
For frontend development with live reload, you can still run the local Vite development server separately.
Start the local frontend development server with:
```bash
bun run dev
```
Or you could stop the `backend` Docker Compose service:
```bash
docker compose stop backend
```
And then you can run the local development server for the backend:
```bash
cd backend
fastapi dev app/main.py
```
## Docker Compose in `localhost.tiangolo.com`
When you start the Docker Compose stack, it uses `localhost` by default, with different ports for each service (backend, adminer, etc).
When you deploy it to production (or staging), the application uses one domain. The frontend is served at `/` and the API lives under `/api`.
In the guide about [deployment](deployment.md) you can read about Traefik, the configured proxy. That's the component in charge of transmitting traffic to the application service based on the domain.
If you want to test that it's all working locally, you can edit the local `.env` file, and change:
```dotenv
DOMAIN=localhost.tiangolo.com
```
That will be used by the Docker Compose files to configure the base domain for the services.
Traefik will transmit application traffic at `localhost.tiangolo.com` to FastAPI, which serves both the frontend and API.
The domain `localhost.tiangolo.com` is a special domain that is configured (with all its subdomains) to point to `127.0.0.1`. This way you can use that for your local development.
After you update it, run again:
```bash
docker compose watch
```
When deploying, for example in production, the main Traefik is configured outside of the Docker Compose files. For local development, there's an included Traefik in `compose.override.yml`, just to let you test that the domain works as expected, for example with `localhost.tiangolo.com`.
## Docker Compose files and env vars
There is a main `compose.yml` file with all the configurations that apply to the whole stack, it is used automatically by `docker compose`.
And there's also a `compose.override.yml` with overrides for development, for example to mount the source code as a volume. It is used automatically by `docker compose` to apply overrides on top of `compose.yml`.
These Docker Compose files use the `.env` file containing configurations to be injected as environment variables in the containers.
They also use some additional configurations taken from environment variables set in the scripts before calling the `docker compose` command.
After changing variables, make sure you restart the stack: After changing variables, make sure you restart the stack:
@@ -115,95 +97,42 @@ After changing variables, make sure you restart the stack:
docker compose watch docker compose watch
``` ```
## The .env file ## The `.env` File
The `.env` file is the one that contains all your configurations, generated keys and passwords, etc. The tracked `.env` file contains local development defaults, passwords, and other configuration. Its hostnames use `localhost` for processes running on your machine. Docker Compose overrides hostnames such as the database and SMTP server with their Compose service names.
Depending on your workflow, you could want to exclude it from Git, for example if your project is public. In that case, you would have to make sure to set up a way for your CI tools to obtain it while building or deploying your project. Do not store deployment secrets in `.env`. Configure them as described in the [FastAPI Cloud deployment guide](./deployment.md) or the [Docker Compose deployment guide](./deployment-docker-compose.md).
One way to do it could be to add each environment variable to your CI/CD system, and updating the `compose.yml` file to read that specific env var instead of reading the `.env` file. ## Pre-commit Hooks and Code Linting
## Pre-commits and code linting The project uses [prek](https://prek.j178.dev/), a modern alternative to [pre-commit](https://pre-commit.com/), for code linting and formatting.
we are using a tool called [prek](https://prek.j178.dev/) (modern alternative to [Pre-commit](https://pre-commit.com/)) for code linting and formatting.
When you install it, it runs right before making a commit in git. This way it ensures that the code is consistent and formatted even before it is committed.
You can find a file `.pre-commit-config.yaml` with configurations at the root of the project. You can find a file `.pre-commit-config.yaml` with configurations at the root of the project.
#### Install prek to run automatically ### Install `prek` to Run Automatically
`prek` is already part of the dependencies of the project. `prek` is already part of the dependencies of the project.
After having the `prek` tool installed and available, you need to "install" it in the local repository, so that it runs automatically before each commit. From the project root, install the Git hook so that `prek` runs automatically before each commit:
Using `uv`, you could do it with (make sure you are inside `backend` folder):
```bash ```bash
❯ uv run prek install -f uv run prek install -f
prek installed at `../.git/hooks/pre-commit`
``` ```
The `-f` flag forces the installation, in case there was already a `pre-commit` hook previously installed. The `-f` flag forces the installation, in case there was already a `pre-commit` hook previously installed.
Now whenever you try to commit, e.g. with: Now whenever you try to commit, for example with:
```bash ```bash
git commit git commit
``` ```
...prek will run and check and format the code you are about to commit, and will ask you to add that code (stage it) with git again before committing. `prek` will check and format the code you are about to commit. If it modifies any files, add those files to Git again before committing.
Then you can `git add` the modified/fixed files again and now you can commit. ### Run `prek` Manually
#### Running prek hooks manually You can also run `prek` manually on all files from the project root:
you can also run `prek` manually on all the files, you can do it using `uv` with:
```bash ```bash
❯ uv run prek run --all-files uv run prek run --all-files
check for added large files..............................................Passed
check toml...............................................................Passed
check yaml...............................................................Passed
fix end of files.........................................................Passed
trim trailing whitespace.................................................Passed
ruff.....................................................................Passed
ruff-format..............................................................Passed
biome check..............................................................Passed
``` ```
## URLs
The production or staging URLs would use these same paths, but with your own domain.
### Development URLs
Development URLs, for local development.
Application: <http://localhost:8000>
Automatic Interactive Docs (Swagger UI): <http://localhost:8000/docs>
Automatic Alternative Docs (ReDoc): <http://localhost:8000/redoc>
Adminer: <http://localhost:8080>
Traefik UI: <http://localhost:8090>
MailCatcher: <http://localhost:1080>
### Development URLs with `localhost.tiangolo.com` Configured
Development URLs, for local development.
Application: <http://localhost.tiangolo.com>
Automatic Interactive Docs (Swagger UI): <http://localhost.tiangolo.com/docs>
Automatic Alternative Docs (ReDoc): <http://localhost.tiangolo.com/redoc>
Adminer: <http://localhost.tiangolo.com:8080>
Traefik UI: <http://localhost.tiangolo.com:8090>
MailCatcher: <http://localhost.tiangolo.com:1080>
+1 -1
View File
@@ -1,2 +1,2 @@
VITE_API_URL=http://localhost:8000 VITE_API_URL=http://localhost:8000
MAILCATCHER_HOST=http://localhost:1080 MAILPIT_HOST=http://localhost:8025
+1 -1
View File
@@ -1,4 +1,4 @@
FROM mcr.microsoft.com/playwright:v1.61.1-noble FROM mcr.microsoft.com/playwright:v1.64.1-noble
WORKDIR /app WORKDIR /app
+32 -31
View File
@@ -1,74 +1,73 @@
# FastAPI Project - Frontend # FastAPI Project - Frontend
The frontend is built with [Vite](https://vitejs.dev/), [React](https://reactjs.org/), [TypeScript](https://www.typescriptlang.org/), [TanStack Query](https://tanstack.com/query), [TanStack Router](https://tanstack.com/router) and [Tailwind CSS](https://tailwindcss.com/). The frontend is built with [Vite](https://vitejs.dev/), [React](https://react.dev/), [TypeScript](https://www.typescriptlang.org/), [TanStack Query](https://tanstack.com/query), [TanStack Router](https://tanstack.com/router), [Tailwind CSS](https://tailwindcss.com/), and [shadcn/ui](https://ui.shadcn.com/).
## Requirements ## Requirements
- [Bun](https://bun.sh/) (recommended) or [Node.js](https://nodejs.org/) - [Bun](https://bun.sh/)
## Quick Start ## Quick Start
From the project root, install the dependencies and start the frontend development server:
```bash ```bash
bun install bun install
bun run dev bun run dev
``` ```
* Then open your browser at http://localhost:5173/. Then open <http://localhost:5173/> in your browser.
Notice that this live server is not running inside Docker, it's for local development, and that is the recommended workflow. Once you are happy with your frontend, you can build the backend Docker image and start it, to test the production-like setup where FastAPI serves the built frontend at `http://localhost:8000`. Run `uv run bash scripts/prestart.sh` and `uv run fastapi dev` from the `backend` directory, with PostgreSQL running in Docker Compose. See [../development.md](../development.md) for the complete setup.
Check the file `package.json` to see other available options. To serve the frontend with FastAPI, run `bun run build` from the `frontend` directory and open `http://localhost:8000`.
### Removing the frontend Check `frontend/package.json` to see the other available commands.
## Removing the Frontend
If you are developing an API-only app and want to remove the frontend, you can do it easily: If you are developing an API-only app and want to remove the frontend, you can do it easily:
* Remove the `./frontend` directory. - Remove the `./frontend` directory.
* In the `backend/app/main.py` file, remove the `app.frontend()` call. - In the `backend/app/main.py` file, remove the `app.frontend()` call.
* In the `backend/Dockerfile` file, remove the frontend build stage and the `COPY --from=frontend-build` instruction. - In the `backend/Dockerfile` file, remove the frontend build stage and the `COPY --from=frontend-build` instruction.
* In the `compose.override.yml` file, remove the `playwright` service. - In the `compose.override.yml` file, remove the `playwright` service.
Done, you have a frontend-less (api-only) app. 🤓 - In the `.github/workflows/deploy.yml` file, remove the **Set up Bun**, **Install frontend dependencies**, and **Build frontend** steps.
--- - In the `.fastapicloudignore` file, remove the `!backend/app/frontend/` entry.
If you want, you can also remove the `FRONTEND_HOST` environment variable from: Done, you now have an API-only app. 🤓
* `.env`
But it would be only to clean them up, leaving them won't really have any effect either way.
## Generate Client ## Generate Client
### Automatically ### Automatically
* Activate the backend virtual environment. - From the project root, run the script:
* From the top level project directory, run the script:
```bash ```bash
bash ./scripts/generate-client.sh bash ./scripts/generate-client.sh
``` ```
* Commit the changes. - Commit the changes.
### Manually ### Manually
* Start the Docker Compose stack. - Make sure the backend is running.
* Download the OpenAPI JSON file from `http://localhost/api/v1/openapi.json` and copy it to a new file `openapi.json` at the root of the `frontend` directory. - Download the OpenAPI JSON file from `http://localhost:8000/api/v1/openapi.json` and copy it to a new file `openapi.json` at the root of the `frontend` directory.
* To generate the frontend client, run: - To generate the frontend client, run:
```bash ```bash
bun run generate-client bun run generate-client
``` ```
* Commit the changes. - Commit the changes.
Notice that everytime the backend changes (changing the OpenAPI schema), you should follow these steps again to update the frontend client. Regenerate the client whenever backend changes affect the OpenAPI schema.
## Using a Remote API ## Using a Remote API
@@ -84,18 +83,20 @@ Then, when you run the frontend, it will use that URL as the base URL for the AP
The frontend code is structured as follows: The frontend code is structured as follows:
* `frontend/src` - The main frontend code. - `frontend/src` - The main frontend code.
* `frontend/src/assets` - Static assets. - `frontend/public` - Static assets.
* `frontend/src/client` - The generated OpenAPI client. - `frontend/src/client` - The generated OpenAPI client.
* `frontend/src/components` - The different components of the frontend. - `frontend/src/components` - The components of the frontend, including the shadcn/ui components in `frontend/src/components/ui`.
* `frontend/src/hooks` - Custom hooks. - `frontend/src/hooks` - Custom hooks.
* `frontend/src/routes` - The different routes of the frontend which include the pages. - `frontend/src/lib` - Shared frontend utilities.
- `frontend/src/routes` - The frontend routes and pages.
## End-to-End Testing with Playwright ## End-to-End Testing with Playwright
The frontend includes initial end-to-end tests using Playwright. To run the tests, you need to have the Docker Compose stack running. Start the stack with the following command: The frontend includes initial end-to-end tests using Playwright. To run the tests, you need to have the Docker Compose stack running. Start the stack with the following command:
```bash ```bash
docker compose run --rm backend bash scripts/prestart.sh
docker compose up -d --wait backend docker compose up -d --wait backend
``` ```
+15 -3
View File
@@ -1,5 +1,5 @@
{ {
"$schema": "https://biomejs.dev/schemas/2.3.14/schema.json", "$schema": "https://biomejs.dev/schemas/2.6.3/schema.json",
"assist": { "actions": { "source": { "organizeImports": "on" } } }, "assist": { "actions": { "source": { "organizeImports": "on" } } },
"files": { "files": {
"includes": [ "includes": [
@@ -16,7 +16,7 @@
"linter": { "linter": {
"enabled": true, "enabled": true,
"rules": { "rules": {
"recommended": true, "preset": "recommended",
"suspicious": { "suspicious": {
"noExplicitAny": "off", "noExplicitAny": "off",
"noArrayIndexKey": "off" "noArrayIndexKey": "off"
@@ -42,5 +42,17 @@
"parser": { "parser": {
"tailwindDirectives": true "tailwindDirectives": true
} }
} },
"overrides": [
{
"includes": ["**/*.svg"],
"linter": {
"rules": {
"a11y": {
"noSvgWithoutTitle": "off"
}
}
}
}
]
} }
+39 -40
View File
@@ -13,54 +13,53 @@
"test:ui": "bunx playwright test --ui" "test:ui": "bunx playwright test --ui"
}, },
"dependencies": { "dependencies": {
"@hookform/resolvers": "^5.4.0", "@hookform/resolvers": "^6.0.1",
"@radix-ui/react-avatar": "^1.1.12", "@radix-ui/react-avatar": "^1.3.1",
"@radix-ui/react-checkbox": "^1.3.4", "@radix-ui/react-checkbox": "^1.4.1",
"@radix-ui/react-dialog": "^1.1.16", "@radix-ui/react-dialog": "^1.3.1",
"@radix-ui/react-dropdown-menu": "^2.1.17", "@radix-ui/react-dropdown-menu": "^2.2.1",
"@radix-ui/react-label": "^2.1.9", "@radix-ui/react-label": "^2.1.17",
"@radix-ui/react-radio-group": "^1.4.0", "@radix-ui/react-radio-group": "^1.5.1",
"@radix-ui/react-scroll-area": "^1.2.11", "@radix-ui/react-scroll-area": "^1.4.0",
"@radix-ui/react-select": "^2.3.0", "@radix-ui/react-select": "^2.4.1",
"@radix-ui/react-separator": "^1.1.9", "@radix-ui/react-separator": "^1.2.0",
"@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-tabs": "^1.1.14", "@radix-ui/react-tabs": "^1.2.1",
"@radix-ui/react-tooltip": "^1.2.9", "@radix-ui/react-tooltip": "^1.4.1",
"@tailwindcss/vite": "^4.3.0", "@tailwindcss/vite": "^4.3.3",
"@tanstack/react-query": "^5.101.0", "@tanstack/react-query": "^5.105.0",
"@tanstack/react-query-devtools": "^5.101.0", "@tanstack/react-query-devtools": "^5.105.0",
"@tanstack/react-router": "^1.170.15", "@tanstack/react-router": "^1.170.46",
"@tanstack/react-router-devtools": "^1.167.0", "@tanstack/react-router-devtools": "^1.167.2",
"@tanstack/react-table": "^8.21.3", "@tanstack/react-table": "^9.3.1",
"axios": "1.18.0", "axios": "1.20.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"form-data": "4.0.6", "form-data": "4.0.6",
"lucide-react": "^1.17.0", "lucide-react": "^1.62.0",
"next-themes": "^0.4.6", "react": "^19.3.1",
"react": "^19.2.7", "react-dom": "^19.3.1",
"react-dom": "^19.2.7", "react-error-boundary": "^6.1.6",
"react-error-boundary": "^6.1.2", "react-hook-form": "^7.91.0",
"react-hook-form": "^7.78.0", "react-icons": "^5.7.0",
"react-icons": "^5.6.0", "sonner": "^2.0.8",
"sonner": "^2.0.7", "tailwind-merge": "^3.7.0",
"tailwind-merge": "^3.6.0",
"tailwindcss": "^4.2.1", "tailwindcss": "^4.2.1",
"zod": "^4.4.3" "zod": "^4.6.7"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^2.4.16", "@biomejs/biome": "^2.6.3",
"@hey-api/openapi-ts": "0.97.3", "@hey-api/openapi-ts": "0.97.3",
"@playwright/test": "1.61.1", "@playwright/test": "1.64.1",
"@tanstack/router-devtools": "^1.167.0", "@tanstack/router-devtools": "^1.167.2",
"@tanstack/router-plugin": "^1.168.18", "@tanstack/router-plugin": "^1.168.48",
"@types/node": "^25.9.2", "@types/node": "^26.6.5",
"@types/react": "^19.2.17", "@types/react": "^19.3.0",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.3.0",
"@vitejs/plugin-react-swc": "^4.3.1", "@vitejs/plugin-react-swc": "^4.3.3",
"dotenv": "^17.4.2", "dotenv": "^18.0.9",
"tw-animate-css": "^1.4.0", "tw-animate-css": "^1.4.0",
"typescript": "^5.9.3", "typescript": "^6.0.3",
"vite": "^8.0.16" "vite": "^8.3.5"
} }
} }
@@ -66,7 +66,7 @@
style="fill:#ffffff;fill-opacity:0.980392;stroke:none;stroke-width:0.136325;stop-color:#000000" style="fill:#ffffff;fill-opacity:0.980392;stroke:none;stroke-width:0.136325;stop-color:#000000"
d="M 32.092357,-1.166849e-4 A 32.092354,31.988569 0 0 0 -2.5752734e-4,31.988628 32.092354,31.988569 0 0 0 32.092357,63.977374 32.092354,31.988569 0 0 0 64.184455,31.988628 32.092354,31.988569 0 0 0 32.092357,-1.166849e-4 Z M 29.878022,14.484271 H 50.070588 L 29.794823,28.73353 H 43.654442 L 14.114126,49.49247 20.530789,35.243727 23.462393,28.73353 Z" /> d="M 32.092357,-1.166849e-4 A 32.092354,31.988569 0 0 0 -2.5752734e-4,31.988628 32.092354,31.988569 0 0 0 32.092357,63.977374 32.092354,31.988569 0 0 0 64.184455,31.988628 32.092354,31.988569 0 0 0 32.092357,-1.166849e-4 Z M 29.878022,14.484271 H 50.070588 L 29.794823,28.73353 H 43.654442 L 14.114126,49.49247 20.530789,35.243727 23.462393,28.73353 Z" />
<path <path
style="font-size:79.7151px;line-height:1.25;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;letter-spacing:0px;word-spacing:0px;fill:#ffffff;stroke-width:1.99288" style="fill:#ffffff;stroke-width:1.99288"
d="M 89.762163,59.410606 V 4.1680399 H 121.88735 V 9.5089518 H 95.979941 V 28.082571 h 22.957949 v 5.340912 H 95.979941 v 25.987123 z m 51.017587,0.876867 q -4.46405,0 -7.97152,-1.275442 -3.50746,-1.275442 -5.50034,-4.145185 -1.99288,-2.869744 -1.99288,-7.572935 0,-4.543761 2.23203,-7.33379 2.23202,-2.869743 6.13806,-4.145185 3.98576,-1.275442 8.92809,-1.275442 2.23203,0 4.70319,0.398576 2.47117,0.398575 3.10889,0.717436 v -2.391453 q 0,-2.710314 -0.71743,-5.181482 -0.63772,-2.550883 -2.71032,-4.145186 -2.07259,-1.594302 -6.29749,-1.594302 -4.38433,0 -6.69607,0.637721 -2.31174,0.637721 -3.42775,1.036297 l -0.79715,-5.101767 q 1.43487,-0.637721 4.38433,-1.195727 2.94946,-0.558005 6.93522,-0.558005 5.65977,0 8.92809,1.992877 3.34803,1.992878 4.7829,5.500342 1.51459,3.42775 1.51459,7.891796 v 25.907408 q -1.67402,0.398576 -5.97863,1.116012 -4.30462,0.717436 -9.56581,0.717436 z m 0.87686,-5.101767 q 2.79003,0 5.02205,-0.15943 2.23203,-0.239146 3.74661,-0.558006 V 40.597842 q -0.79715,-0.398575 -2.71031,-0.797151 -1.91316,-0.398576 -4.94234,-0.398576 -2.55088,0 -5.18148,0.558006 -2.6306,0.558006 -4.46404,2.232023 -1.75374,1.674017 -1.75374,5.022052 0,4.464045 2.79003,6.217778 2.79003,1.753732 7.49322,1.753732 z m 36.4298,5.181482 q -5.34092,0 -8.37009,-0.956582 -2.94946,-0.876866 -3.98575,-1.355156 l 1.43487,-5.261197 q 0.87686,0.31886 3.58718,1.355157 2.71031,1.036296 7.33379,1.036296 4.38433,0 7.09464,-1.355157 2.71031,-1.355157 2.71031,-4.703191 0,-2.152308 -0.95658,-3.427749 -0.95658,-1.355157 -3.10889,-2.471169 -2.07259,-1.116011 -5.73948,-2.550883 -3.10889,-1.275442 -5.73949,-2.710313 -2.6306,-1.434872 -4.30462,-3.666895 -1.5943,-2.232023 -1.5943,-5.739488 0,-3.427749 1.75373,-5.978632 1.75374,-2.550884 4.94234,-3.985755 3.26832,-1.434872 7.73237,-1.434872 4.14518,0 7.01492,0.717436 2.86975,0.717436 4.06547,1.275441 l -1.35515,5.181482 q -1.0363,-0.558006 -3.34804,-1.275442 -2.31173,-0.797151 -6.53663,-0.797151 -3.34804,0 -5.81921,1.434872 -2.47117,1.355157 -2.47117,4.384331 0,2.152308 1.0363,3.507464 1.0363,1.275442 3.10889,2.311738 2.07259,1.036297 5.10177,2.232023 3.42775,1.355157 6.13806,2.869744 2.79003,1.434872 4.46405,3.74661 1.67401,2.311738 1.67401,6.138063 0,3.74661 -1.91316,6.377208 -1.91316,2.550883 -5.50034,3.826325 -3.50747,1.275442 -8.4498,1.275442 z m 39.21967,-0.07972 q -5.2612,0 -8.29037,-1.833448 -3.02917,-1.833447 -4.30461,-5.500342 -1.19573,-3.666895 -1.19573,-9.167237 V 6.1609175 L 209.494,5.1246211 V 18.118183 h 16.18217 v 5.022051 H 209.494 v 21.124503 q 0,4.38433 0.95658,6.696068 1.0363,2.311738 2.86975,3.188605 1.91316,0.797151 4.46404,0.797151 3.02918,0 5.02205,-0.717436 2.0726,-0.717436 3.26832,-1.275442 l 1.27544,4.862621 q -1.19572,0.717436 -3.98575,1.594302 -2.71031,0.876867 -6.05835,0.876867 z m 11.95723,-0.876867 q 4.30462,-11.55869 7.8918,-21.044787 3.58718,-9.486097 7.01493,-17.776468 3.50746,-8.370086 7.4135,-16.4213111 h 5.58006 q 2.86974,6.0583481 5.50034,12.2761261 2.71032,6.138063 5.34091,12.754416 2.6306,6.616354 5.42063,14.109574 2.86975,7.413504 6.05835,16.10245 h -6.77578 q -1.43488,-3.985755 -2.79003,-7.572934 -1.35516,-3.666895 -2.55089,-7.17436 h -26.30598 q -1.27544,3.507465 -2.6306,7.17436 -1.35516,3.587179 -2.71031,7.572934 z M 242.8946,39.402115 h 22.63909 q -1.51459,-3.985755 -2.94946,-7.732365 -1.43487,-3.746609 -2.86975,-7.254074 -1.35515,-3.507464 -2.79002,-6.775784 -1.35516,-3.348034 -2.79003,-6.536638 -1.35516,3.188604 -2.79003,6.536638 -1.35516,3.26832 -2.79003,6.775784 -1.35516,3.507465 -2.79003,7.254074 -1.43487,3.74661 -2.86974,7.732365 z m 44.481,20.008491 V 5.2043362 q 3.02917,-0.797151 6.93521,-1.1160114 3.98576,-0.3985755 7.33379,-0.3985755 11.71812,0 17.53732,4.5437609 5.89892,4.4640458 5.89892,12.7544168 0,6.297493 -2.94946,10.123818 -2.86974,3.826325 -8.37008,5.580057 -5.42063,1.674017 -13.07328,1.674017 h -7.09465 v 21.044787 z m 6.21777,-26.385699 h 6.53664 q 5.65978,0 9.80496,-0.956581 4.14519,-1.036296 6.37721,-3.666895 2.31174,-2.630598 2.31174,-7.493219 0,-4.703192 -2.39146,-7.254075 -2.39145,-2.550883 -6.21777,-3.587179 -3.82633,-1.0362968 -8.13094,-1.0362968 -2.79003,0 -4.86263,0.2391453 -1.99287,0.1594302 -3.42775,0.3188604 z M 335.0452,59.410606 V 4.1680399 h 6.21778 V 59.410606 Z" d="M 89.762163,59.410606 V 4.1680399 H 121.88735 V 9.5089518 H 95.979941 V 28.082571 h 22.957949 v 5.340912 H 95.979941 v 25.987123 z m 51.017587,0.876867 q -4.46405,0 -7.97152,-1.275442 -3.50746,-1.275442 -5.50034,-4.145185 -1.99288,-2.869744 -1.99288,-7.572935 0,-4.543761 2.23203,-7.33379 2.23202,-2.869743 6.13806,-4.145185 3.98576,-1.275442 8.92809,-1.275442 2.23203,0 4.70319,0.398576 2.47117,0.398575 3.10889,0.717436 v -2.391453 q 0,-2.710314 -0.71743,-5.181482 -0.63772,-2.550883 -2.71032,-4.145186 -2.07259,-1.594302 -6.29749,-1.594302 -4.38433,0 -6.69607,0.637721 -2.31174,0.637721 -3.42775,1.036297 l -0.79715,-5.101767 q 1.43487,-0.637721 4.38433,-1.195727 2.94946,-0.558005 6.93522,-0.558005 5.65977,0 8.92809,1.992877 3.34803,1.992878 4.7829,5.500342 1.51459,3.42775 1.51459,7.891796 v 25.907408 q -1.67402,0.398576 -5.97863,1.116012 -4.30462,0.717436 -9.56581,0.717436 z m 0.87686,-5.101767 q 2.79003,0 5.02205,-0.15943 2.23203,-0.239146 3.74661,-0.558006 V 40.597842 q -0.79715,-0.398575 -2.71031,-0.797151 -1.91316,-0.398576 -4.94234,-0.398576 -2.55088,0 -5.18148,0.558006 -2.6306,0.558006 -4.46404,2.232023 -1.75374,1.674017 -1.75374,5.022052 0,4.464045 2.79003,6.217778 2.79003,1.753732 7.49322,1.753732 z m 36.4298,5.181482 q -5.34092,0 -8.37009,-0.956582 -2.94946,-0.876866 -3.98575,-1.355156 l 1.43487,-5.261197 q 0.87686,0.31886 3.58718,1.355157 2.71031,1.036296 7.33379,1.036296 4.38433,0 7.09464,-1.355157 2.71031,-1.355157 2.71031,-4.703191 0,-2.152308 -0.95658,-3.427749 -0.95658,-1.355157 -3.10889,-2.471169 -2.07259,-1.116011 -5.73948,-2.550883 -3.10889,-1.275442 -5.73949,-2.710313 -2.6306,-1.434872 -4.30462,-3.666895 -1.5943,-2.232023 -1.5943,-5.739488 0,-3.427749 1.75373,-5.978632 1.75374,-2.550884 4.94234,-3.985755 3.26832,-1.434872 7.73237,-1.434872 4.14518,0 7.01492,0.717436 2.86975,0.717436 4.06547,1.275441 l -1.35515,5.181482 q -1.0363,-0.558006 -3.34804,-1.275442 -2.31173,-0.797151 -6.53663,-0.797151 -3.34804,0 -5.81921,1.434872 -2.47117,1.355157 -2.47117,4.384331 0,2.152308 1.0363,3.507464 1.0363,1.275442 3.10889,2.311738 2.07259,1.036297 5.10177,2.232023 3.42775,1.355157 6.13806,2.869744 2.79003,1.434872 4.46405,3.74661 1.67401,2.311738 1.67401,6.138063 0,3.74661 -1.91316,6.377208 -1.91316,2.550883 -5.50034,3.826325 -3.50747,1.275442 -8.4498,1.275442 z m 39.21967,-0.07972 q -5.2612,0 -8.29037,-1.833448 -3.02917,-1.833447 -4.30461,-5.500342 -1.19573,-3.666895 -1.19573,-9.167237 V 6.1609175 L 209.494,5.1246211 V 18.118183 h 16.18217 v 5.022051 H 209.494 v 21.124503 q 0,4.38433 0.95658,6.696068 1.0363,2.311738 2.86975,3.188605 1.91316,0.797151 4.46404,0.797151 3.02918,0 5.02205,-0.717436 2.0726,-0.717436 3.26832,-1.275442 l 1.27544,4.862621 q -1.19572,0.717436 -3.98575,1.594302 -2.71031,0.876867 -6.05835,0.876867 z m 11.95723,-0.876867 q 4.30462,-11.55869 7.8918,-21.044787 3.58718,-9.486097 7.01493,-17.776468 3.50746,-8.370086 7.4135,-16.4213111 h 5.58006 q 2.86974,6.0583481 5.50034,12.2761261 2.71032,6.138063 5.34091,12.754416 2.6306,6.616354 5.42063,14.109574 2.86975,7.413504 6.05835,16.10245 h -6.77578 q -1.43488,-3.985755 -2.79003,-7.572934 -1.35516,-3.666895 -2.55089,-7.17436 h -26.30598 q -1.27544,3.507465 -2.6306,7.17436 -1.35516,3.587179 -2.71031,7.572934 z M 242.8946,39.402115 h 22.63909 q -1.51459,-3.985755 -2.94946,-7.732365 -1.43487,-3.746609 -2.86975,-7.254074 -1.35515,-3.507464 -2.79002,-6.775784 -1.35516,-3.348034 -2.79003,-6.536638 -1.35516,3.188604 -2.79003,6.536638 -1.35516,3.26832 -2.79003,6.775784 -1.35516,3.507465 -2.79003,7.254074 -1.43487,3.74661 -2.86974,7.732365 z m 44.481,20.008491 V 5.2043362 q 3.02917,-0.797151 6.93521,-1.1160114 3.98576,-0.3985755 7.33379,-0.3985755 11.71812,0 17.53732,4.5437609 5.89892,4.4640458 5.89892,12.7544168 0,6.297493 -2.94946,10.123818 -2.86974,3.826325 -8.37008,5.580057 -5.42063,1.674017 -13.07328,1.674017 h -7.09465 v 21.044787 z m 6.21777,-26.385699 h 6.53664 q 5.65978,0 9.80496,-0.956581 4.14519,-1.036296 6.37721,-3.666895 2.31174,-2.630598 2.31174,-7.493219 0,-4.703192 -2.39146,-7.254075 -2.39145,-2.550883 -6.21777,-3.587179 -3.82633,-1.0362968 -8.13094,-1.0362968 -2.79003,0 -4.86263,0.2391453 -1.99287,0.1594302 -3.42775,0.3188604 z M 335.0452,59.410606 V 4.1680399 h 6.21778 V 59.410606 Z"
id="text979-3" id="text979-3"
aria-label="FastAPI" /> aria-label="FastAPI" />

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

@@ -74,7 +74,7 @@
d="m 962.2685,-187.40837 -6.64403,14.80375 -3.03599,6.76393 -6.64456,14.80375 30.59142,-21.56768 h -14.35312 l 20.99715,-14.80375 z" /> d="m 962.2685,-187.40837 -6.64403,14.80375 -3.03599,6.76393 -6.64456,14.80375 30.59142,-21.56768 h -14.35312 l 20.99715,-14.80375 z" />
</g> </g>
<path <path
style="font-size:79.7151px;line-height:1.25;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;letter-spacing:0px;word-spacing:0px;fill:#009688;stroke-width:1.99288" style="fill:#009688;stroke-width:1.99288"
d="M 89.762163,59.410606 V 4.1680399 H 121.88735 V 9.5089518 H 95.979941 V 28.082571 h 22.957949 v 5.340912 H 95.979941 v 25.987123 z m 51.017587,0.876867 q -4.46405,0 -7.97152,-1.275442 -3.50746,-1.275442 -5.50034,-4.145185 -1.99288,-2.869744 -1.99288,-7.572935 0,-4.543761 2.23203,-7.33379 2.23202,-2.869743 6.13806,-4.145185 3.98576,-1.275442 8.92809,-1.275442 2.23203,0 4.70319,0.398576 2.47117,0.398575 3.10889,0.717436 v -2.391453 q 0,-2.710314 -0.71743,-5.181482 -0.63772,-2.550883 -2.71032,-4.145186 -2.07259,-1.594302 -6.29749,-1.594302 -4.38433,0 -6.69607,0.637721 -2.31174,0.637721 -3.42775,1.036297 l -0.79715,-5.101767 q 1.43487,-0.637721 4.38433,-1.195727 2.94946,-0.558005 6.93522,-0.558005 5.65977,0 8.92809,1.992877 3.34803,1.992878 4.7829,5.500342 1.51459,3.42775 1.51459,7.891796 v 25.907408 q -1.67402,0.398576 -5.97863,1.116012 -4.30462,0.717436 -9.56581,0.717436 z m 0.87686,-5.101767 q 2.79003,0 5.02205,-0.15943 2.23203,-0.239146 3.74661,-0.558006 V 40.597842 q -0.79715,-0.398575 -2.71031,-0.797151 -1.91316,-0.398576 -4.94234,-0.398576 -2.55088,0 -5.18148,0.558006 -2.6306,0.558006 -4.46404,2.232023 -1.75374,1.674017 -1.75374,5.022052 0,4.464045 2.79003,6.217778 2.79003,1.753732 7.49322,1.753732 z m 36.4298,5.181482 q -5.34092,0 -8.37009,-0.956582 -2.94946,-0.876866 -3.98575,-1.355156 l 1.43487,-5.261197 q 0.87686,0.31886 3.58718,1.355157 2.71031,1.036296 7.33379,1.036296 4.38433,0 7.09464,-1.355157 2.71031,-1.355157 2.71031,-4.703191 0,-2.152308 -0.95658,-3.427749 -0.95658,-1.355157 -3.10889,-2.471169 -2.07259,-1.116011 -5.73948,-2.550883 -3.10889,-1.275442 -5.73949,-2.710313 -2.6306,-1.434872 -4.30462,-3.666895 -1.5943,-2.232023 -1.5943,-5.739488 0,-3.427749 1.75373,-5.978632 1.75374,-2.550884 4.94234,-3.985755 3.26832,-1.434872 7.73237,-1.434872 4.14518,0 7.01492,0.717436 2.86975,0.717436 4.06547,1.275441 l -1.35515,5.181482 q -1.0363,-0.558006 -3.34804,-1.275442 -2.31173,-0.797151 -6.53663,-0.797151 -3.34804,0 -5.81921,1.434872 -2.47117,1.355157 -2.47117,4.384331 0,2.152308 1.0363,3.507464 1.0363,1.275442 3.10889,2.311738 2.07259,1.036297 5.10177,2.232023 3.42775,1.355157 6.13806,2.869744 2.79003,1.434872 4.46405,3.74661 1.67401,2.311738 1.67401,6.138063 0,3.74661 -1.91316,6.377208 -1.91316,2.550883 -5.50034,3.826325 -3.50747,1.275442 -8.4498,1.275442 z m 39.21967,-0.07972 q -5.2612,0 -8.29037,-1.833448 -3.02917,-1.833447 -4.30461,-5.500342 -1.19573,-3.666895 -1.19573,-9.167237 V 6.1609175 L 209.494,5.1246211 V 18.118183 h 16.18217 v 5.022051 H 209.494 v 21.124503 q 0,4.38433 0.95658,6.696068 1.0363,2.311738 2.86975,3.188605 1.91316,0.797151 4.46404,0.797151 3.02918,0 5.02205,-0.717436 2.0726,-0.717436 3.26832,-1.275442 l 1.27544,4.862621 q -1.19572,0.717436 -3.98575,1.594302 -2.71031,0.876867 -6.05835,0.876867 z m 11.95723,-0.876867 q 4.30462,-11.55869 7.8918,-21.044787 3.58718,-9.486097 7.01493,-17.776468 3.50746,-8.370086 7.4135,-16.4213111 h 5.58006 q 2.86974,6.0583481 5.50034,12.2761261 2.71032,6.138063 5.34091,12.754416 2.6306,6.616354 5.42063,14.109574 2.86975,7.413504 6.05835,16.10245 h -6.77578 q -1.43488,-3.985755 -2.79003,-7.572934 -1.35516,-3.666895 -2.55089,-7.17436 h -26.30598 q -1.27544,3.507465 -2.6306,7.17436 -1.35516,3.587179 -2.71031,7.572934 z M 242.8946,39.402115 h 22.63909 q -1.51459,-3.985755 -2.94946,-7.732365 -1.43487,-3.746609 -2.86975,-7.254074 -1.35515,-3.507464 -2.79002,-6.775784 -1.35516,-3.348034 -2.79003,-6.536638 -1.35516,3.188604 -2.79003,6.536638 -1.35516,3.26832 -2.79003,6.775784 -1.35516,3.507465 -2.79003,7.254074 -1.43487,3.74661 -2.86974,7.732365 z m 44.481,20.008491 V 5.2043362 q 3.02917,-0.797151 6.93521,-1.1160114 3.98576,-0.3985755 7.33379,-0.3985755 11.71812,0 17.53732,4.5437609 5.89892,4.4640458 5.89892,12.7544168 0,6.297493 -2.94946,10.123818 -2.86974,3.826325 -8.37008,5.580057 -5.42063,1.674017 -13.07328,1.674017 h -7.09465 v 21.044787 z m 6.21777,-26.385699 h 6.53664 q 5.65978,0 9.80496,-0.956581 4.14519,-1.036296 6.37721,-3.666895 2.31174,-2.630598 2.31174,-7.493219 0,-4.703192 -2.39146,-7.254075 -2.39145,-2.550883 -6.21777,-3.587179 -3.82633,-1.0362968 -8.13094,-1.0362968 -2.79003,0 -4.86263,0.2391453 -1.99287,0.1594302 -3.42775,0.3188604 z M 335.0452,59.410606 V 4.1680399 h 6.21778 V 59.410606 Z" d="M 89.762163,59.410606 V 4.1680399 H 121.88735 V 9.5089518 H 95.979941 V 28.082571 h 22.957949 v 5.340912 H 95.979941 v 25.987123 z m 51.017587,0.876867 q -4.46405,0 -7.97152,-1.275442 -3.50746,-1.275442 -5.50034,-4.145185 -1.99288,-2.869744 -1.99288,-7.572935 0,-4.543761 2.23203,-7.33379 2.23202,-2.869743 6.13806,-4.145185 3.98576,-1.275442 8.92809,-1.275442 2.23203,0 4.70319,0.398576 2.47117,0.398575 3.10889,0.717436 v -2.391453 q 0,-2.710314 -0.71743,-5.181482 -0.63772,-2.550883 -2.71032,-4.145186 -2.07259,-1.594302 -6.29749,-1.594302 -4.38433,0 -6.69607,0.637721 -2.31174,0.637721 -3.42775,1.036297 l -0.79715,-5.101767 q 1.43487,-0.637721 4.38433,-1.195727 2.94946,-0.558005 6.93522,-0.558005 5.65977,0 8.92809,1.992877 3.34803,1.992878 4.7829,5.500342 1.51459,3.42775 1.51459,7.891796 v 25.907408 q -1.67402,0.398576 -5.97863,1.116012 -4.30462,0.717436 -9.56581,0.717436 z m 0.87686,-5.101767 q 2.79003,0 5.02205,-0.15943 2.23203,-0.239146 3.74661,-0.558006 V 40.597842 q -0.79715,-0.398575 -2.71031,-0.797151 -1.91316,-0.398576 -4.94234,-0.398576 -2.55088,0 -5.18148,0.558006 -2.6306,0.558006 -4.46404,2.232023 -1.75374,1.674017 -1.75374,5.022052 0,4.464045 2.79003,6.217778 2.79003,1.753732 7.49322,1.753732 z m 36.4298,5.181482 q -5.34092,0 -8.37009,-0.956582 -2.94946,-0.876866 -3.98575,-1.355156 l 1.43487,-5.261197 q 0.87686,0.31886 3.58718,1.355157 2.71031,1.036296 7.33379,1.036296 4.38433,0 7.09464,-1.355157 2.71031,-1.355157 2.71031,-4.703191 0,-2.152308 -0.95658,-3.427749 -0.95658,-1.355157 -3.10889,-2.471169 -2.07259,-1.116011 -5.73948,-2.550883 -3.10889,-1.275442 -5.73949,-2.710313 -2.6306,-1.434872 -4.30462,-3.666895 -1.5943,-2.232023 -1.5943,-5.739488 0,-3.427749 1.75373,-5.978632 1.75374,-2.550884 4.94234,-3.985755 3.26832,-1.434872 7.73237,-1.434872 4.14518,0 7.01492,0.717436 2.86975,0.717436 4.06547,1.275441 l -1.35515,5.181482 q -1.0363,-0.558006 -3.34804,-1.275442 -2.31173,-0.797151 -6.53663,-0.797151 -3.34804,0 -5.81921,1.434872 -2.47117,1.355157 -2.47117,4.384331 0,2.152308 1.0363,3.507464 1.0363,1.275442 3.10889,2.311738 2.07259,1.036297 5.10177,2.232023 3.42775,1.355157 6.13806,2.869744 2.79003,1.434872 4.46405,3.74661 1.67401,2.311738 1.67401,6.138063 0,3.74661 -1.91316,6.377208 -1.91316,2.550883 -5.50034,3.826325 -3.50747,1.275442 -8.4498,1.275442 z m 39.21967,-0.07972 q -5.2612,0 -8.29037,-1.833448 -3.02917,-1.833447 -4.30461,-5.500342 -1.19573,-3.666895 -1.19573,-9.167237 V 6.1609175 L 209.494,5.1246211 V 18.118183 h 16.18217 v 5.022051 H 209.494 v 21.124503 q 0,4.38433 0.95658,6.696068 1.0363,2.311738 2.86975,3.188605 1.91316,0.797151 4.46404,0.797151 3.02918,0 5.02205,-0.717436 2.0726,-0.717436 3.26832,-1.275442 l 1.27544,4.862621 q -1.19572,0.717436 -3.98575,1.594302 -2.71031,0.876867 -6.05835,0.876867 z m 11.95723,-0.876867 q 4.30462,-11.55869 7.8918,-21.044787 3.58718,-9.486097 7.01493,-17.776468 3.50746,-8.370086 7.4135,-16.4213111 h 5.58006 q 2.86974,6.0583481 5.50034,12.2761261 2.71032,6.138063 5.34091,12.754416 2.6306,6.616354 5.42063,14.109574 2.86975,7.413504 6.05835,16.10245 h -6.77578 q -1.43488,-3.985755 -2.79003,-7.572934 -1.35516,-3.666895 -2.55089,-7.17436 h -26.30598 q -1.27544,3.507465 -2.6306,7.17436 -1.35516,3.587179 -2.71031,7.572934 z M 242.8946,39.402115 h 22.63909 q -1.51459,-3.985755 -2.94946,-7.732365 -1.43487,-3.746609 -2.86975,-7.254074 -1.35515,-3.507464 -2.79002,-6.775784 -1.35516,-3.348034 -2.79003,-6.536638 -1.35516,3.188604 -2.79003,6.536638 -1.35516,3.26832 -2.79003,6.775784 -1.35516,3.507465 -2.79003,7.254074 -1.43487,3.74661 -2.86974,7.732365 z m 44.481,20.008491 V 5.2043362 q 3.02917,-0.797151 6.93521,-1.1160114 3.98576,-0.3985755 7.33379,-0.3985755 11.71812,0 17.53732,4.5437609 5.89892,4.4640458 5.89892,12.7544168 0,6.297493 -2.94946,10.123818 -2.86974,3.826325 -8.37008,5.580057 -5.42063,1.674017 -13.07328,1.674017 h -7.09465 v 21.044787 z m 6.21777,-26.385699 h 6.53664 q 5.65978,0 9.80496,-0.956581 4.14519,-1.036296 6.37721,-3.666895 2.31174,-2.630598 2.31174,-7.493219 0,-4.703192 -2.39146,-7.254075 -2.39145,-2.550883 -6.21777,-3.587179 -3.82633,-1.0362968 -8.13094,-1.0362968 -2.79003,0 -4.86263,0.2391453 -1.99287,0.1594302 -3.42775,0.3188604 z M 335.0452,59.410606 V 4.1680399 h 6.21778 V 59.410606 Z"
id="text979-3" id="text979-3"
aria-label="FastAPI" /> aria-label="FastAPI" />

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

+2 -1
View File
@@ -1,6 +1,7 @@
import type { ColumnDef } from "@tanstack/react-table" import type { ColumnDef } from "@tanstack/react-table"
import type { UserPublic } from "@/client" import type { UserPublic } from "@/client"
import type { DataTableFeatures } from "@/components/Common/DataTable"
import { Badge } from "@/components/ui/badge" import { Badge } from "@/components/ui/badge"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { UserActionsMenu } from "./UserActionsMenu" import { UserActionsMenu } from "./UserActionsMenu"
@@ -9,7 +10,7 @@ export type UserTableData = UserPublic & {
isCurrentUser: boolean isCurrentUser: boolean
} }
export const columns: ColumnDef<UserTableData>[] = [ export const columns: ColumnDef<DataTableFeatures, UserTableData>[] = [
{ {
accessorKey: "full_name", accessorKey: "full_name",
header: "Full Name", header: "Full Name",
+26 -20
View File
@@ -1,9 +1,11 @@
import { import {
type ColumnDef, type ColumnDef,
createPaginatedRowModel,
flexRender, flexRender,
getCoreRowModel, type RowData,
getPaginationRowModel, rowPaginationFeature,
useReactTable, tableFeatures,
useTable,
} from "@tanstack/react-table" } from "@tanstack/react-table"
import { import {
ChevronLeft, ChevronLeft,
@@ -29,20 +31,26 @@ import {
TableRow, TableRow,
} from "@/components/ui/table" } from "@/components/ui/table"
interface DataTableProps<TData, TValue> { const dataTableFeatures = tableFeatures({
columns: ColumnDef<TData, TValue>[] rowPaginationFeature,
paginatedRowModel: createPaginatedRowModel(),
})
export type DataTableFeatures = typeof dataTableFeatures
interface DataTableProps<TData extends RowData> {
columns: ColumnDef<DataTableFeatures, TData>[]
data: TData[] data: TData[]
} }
export function DataTable<TData, TValue>({ export function DataTable<TData extends RowData>({
columns, columns,
data, data,
}: DataTableProps<TData, TValue>) { }: DataTableProps<TData>) {
const table = useReactTable({ const table = useTable({
features: dataTableFeatures,
data, data,
columns, columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
}) })
return ( return (
@@ -70,7 +78,7 @@ export function DataTable<TData, TValue>({
{table.getRowModel().rows.length ? ( {table.getRowModel().rows.length ? (
table.getRowModel().rows.map((row) => ( table.getRowModel().rows.map((row) => (
<TableRow key={row.id}> <TableRow key={row.id}>
{row.getVisibleCells().map((cell) => ( {row.getAllCells().map((cell) => (
<TableCell key={cell.id}> <TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())} {flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell> </TableCell>
@@ -95,13 +103,13 @@ export function DataTable<TData, TValue>({
<div className="flex flex-col sm:flex-row sm:items-center gap-4"> <div className="flex flex-col sm:flex-row sm:items-center gap-4">
<div className="text-sm text-muted-foreground"> <div className="text-sm text-muted-foreground">
Showing{" "} Showing{" "}
{table.getState().pagination.pageIndex * {table.state.pagination.pageIndex *
table.getState().pagination.pageSize + table.state.pagination.pageSize +
1}{" "} 1}{" "}
to{" "} to{" "}
{Math.min( {Math.min(
(table.getState().pagination.pageIndex + 1) * (table.state.pagination.pageIndex + 1) *
table.getState().pagination.pageSize, table.state.pagination.pageSize,
data.length, data.length,
)}{" "} )}{" "}
of{" "} of{" "}
@@ -111,15 +119,13 @@ export function DataTable<TData, TValue>({
<div className="flex items-center gap-x-2"> <div className="flex items-center gap-x-2">
<p className="text-sm text-muted-foreground">Rows per page</p> <p className="text-sm text-muted-foreground">Rows per page</p>
<Select <Select
value={`${table.getState().pagination.pageSize}`} value={`${table.state.pagination.pageSize}`}
onValueChange={(value) => { onValueChange={(value) => {
table.setPageSize(Number(value)) table.setPageSize(Number(value))
}} }}
> >
<SelectTrigger className="h-8 w-[70px]"> <SelectTrigger className="h-8 w-[70px]">
<SelectValue <SelectValue placeholder={table.state.pagination.pageSize} />
placeholder={table.getState().pagination.pageSize}
/>
</SelectTrigger> </SelectTrigger>
<SelectContent side="top"> <SelectContent side="top">
{[5, 10, 25, 50].map((pageSize) => ( {[5, 10, 25, 50].map((pageSize) => (
@@ -136,7 +142,7 @@ export function DataTable<TData, TValue>({
<div className="flex items-center gap-x-1 text-sm text-muted-foreground"> <div className="flex items-center gap-x-1 text-sm text-muted-foreground">
<span>Page</span> <span>Page</span>
<span className="font-medium text-foreground"> <span className="font-medium text-foreground">
{table.getState().pagination.pageIndex + 1} {table.state.pagination.pageIndex + 1}
</span> </span>
<span>of</span> <span>of</span>
<span className="font-medium text-foreground"> <span className="font-medium text-foreground">
+2 -1
View File
@@ -2,6 +2,7 @@ import type { ColumnDef } from "@tanstack/react-table"
import { Check, Copy } from "lucide-react" import { Check, Copy } from "lucide-react"
import type { ItemPublic } from "@/client" import type { ItemPublic } from "@/client"
import type { DataTableFeatures } from "@/components/Common/DataTable"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { useCopyToClipboard } from "@/hooks/useCopyToClipboard" import { useCopyToClipboard } from "@/hooks/useCopyToClipboard"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
@@ -31,7 +32,7 @@ function CopyId({ id }: { id: string }) {
) )
} }
export const columns: ColumnDef<ItemPublic>[] = [ export const columns: ColumnDef<DataTableFeatures, ItemPublic>[] = [
{ {
accessorKey: "id", accessorKey: "id",
header: "ID", header: "ID",
+3 -3
View File
@@ -7,15 +7,15 @@ import {
OctagonXIcon, OctagonXIcon,
TriangleAlertIcon, TriangleAlertIcon,
} from "lucide-react" } from "lucide-react"
import { useTheme } from "next-themes" import { useTheme } from "@/components/theme-provider"
import { Toaster as Sonner, type ToasterProps } from "sonner" import { Toaster as Sonner, type ToasterProps } from "sonner"
const Toaster = ({ ...props }: ToasterProps) => { const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme() const { theme } = useTheme()
return ( return (
<Sonner <Sonner
theme={theme as ToasterProps["theme"]} theme={theme}
className="toaster group" className="toaster group"
icons={{ icons={{
success: <CircleCheckIcon className="size-4" />, success: <CircleCheckIcon className="size-4" />,
+61 -61
View File
@@ -9,29 +9,18 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root' import { Route as rootRouteImport } from './routes/__root'
import { Route as SignupRouteImport } from './routes/signup'
import { Route as ResetPasswordRouteImport } from './routes/reset-password'
import { Route as RecoverPasswordRouteImport } from './routes/recover-password'
import { Route as LoginRouteImport } from './routes/login'
import { Route as LayoutRouteImport } from './routes/_layout' import { Route as LayoutRouteImport } from './routes/_layout'
import { Route as LoginRouteImport } from './routes/login'
import { Route as RecoverPasswordRouteImport } from './routes/recover-password'
import { Route as ResetPasswordRouteImport } from './routes/reset-password'
import { Route as SignupRouteImport } from './routes/signup'
import { Route as LayoutIndexRouteImport } from './routes/_layout/index' import { Route as LayoutIndexRouteImport } from './routes/_layout/index'
import { Route as LayoutSettingsRouteImport } from './routes/_layout/settings'
import { Route as LayoutItemsRouteImport } from './routes/_layout/items'
import { Route as LayoutAdminRouteImport } from './routes/_layout/admin' import { Route as LayoutAdminRouteImport } from './routes/_layout/admin'
import { Route as LayoutItemsRouteImport } from './routes/_layout/items'
import { Route as LayoutSettingsRouteImport } from './routes/_layout/settings'
const SignupRoute = SignupRouteImport.update({ const LayoutRoute = LayoutRouteImport.update({
id: '/signup', id: '/_layout',
path: '/signup',
getParentRoute: () => rootRouteImport,
} as any)
const ResetPasswordRoute = ResetPasswordRouteImport.update({
id: '/reset-password',
path: '/reset-password',
getParentRoute: () => rootRouteImport,
} as any)
const RecoverPasswordRoute = RecoverPasswordRouteImport.update({
id: '/recover-password',
path: '/recover-password',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any)
const LoginRoute = LoginRouteImport.update({ const LoginRoute = LoginRouteImport.update({
@@ -39,8 +28,19 @@ const LoginRoute = LoginRouteImport.update({
path: '/login', path: '/login',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any)
const LayoutRoute = LayoutRouteImport.update({ const RecoverPasswordRoute = RecoverPasswordRouteImport.update({
id: '/_layout', id: '/recover-password',
path: '/recover-password',
getParentRoute: () => rootRouteImport,
} as any)
const ResetPasswordRoute = ResetPasswordRouteImport.update({
id: '/reset-password',
path: '/reset-password',
getParentRoute: () => rootRouteImport,
} as any)
const SignupRoute = SignupRouteImport.update({
id: '/signup',
path: '/signup',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any)
const LayoutIndexRoute = LayoutIndexRouteImport.update({ const LayoutIndexRoute = LayoutIndexRouteImport.update({
@@ -48,9 +48,9 @@ const LayoutIndexRoute = LayoutIndexRouteImport.update({
path: '/', path: '/',
getParentRoute: () => LayoutRoute, getParentRoute: () => LayoutRoute,
} as any) } as any)
const LayoutSettingsRoute = LayoutSettingsRouteImport.update({ const LayoutAdminRoute = LayoutAdminRouteImport.update({
id: '/settings', id: '/admin',
path: '/settings', path: '/admin',
getParentRoute: () => LayoutRoute, getParentRoute: () => LayoutRoute,
} as any) } as any)
const LayoutItemsRoute = LayoutItemsRouteImport.update({ const LayoutItemsRoute = LayoutItemsRouteImport.update({
@@ -58,9 +58,9 @@ const LayoutItemsRoute = LayoutItemsRouteImport.update({
path: '/items', path: '/items',
getParentRoute: () => LayoutRoute, getParentRoute: () => LayoutRoute,
} as any) } as any)
const LayoutAdminRoute = LayoutAdminRouteImport.update({ const LayoutSettingsRoute = LayoutSettingsRouteImport.update({
id: '/admin', id: '/settings',
path: '/admin', path: '/settings',
getParentRoute: () => LayoutRoute, getParentRoute: () => LayoutRoute,
} as any) } as any)
@@ -140,25 +140,11 @@ export interface RootRouteChildren {
declare module '@tanstack/react-router' { declare module '@tanstack/react-router' {
interface FileRoutesByPath { interface FileRoutesByPath {
'/signup': { '/_layout': {
id: '/signup' id: '/_layout'
path: '/signup' path: ''
fullPath: '/signup' fullPath: '/'
preLoaderRoute: typeof SignupRouteImport preLoaderRoute: typeof LayoutRouteImport
parentRoute: typeof rootRouteImport
}
'/reset-password': {
id: '/reset-password'
path: '/reset-password'
fullPath: '/reset-password'
preLoaderRoute: typeof ResetPasswordRouteImport
parentRoute: typeof rootRouteImport
}
'/recover-password': {
id: '/recover-password'
path: '/recover-password'
fullPath: '/recover-password'
preLoaderRoute: typeof RecoverPasswordRouteImport
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport
} }
'/login': { '/login': {
@@ -168,11 +154,25 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof LoginRouteImport preLoaderRoute: typeof LoginRouteImport
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport
} }
'/_layout': { '/recover-password': {
id: '/_layout' id: '/recover-password'
path: '' path: '/recover-password'
fullPath: '/' fullPath: '/recover-password'
preLoaderRoute: typeof LayoutRouteImport preLoaderRoute: typeof RecoverPasswordRouteImport
parentRoute: typeof rootRouteImport
}
'/reset-password': {
id: '/reset-password'
path: '/reset-password'
fullPath: '/reset-password'
preLoaderRoute: typeof ResetPasswordRouteImport
parentRoute: typeof rootRouteImport
}
'/signup': {
id: '/signup'
path: '/signup'
fullPath: '/signup'
preLoaderRoute: typeof SignupRouteImport
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport
} }
'/_layout/': { '/_layout/': {
@@ -182,11 +182,11 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof LayoutIndexRouteImport preLoaderRoute: typeof LayoutIndexRouteImport
parentRoute: typeof LayoutRoute parentRoute: typeof LayoutRoute
} }
'/_layout/settings': { '/_layout/admin': {
id: '/_layout/settings' id: '/_layout/admin'
path: '/settings' path: '/admin'
fullPath: '/settings' fullPath: '/admin'
preLoaderRoute: typeof LayoutSettingsRouteImport preLoaderRoute: typeof LayoutAdminRouteImport
parentRoute: typeof LayoutRoute parentRoute: typeof LayoutRoute
} }
'/_layout/items': { '/_layout/items': {
@@ -196,11 +196,11 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof LayoutItemsRouteImport preLoaderRoute: typeof LayoutItemsRouteImport
parentRoute: typeof LayoutRoute parentRoute: typeof LayoutRoute
} }
'/_layout/admin': { '/_layout/settings': {
id: '/_layout/admin' id: '/_layout/settings'
path: '/admin' path: '/settings'
fullPath: '/admin' fullPath: '/settings'
preLoaderRoute: typeof LayoutAdminRouteImport preLoaderRoute: typeof LayoutSettingsRouteImport
parentRoute: typeof LayoutRoute parentRoute: typeof LayoutRoute
} }
} }
+1 -1
View File
@@ -25,7 +25,7 @@ function Layout() {
<SidebarProvider> <SidebarProvider>
<AppSidebar /> <AppSidebar />
<SidebarInset> <SidebarInset>
<header className="sticky top-0 z-10 flex h-16 shrink-0 items-center gap-2 border-b px-4"> <header className="sticky top-0 z-10 flex h-16 shrink-0 items-center gap-2 border-b bg-background px-4">
<SidebarTrigger className="-ml-1 text-muted-foreground" /> <SidebarTrigger className="-ml-1 text-muted-foreground" />
</header> </header>
<main className="flex-1 p-6 md:p-8"> <main className="flex-1 p-6 md:p-8">
+15 -24
View File
@@ -1,10 +1,12 @@
import { expect, test } from "@playwright/test" import { expect, test } from "@playwright/test"
import { findLastEmail } from "./utils/mailcatcher" import { waitForEmailHtml } from "./utils/mailpit"
import { randomEmail, randomPassword } from "./utils/random" import { randomEmail, randomPassword } from "./utils/random"
import { logInUser, signUpNewUser } from "./utils/user" import { logInUser, signUpNewUser } from "./utils/user"
test.use({ storageState: { cookies: [], origins: [] } }) test.use({ storageState: { cookies: [], origins: [] } })
const resetPath = "/reset-password?token="
test("Password Recovery title is visible", async ({ page }) => { test("Password Recovery title is visible", async ({ page }) => {
await page.goto("/recover-password") await page.goto("/recover-password")
@@ -44,23 +46,17 @@ test("User can reset password successfully using the link", async ({
await page.getByRole("button", { name: "Continue" }).click() await page.getByRole("button", { name: "Continue" }).click()
const emailData = await findLastEmail({ const emailHtml = await waitForEmailHtml({
request, request,
filter: (e) => e.recipients.includes(`<${email}>`), query: `to:${email}`,
timeout: 5000,
}) })
await page.goto( expect(emailHtml).toContain(resetPath)
`${process.env.MAILCATCHER_HOST}/messages/${emailData.id}.html`, const resetUrl = emailHtml.match(/\/reset-password\?token=[^"]+/)?.[0]
) expect(resetUrl).toBeDefined()
const selector = 'a[href*="/reset-password?token="]'
const url = await page.getAttribute(selector, "href")
const resetUrl = new URL(url!)
// Set the new password and confirm it // Set the new password and confirm it
await page.goto(`${resetUrl.pathname}${resetUrl.search}`) await page.goto(resetUrl!)
await page.getByTestId("new-password-input").fill(newPassword) await page.getByTestId("new-password-input").fill(newPassword)
await page.getByTestId("confirm-password-input").fill(newPassword) await page.getByTestId("confirm-password-input").fill(newPassword)
@@ -97,22 +93,17 @@ test("Weak new password validation", async ({ page, request }) => {
await page.getByTestId("email-input").fill(email) await page.getByTestId("email-input").fill(email)
await page.getByRole("button", { name: "Continue" }).click() await page.getByRole("button", { name: "Continue" }).click()
const emailData = await findLastEmail({ const emailHtml = await waitForEmailHtml({
request, request,
filter: (e) => e.recipients.includes(`<${email}>`), query: `to:${email}`,
timeout: 5000,
}) })
await page.goto( expect(emailHtml).toContain(resetPath)
`${process.env.MAILCATCHER_HOST}/messages/${emailData.id}.html`, const resetUrl = emailHtml.match(/\/reset-password\?token=[^"]+/)?.[0]
) expect(resetUrl).toBeDefined()
const selector = 'a[href*="/reset-password?token="]'
const url = await page.getAttribute(selector, "href")
const resetUrl = new URL(url!)
// Set a weak new password // Set a weak new password
await page.goto(`${resetUrl.pathname}${resetUrl.search}`) await page.goto(resetUrl!)
await page.getByTestId("new-password-input").fill(weakPassword) await page.getByTestId("new-password-input").fill(weakPassword)
await page.getByTestId("confirm-password-input").fill(weakPassword) await page.getByTestId("confirm-password-input").fill(weakPassword)
await page.getByRole("button", { name: "Reset Password" }).click() await page.getByRole("button", { name: "Reset Password" }).click()
+4 -3
View File
@@ -79,15 +79,16 @@ test("Sign up with existing email", async ({ page }) => {
await fillForm(page, fullName, email, password, password) await fillForm(page, fullName, email, password, password)
await page.getByRole("button", { name: "Sign Up" }).click() await page.getByRole("button", { name: "Sign Up" }).click()
await expect(page).toHaveURL(/\/login$/)
await page.goto("/signup") await page.goto("/signup")
await fillForm(page, fullName, email, password, password) await fillForm(page, fullName, email, password, password)
await page.getByRole("button", { name: "Sign Up" }).click() await page.getByRole("button", { name: "Sign Up" }).click()
await page await expect(
.getByText("The user with this email already exists in the system") page.getByText("The user with this email already exists in the system"),
.click() ).toBeVisible()
}) })
test("Sign up with weak password", async ({ page }) => { test("Sign up with weak password", async ({ page }) => {
-62
View File
@@ -1,62 +0,0 @@
import type { APIRequestContext } from "@playwright/test"
type Email = {
id: number
recipients: string[]
subject: string
}
async function findEmail({
request,
filter,
}: {
request: APIRequestContext
filter?: (email: Email) => boolean
}) {
const response = await request.get(`${process.env.MAILCATCHER_HOST}/messages`)
let emails = await response.json()
if (filter) {
emails = emails.filter(filter)
}
const email = emails[emails.length - 1]
if (email) {
return email as Email
}
return null
}
export function findLastEmail({
request,
filter,
timeout = 5000,
}: {
request: APIRequestContext
filter?: (email: Email) => boolean
timeout?: number
}) {
const timeoutPromise = new Promise<never>((_, reject) =>
setTimeout(
() => reject(new Error("Timeout while trying to get latest email")),
timeout,
),
)
const checkEmails = async () => {
while (true) {
const emailData = await findEmail({ request, filter })
if (emailData) {
return emailData
}
// Wait for 100ms before checking again
await new Promise((resolve) => setTimeout(resolve, 100))
}
}
return Promise.race([timeoutPromise, checkEmails()])
}
+46
View File
@@ -0,0 +1,46 @@
import type { APIRequestContext } from "@playwright/test"
type EmailSummary = {
ID: string
}
export async function waitForEmailHtml({
request,
query,
timeout = 5000,
}: {
request: APIRequestContext
query: string
timeout?: number
}) {
const deadline = Date.now() + timeout
while (Date.now() < deadline) {
const response = await request.get(
`${process.env.MAILPIT_HOST}/api/v1/search`,
{
params: { query, limit: 1 },
},
)
const { messages }: { messages: EmailSummary[] } = await response.json()
const email = messages[0]
if (email) {
const htmlResponse = await request.get(
`${process.env.MAILPIT_HOST}/view/${email.ID}.html`,
)
if (!htmlResponse.ok()) {
throw new Error(
`Could not get the HTML for email "${email.ID}": ${htmlResponse.status()}`,
)
}
return htmlResponse.text()
}
await new Promise((resolve) => setTimeout(resolve, 100))
}
throw new Error(`Timeout while trying to get the latest email for "${query}"`)
}
+1 -1
View File
@@ -17,7 +17,7 @@
"noUnusedLocals": true, "noUnusedLocals": true,
"noUnusedParameters": true, "noUnusedParameters": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"baseUrl": ".", "types": ["node"],
"paths": { "paths": {
"@/*": ["./src/*"] "@/*": ["./src/*"]
} }
+1 -1
View File
@@ -12,7 +12,7 @@ export default defineConfig({
}, },
resolve: { resolve: {
alias: { alias: {
"@": path.resolve(__dirname, "./src"), "@": path.resolve(import.meta.dirname, "./src"),
}, },
}, },
plugins: [ plugins: [
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

+5 -2
View File
@@ -2,12 +2,15 @@
"name": "fastapi-full-stack-template", "name": "fastapi-full-stack-template",
"private": true, "private": true,
"workspaces": [ "workspaces": [
"frontend" "frontend",
"packages/*"
], ],
"scripts": { "scripts": {
"dev": "bun run --filter frontend dev", "dev": "bun run --filter frontend dev",
"lint": "bun run --filter frontend lint", "lint": "bun run --filter frontend lint",
"test": "bun run --filter frontend test", "test": "bun run --filter frontend test",
"test:ui": "bun run --filter frontend test:ui" "test:ui": "bun run --filter frontend test:ui",
"email:dev": "bun run --filter emails dev",
"email:export": "bun run --filter emails export"
} }
} }
+3
View File
@@ -0,0 +1,3 @@
{
"extends": ["../../frontend/biome.json"]
}
@@ -0,0 +1,65 @@
import { Text } from "@react-email/components"
import { LinkButton } from "../ui/Button"
import { Callout, Detail } from "../ui/Callout"
import { Heading } from "../ui/Heading"
import { Layout } from "../ui/Layout"
import { Link } from "../ui/Link"
type NewAccountProps = {
project_name: string
username: string
password: string
link: string
}
export default function NewAccount({
project_name = "{{ project_name }}",
username = "{{ username }}",
password = "{{ password }}",
link = "{{ link }}",
}: NewAccountProps) {
return (
<Layout
title={`${project_name} - New account`}
preview={`Your ${project_name} account is ready`}
project_name={project_name}
>
<Heading>Welcome to {project_name}!</Heading>
<Text style={bodyTextStyle}>Hi,</Text>
<Text style={bodyTextStyle}>
Your account has been successfully created and is ready to use. These
are your credentials:
</Text>
<Callout>
<Detail label="Username" value={username} />
<Detail label="Password" value={password} />
</Callout>
<Text style={bodyTextStyle}>
Get started by signing in to your dashboard:
</Text>
<LinkButton href={link}>Go to Dashboard</LinkButton>
<Text style={supportingTextStyle}>
Or copy and paste this link into your browser:
<br />
<Link href={link}>{link}</Link>
</Text>
<Text style={supportingTextStyle}>
For security reasons, change your password after your first sign in.
</Text>
</Layout>
)
}
const bodyTextStyle = {
color: "#334155",
fontSize: "15px",
lineHeight: "26px",
margin: "0 0 18px",
}
const supportingTextStyle = {
color: "#64748b",
fontSize: "14px",
lineHeight: "23px",
margin: "0 0 16px",
}
@@ -0,0 +1,61 @@
import { Text } from "@react-email/components"
import { LinkButton } from "../ui/Button"
import { Heading } from "../ui/Heading"
import { Layout } from "../ui/Layout"
import { Link } from "../ui/Link"
type ResetPasswordProps = {
project_name: string
username: string
link: string
valid_hours: string
}
export default function ResetPassword({
project_name = "{{ project_name }}",
username = "{{ username }}",
link = "{{ link }}",
valid_hours = "{{ valid_hours }}",
}: ResetPasswordProps) {
return (
<Layout
title={`${project_name} - Password recovery`}
preview={`Reset your ${project_name} password`}
project_name={project_name}
>
<Heading>Reset your password</Heading>
<Text style={bodyTextStyle}>Hi {username},</Text>
<Text style={bodyTextStyle}>
We've received a request to reset the password for your {project_name}{" "}
account. Choose a new one by clicking the button below:
</Text>
<LinkButton href={link}>Reset password</LinkButton>
<Text style={supportingTextStyle}>
Or copy and paste this link into your browser:
<br />
<Link href={link}>{link}</Link>
</Text>
<Text style={supportingTextStyle}>
This link will expire in {valid_hours} hours.
</Text>
<Text style={supportingTextStyle}>
If you didn't request a password recovery, you can safely ignore this
email.
</Text>
</Layout>
)
}
const bodyTextStyle = {
color: "#334155",
fontSize: "15px",
lineHeight: "26px",
margin: "0 0 18px",
}
const supportingTextStyle = {
color: "#64748b",
fontSize: "14px",
lineHeight: "23px",
margin: "0 0 16px",
}
@@ -0,0 +1,49 @@
import { Text } from "@react-email/components"
import { Callout, Detail } from "../ui/Callout"
import { Heading } from "../ui/Heading"
import { Layout } from "../ui/Layout"
type TestEmailProps = {
project_name: string
email: string
}
export default function TestEmail({
project_name = "{{ project_name }}",
email = "{{ email }}",
}: TestEmailProps) {
return (
<Layout
title={`${project_name} - Test email`}
preview={`${project_name} email delivery is working`}
project_name={project_name}
>
<Heading>Test email</Heading>
<Text style={bodyTextStyle}>Hi,</Text>
<Text style={bodyTextStyle}>
This is a test email from {project_name}. If you're reading it, email
delivery is configured correctly.
</Text>
<Callout>
<Detail label="Sent to" value={email} />
</Callout>
<Text style={supportingTextStyle}>
If you weren't expecting this email, you can safely ignore it.
</Text>
</Layout>
)
}
const bodyTextStyle = {
color: "#334155",
fontSize: "15px",
lineHeight: "26px",
margin: "0 0 18px",
}
const supportingTextStyle = {
color: "#64748b",
fontSize: "14px",
lineHeight: "23px",
margin: "0 0 16px",
}
+23
View File
@@ -0,0 +1,23 @@
{
"name": "emails",
"version": "0.1.0",
"private": true,
"scripts": {
"build": "email build",
"dev": "email dev --port 3333",
"lint": "biome check --write --unsafe --no-errors-on-unmatched --files-ignore-unknown=true ./",
"export": "email export --outDir ../../backend/app/email-templates"
},
"dependencies": {
"@react-email/components": "1.0.12",
"react": "^19.3.1",
"react-dom": "^19.3.1",
"react-email": "6.11.3"
},
"devDependencies": {
"@biomejs/biome": "^2.6.3",
"@react-email/ui": "6.11.3",
"@types/react": "^19.3.0",
"@types/react-dom": "^19.3.0"
}
}
+14
View File
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"composite": false,
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"strictNullChecks": true
},
"include": ["**/*.ts", "**/*.tsx"],
"exclude": ["node_modules", ".react-email"]
}
+23
View File
@@ -0,0 +1,23 @@
import { Button } from "@react-email/components"
type ButtonProps = React.PropsWithChildren & { href: string }
export const LinkButton = ({ children, href }: ButtonProps) => (
<Button href={href} style={buttonStyle}>
{children}
</Button>
)
const buttonStyle = {
backgroundColor: "#00796b",
borderRadius: "6px",
color: "#ffffff",
display: "inline-block",
fontSize: "14px",
fontWeight: "700",
lineHeight: "20px",
margin: "8px 0 28px",
padding: "13px 28px",
textAlign: "center" as const,
textDecoration: "none",
}
+46
View File
@@ -0,0 +1,46 @@
import { Section, Text } from "@react-email/components"
type CalloutProps = React.PropsWithChildren
export const Callout = ({ children }: CalloutProps) => (
<Section style={calloutStyle}>{children}</Section>
)
type DetailProps = { label: string; value: string }
export const Detail = ({ label, value }: DetailProps) => (
<Text style={detailStyle}>
<span style={labelStyle}>{label}</span>
<br />
<span style={valueStyle}>{value}</span>
</Text>
)
const calloutStyle = {
backgroundColor: "#f2f8f7",
borderLeft: "3px solid #00897b",
margin: "24px 0 28px",
padding: "6px 18px",
}
const detailStyle = {
margin: "12px 0",
}
const labelStyle = {
color: "#64748b",
fontSize: "11px",
fontWeight: "700",
letterSpacing: "1px",
lineHeight: "16px",
textTransform: "uppercase" as const,
}
const valueStyle = {
color: "#1e293b",
fontFamily: 'Consolas, "Courier New", monospace',
fontSize: "14px",
fontWeight: "700",
lineHeight: "22px",
wordBreak: "break-all" as const,
}
+18
View File
@@ -0,0 +1,18 @@
import { Heading as BaseHeading } from "@react-email/components"
type HeadingProps = React.PropsWithChildren
export const Heading = ({ children }: HeadingProps) => (
<BaseHeading as="h1" style={headingStyle}>
{children}
</BaseHeading>
)
const headingStyle = {
color: "#17212b",
fontSize: "26px",
fontWeight: "700",
letterSpacing: "-0.3px",
lineHeight: "34px",
margin: "0 0 20px",
}
+111
View File
@@ -0,0 +1,111 @@
import {
Body,
Column,
Container,
Head,
Hr,
Html,
Preview,
Row,
Section,
Text,
} from "@react-email/components"
type LayoutProps = React.PropsWithChildren & {
title: string
project_name: string
preview?: string
}
export const Layout = ({
project_name,
title,
preview,
children,
}: LayoutProps) => {
return (
<Html lang="en" dir="ltr">
<Head>
<title>{title}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</Head>
{preview ? <Preview>{preview}</Preview> : null}
<Body style={bodyStyle}>
<Container style={containerStyle}>
<Row>
<Column style={contentStyle}>
<Section>
<Hr style={brandRuleStyle} />
<Text style={brandStyle}>{project_name}</Text>
</Section>
<Section>{children}</Section>
<Hr style={footerRuleStyle} />
<Section>
<Text style={footerStyle}>
© {new Date().getFullYear()} {project_name}. All rights
reserved.
</Text>
</Section>
</Column>
</Row>
</Container>
</Body>
</Html>
)
}
const fontFamily =
'-apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif'
const bodyStyle = {
backgroundColor: "#f4f7f6",
color: "#1e293b",
fontFamily,
margin: "0",
padding: "32px 12px",
}
const containerStyle = {
backgroundColor: "#ffffff",
border: "1px solid #dce7e5",
borderRadius: "8px",
boxSizing: "border-box" as const,
margin: "0 auto",
maxWidth: "560px",
width: "100%",
}
const contentStyle = {
padding: "40px 36px 32px",
}
const brandRuleStyle = {
border: "0",
borderTop: "3px solid #00897b",
margin: "0 0 16px",
width: "32px",
}
const brandStyle = {
color: "#334155",
fontSize: "13px",
fontWeight: "700",
letterSpacing: "1px",
lineHeight: "20px",
margin: "0 0 32px",
textTransform: "uppercase" as const,
}
const footerRuleStyle = {
border: "0",
borderTop: "1px solid #dce7e5",
margin: "36px 0 18px",
width: "100%",
}
const footerStyle = {
color: "#64748b",
fontSize: "12px",
lineHeight: "20px",
margin: "0",
}
+15
View File
@@ -0,0 +1,15 @@
import { Link as EmailLink } from "@react-email/components"
type LinkProps = React.PropsWithChildren & { href: string }
export const Link = ({ children, href }: LinkProps) => (
<EmailLink href={href} style={linkStyle}>
{children}
</EmailLink>
)
const linkStyle = {
color: "#00695c",
textDecoration: "underline",
wordBreak: "break-all" as const,
}
+3 -2
View File
@@ -1,7 +1,8 @@
[dependency-groups] [dependency-groups]
dev = [ dev = [
"prek>=0.4.11,<1.0.0", "prek>=0.5.3,<1.0.0",
"zizmor>=1.28.0", "typer>=0.27.2,<1.0.0",
"zizmor>=1.30.1",
] ]
github-actions = [ github-actions = [
+105 -3
View File
@@ -2,13 +2,107 @@
## Latest Changes ## Latest Changes
### Features ### Fixes
* 🔧 Use FastAPI entrypoint. PR [#2360](https://github.com/fastapi/full-stack-fastapi-template/pull/2360) by [@tiangolo](https://github.com/tiangolo). * 🐛 Fix Sonner toast theme sync. PR [#2450](https://github.com/fastapi/full-stack-fastapi-template/pull/2450) by [@chris-bs23](https://github.com/chris-bs23).
* ♻️ Add library-skills for FastAPI and SQLModel. PR [#2354](https://github.com/fastapi/full-stack-fastapi-template/pull/2354) by [@tiangolo](https://github.com/tiangolo). * 🐛 Add background to the sticky layout header. PR [#2384](https://github.com/fastapi/full-stack-fastapi-template/pull/2384) by [@istoutjesdijk](https://github.com/istoutjesdijk).
### Refactors ### Refactors
* ♻️ Simplify database readiness checks. PR [#2460](https://github.com/fastapi/full-stack-fastapi-template/pull/2460) by [@tiangolo](https://github.com/tiangolo).
* ✅ Simplify password reset E2E tests. PR [#2453](https://github.com/fastapi/full-stack-fastapi-template/pull/2453) by [@alejsdev](https://github.com/alejsdev).
* 💄 Refactor email templates and components for improved styling. PR [#2451](https://github.com/fastapi/full-stack-fastapi-template/pull/2451) by [@alejsdev](https://github.com/alejsdev).
* ♻️ Replace Mailcatcher with Mailpit for local email testing. PR [#2436](https://github.com/fastapi/full-stack-fastapi-template/pull/2436) by [@alejsdev](https://github.com/alejsdev).
### Docs
* 🎨 Enable Ruff formatting for Python in Markdown. PR [#2517](https://github.com/fastapi/full-stack-fastapi-template/pull/2517) by [@tiangolo](https://github.com/tiangolo).
* 📝 Update README.md with Mailpit and React Email. PR [#2452](https://github.com/fastapi/full-stack-fastapi-template/pull/2452) by [@alejsdev](https://github.com/alejsdev).
* 📝 Simplify project documentation. PR [#2442](https://github.com/fastapi/full-stack-fastapi-template/pull/2442) by [@tiangolo](https://github.com/tiangolo).
### Internal
* ⬆ Bump the npm-packages group across 1 directory with 30 updates. PR [#2525](https://github.com/fastapi/full-stack-fastapi-template/pull/2525) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump playwright from v1.63.0-noble to v1.64.1-noble in /frontend in the docker group across 1 directory. PR [#2521](https://github.com/fastapi/full-stack-fastapi-template/pull/2521) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump the github-actions group with 6 updates. PR [#2520](https://github.com/fastapi/full-stack-fastapi-template/pull/2520) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump pre-commit hooks. PR [#2523](https://github.com/fastapi/full-stack-fastapi-template/pull/2523) by [@pr-submit[bot]](https://github.com/apps/pr-submit).
* ⬆ Bump dnspython from 2.8.0 to 2.9.0. PR [#2524](https://github.com/fastapi/full-stack-fastapi-template/pull/2524) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump certifi from 2026.6.17 to 2026.10.19. PR [#2518](https://github.com/fastapi/full-stack-fastapi-template/pull/2518) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆️ Migrate `@tanstack/react-table` to 9.x. PR [#2509](https://github.com/fastapi/full-stack-fastapi-template/pull/2509) by [@YuriiMotov](https://github.com/YuriiMotov).
* ⬆ Bump the npm-packages group across 1 directory with 25 updates. PR [#2506](https://github.com/fastapi/full-stack-fastapi-template/pull/2506) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump mako from 1.3.12 to 1.4.2. PR [#2507](https://github.com/fastapi/full-stack-fastapi-template/pull/2507) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump the python-packages group across 1 directory with 8 updates. PR [#2497](https://github.com/fastapi/full-stack-fastapi-template/pull/2497) by [@dependabot[bot]](https://github.com/apps/dependabot).
* Bump axios from 1.18.0 to 1.20.0 in /frontend. PR [#2505](https://github.com/fastapi/full-stack-fastapi-template/pull/2505) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump anyio from 4.14.1 to 4.14.2. PR [#2483](https://github.com/fastapi/full-stack-fastapi-template/pull/2483) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Update sentry-sdk[fastapi] requirement from <3.0.0,>=2.68.1 to >=2.70.0,<3.0.0. PR [#2498](https://github.com/fastapi/full-stack-fastapi-template/pull/2498) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump pre-commit hooks. PR [#2501](https://github.com/fastapi/full-stack-fastapi-template/pull/2501) by [@pr-submit[bot]](https://github.com/apps/pr-submit).
* ⬆ Bump playwright from v1.62.1-noble to v1.63.0-noble in /frontend in the docker group across 1 directory. PR [#2496](https://github.com/fastapi/full-stack-fastapi-template/pull/2496) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump urllib3 from 2.7.0 to 2.8.0. PR [#2503](https://github.com/fastapi/full-stack-fastapi-template/pull/2503) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Update psycopg[binary] requirement from <4.0.0,>=3.3.4 to >=3.3.6,<4.0.0. PR [#2499](https://github.com/fastapi/full-stack-fastapi-template/pull/2499) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump pyjwt from 2.13.0 to 2.15.0. PR [#2494](https://github.com/fastapi/full-stack-fastapi-template/pull/2494) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump the github-actions group with 2 updates. PR [#2495](https://github.com/fastapi/full-stack-fastapi-template/pull/2495) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump traefik from 3.6 to v3.7 in the docker-compose group. PR [#2465](https://github.com/fastapi/full-stack-fastapi-template/pull/2465) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump pre-commit hooks. PR [#2471](https://github.com/fastapi/full-stack-fastapi-template/pull/2471) by [@pr-submit[bot]](https://github.com/apps/pr-submit).
* ⬆ Update pwdlib[argon2,bcrypt] requirement from >=0.3.0 to >=0.3.1. PR [#2468](https://github.com/fastapi/full-stack-fastapi-template/pull/2468) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Update sentry-sdk[fastapi] requirement from <3.0.0,>=2.66.1 to >=2.68.1,<3.0.0. PR [#2467](https://github.com/fastapi/full-stack-fastapi-template/pull/2467) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump the python-packages group with 4 updates. PR [#2466](https://github.com/fastapi/full-stack-fastapi-template/pull/2466) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump the github-actions group with 4 updates. PR [#2464](https://github.com/fastapi/full-stack-fastapi-template/pull/2464) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆️ Bump Typer min version to `0.26.1`. PR [#2459](https://github.com/fastapi/full-stack-fastapi-template/pull/2459) by [@YuriiMotov](https://github.com/YuriiMotov).
* ⬆️ Bump setup-uv action to 10.0.1. PR [#2458](https://github.com/fastapi/full-stack-fastapi-template/pull/2458) by [@YuriiMotov](https://github.com/YuriiMotov).
## 0.12.0 (2026-08-12)
### Features
* ☁️ Add FastAPI Cloud deployment. PR [#2438](https://github.com/fastapi/full-stack-fastapi-template/pull/2438) by [@tiangolo](https://github.com/tiangolo).
### Fixes
* 🐳 Include frontend assets in Docker build context. PR [#2440](https://github.com/fastapi/full-stack-fastapi-template/pull/2440) by [@tiangolo](https://github.com/tiangolo).
### Internal
* 👷 Migrate automatic labels to Latest Changes. PR [#2439](https://github.com/fastapi/full-stack-fastapi-template/pull/2439) by [@tiangolo](https://github.com/tiangolo).
## 0.11.1 (2026-08-11)
### Refactors
* ♻️ Simplify Docker Compose deployment. PR [#2435](https://github.com/fastapi/full-stack-fastapi-template/pull/2435) by [@tiangolo](https://github.com/tiangolo).
* ♻️ Migrate to `DATABASE_URL` instead of separate variables. PR [#2184](https://github.com/fastapi/full-stack-fastapi-template/pull/2184) by [@patrick91](https://github.com/patrick91).
### Internal
* ⬆ Bump the npm-packages group across 1 directory with 33 updates. PR [#2433](https://github.com/fastapi/full-stack-fastapi-template/pull/2433) by [@dependabot[bot]](https://github.com/apps/dependabot).
## 0.11.0 (2026-08-11)
### Features
* ✨ Add react-email for email templates. PR [#2421](https://github.com/fastapi/full-stack-fastapi-template/pull/2421) by [@alejsdev](https://github.com/alejsdev).
* 🔧 Use FastAPI entrypoint. PR [#2360](https://github.com/fastapi/full-stack-fastapi-template/pull/2360) by [@tiangolo](https://github.com/tiangolo).
* ♻️ Add library-skills for FastAPI and SQLModel. PR [#2354](https://github.com/fastapi/full-stack-fastapi-template/pull/2354) by [@tiangolo](https://github.com/tiangolo).
### Fixes
* ✅ Fix sign-up test race. PR [#2422](https://github.com/fastapi/full-stack-fastapi-template/pull/2422) by [@tiangolo](https://github.com/tiangolo).
### Refactors
* ♻️ Remove Copier project generation. PR [#2430](https://github.com/fastapi/full-stack-fastapi-template/pull/2430) by [@tiangolo](https://github.com/tiangolo).
* ♻️ Simplify CORS configuration. PR [#2429](https://github.com/fastapi/full-stack-fastapi-template/pull/2429) by [@tiangolo](https://github.com/tiangolo).
* ♻️ Use `FASTAPI_ENV` for development. PR [#2427](https://github.com/fastapi/full-stack-fastapi-template/pull/2427) by [@tiangolo](https://github.com/tiangolo).
* ♻️ Run database setup explicitly before startup, no need for prestart. PR [#2426](https://github.com/fastapi/full-stack-fastapi-template/pull/2426) by [@tiangolo](https://github.com/tiangolo).
* ♻️ Require explicit SECRET_KEY. PR [#2425](https://github.com/fastapi/full-stack-fastapi-template/pull/2425) by [@tiangolo](https://github.com/tiangolo).
* ♻️ Simplify uv workspace setup. PR [#2423](https://github.com/fastapi/full-stack-fastapi-template/pull/2423) by [@tiangolo](https://github.com/tiangolo).
* ♻️ Use FastAPI entrypoint in Compose. PR [#2420](https://github.com/fastapi/full-stack-fastapi-template/pull/2420) by [@tiangolo](https://github.com/tiangolo).
* ♻️ Require non-empty Compose configuration. PR [#2418](https://github.com/fastapi/full-stack-fastapi-template/pull/2418) by [@tiangolo](https://github.com/tiangolo).
* ♻️ Simplify optional environment configuration. PR [#2417](https://github.com/fastapi/full-stack-fastapi-template/pull/2417) by [@tiangolo](https://github.com/tiangolo).
* ♻️ Simplify Copier environment configuration. PR [#2416](https://github.com/fastapi/full-stack-fastapi-template/pull/2416) by [@tiangolo](https://github.com/tiangolo).
* ♻️ Simplify environment defaults. PR [#2415](https://github.com/fastapi/full-stack-fastapi-template/pull/2415) by [@tiangolo](https://github.com/tiangolo).
* ♻️ Simplify local development workflow. PR [#2414](https://github.com/fastapi/full-stack-fastapi-template/pull/2414) by [@tiangolo](https://github.com/tiangolo).
* ♻️ Simplify environment configuration. PR [#2413](https://github.com/fastapi/full-stack-fastapi-template/pull/2413) by [@tiangolo](https://github.com/tiangolo).
* ♻️ Simplify Traefik deployment, make it part of the stack. PR [#2412](https://github.com/fastapi/full-stack-fastapi-template/pull/2412) by [@tiangolo](https://github.com/tiangolo).
* ♻️ Serve frontend from FastAPI. PR [#2393](https://github.com/fastapi/full-stack-fastapi-template/pull/2393) by [@tiangolo](https://github.com/tiangolo). * ♻️ Serve frontend from FastAPI. PR [#2393](https://github.com/fastapi/full-stack-fastapi-template/pull/2393) by [@tiangolo](https://github.com/tiangolo).
* 🔧 Refactor PostgreSQL env var config. PR [#2392](https://github.com/fastapi/full-stack-fastapi-template/pull/2392) by [@tiangolo](https://github.com/tiangolo). * 🔧 Refactor PostgreSQL env var config. PR [#2392](https://github.com/fastapi/full-stack-fastapi-template/pull/2392) by [@tiangolo](https://github.com/tiangolo).
* ♻️ Refactor models to improve types. PR [#2356](https://github.com/fastapi/full-stack-fastapi-template/pull/2356) by [@tiangolo](https://github.com/tiangolo). * ♻️ Refactor models to improve types. PR [#2356](https://github.com/fastapi/full-stack-fastapi-template/pull/2356) by [@tiangolo](https://github.com/tiangolo).
@@ -23,6 +117,8 @@
### Docs ### Docs
* 📝 Remove obsolete local domain setup. PR [#2428](https://github.com/fastapi/full-stack-fastapi-template/pull/2428) by [@tiangolo](https://github.com/tiangolo).
* 📝 Clarify environment configuration. PR [#2419](https://github.com/fastapi/full-stack-fastapi-template/pull/2419) by [@tiangolo](https://github.com/tiangolo).
* 📝 Improve README screenshot alt text. PR [#2359](https://github.com/fastapi/full-stack-fastapi-template/pull/2359) by [@Bmowville](https://github.com/Bmowville). * 📝 Improve README screenshot alt text. PR [#2359](https://github.com/fastapi/full-stack-fastapi-template/pull/2359) by [@Bmowville](https://github.com/Bmowville).
* ✏️ Fix typo in the comment for DOMAIN in the .env file. PR [#2305](https://github.com/fastapi/full-stack-fastapi-template/pull/2305) by [@serhiiur](https://github.com/serhiiur). * ✏️ Fix typo in the comment for DOMAIN in the .env file. PR [#2305](https://github.com/fastapi/full-stack-fastapi-template/pull/2305) by [@serhiiur](https://github.com/serhiiur).
* 📝 Update security policy. PR [#2297](https://github.com/fastapi/full-stack-fastapi-template/pull/2297) by [@tiangolo](https://github.com/tiangolo). * 📝 Update security policy. PR [#2297](https://github.com/fastapi/full-stack-fastapi-template/pull/2297) by [@tiangolo](https://github.com/tiangolo).
@@ -30,6 +126,12 @@
### Internal ### Internal
* 👷 Add release automation. PR [#2431](https://github.com/fastapi/full-stack-fastapi-template/pull/2431) by [@tiangolo](https://github.com/tiangolo).
* 👷 Remove legacy label check. PR [#2424](https://github.com/fastapi/full-stack-fastapi-template/pull/2424) by [@tiangolo](https://github.com/tiangolo).
* 🔐 Use PR Submit for pull requests. PR [#2411](https://github.com/fastapi/full-stack-fastapi-template/pull/2411) by [@tiangolo](https://github.com/tiangolo).
* 👷 Use GitHub CLI for Git authentication. PR [#2410](https://github.com/fastapi/full-stack-fastapi-template/pull/2410) by [@tiangolo](https://github.com/tiangolo).
* 👷 Use PR Push commit identity. PR [#2409](https://github.com/fastapi/full-stack-fastapi-template/pull/2409) by [@tiangolo](https://github.com/tiangolo).
* 🔒 Replace pre-commit PAT with PR Push. PR [#2407](https://github.com/fastapi/full-stack-fastapi-template/pull/2407) by [@tiangolo](https://github.com/tiangolo).
* ⬆️ Bump `@hey-api/openapi-ts` from 0.73.0 to 0.97.3 and migrate to new client. PR [#2388](https://github.com/fastapi/full-stack-fastapi-template/pull/2388) by [@YuriiMotov](https://github.com/YuriiMotov). * ⬆️ Bump `@hey-api/openapi-ts` from 0.73.0 to 0.97.3 and migrate to new client. PR [#2388](https://github.com/fastapi/full-stack-fastapi-template/pull/2388) by [@YuriiMotov](https://github.com/YuriiMotov).
* 🔥 Remove the old Latest Changes workflow. PR [#2403](https://github.com/fastapi/full-stack-fastapi-template/pull/2403) by [@tiangolo](https://github.com/tiangolo). * 🔥 Remove the old Latest Changes workflow. PR [#2403](https://github.com/fastapi/full-stack-fastapi-template/pull/2403) by [@tiangolo](https://github.com/tiangolo).
* ⬆ Update psycopg[binary] requirement from <4.0.0,>=3.1.13 to >=3.3.4,<4.0.0. PR [#2399](https://github.com/fastapi/full-stack-fastapi-template/pull/2399) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Update psycopg[binary] requirement from <4.0.0,>=3.1.13 to >=3.3.4,<4.0.0. PR [#2399](https://github.com/fastapi/full-stack-fastapi-template/pull/2399) by [@dependabot[bot]](https://github.com/apps/dependabot).
+175
View File
@@ -0,0 +1,175 @@
"""Prepare a release by updating and extracting release notes."""
import re
from datetime import date
from pathlib import Path
from typing import Annotated, Literal
import typer
RELEASE_NOTES_FILE = Path("release-notes.md")
RELEASE_NOTES_HEADER = "# Release Notes\n\n"
LATEST_CHANGES_HEADER = "## Latest Changes"
VERSION_HEADING_PATTERN = re.compile(r"(?m)^## (\d+\.\d+\.\d+)(?: \([^)]+\))?$")
BumpType = Literal["major", "minor", "patch"]
app = typer.Typer()
def parse_version(version: str) -> tuple[int, int, int]:
if not re.fullmatch(r"\d+\.\d+\.\d+", version):
raise ValueError(f"Invalid version: {version!r}. Expected format: X.Y.Z")
major, minor, patch = version.split(".")
return int(major), int(minor), int(patch)
def get_current_version(content: str, release_notes_file: Path) -> str:
match = VERSION_HEADING_PATTERN.search(content)
if not match:
raise RuntimeError(f"Could not find a version section in {release_notes_file}")
return match.group(1)
def bump_version(version: str, bump: BumpType) -> str:
major, minor, patch = parse_version(version)
if bump == "major":
return f"{major + 1}.0.0"
if bump == "minor":
return f"{major}.{minor + 1}.0"
return f"{major}.{minor}.{patch + 1}"
def update_release_notes(
content: str, version: str, release_date: date, release_notes_file: Path
) -> str:
latest_header = f"{RELEASE_NOTES_HEADER}{LATEST_CHANGES_HEADER}\n"
if not content.startswith(latest_header):
raise RuntimeError(f"{release_notes_file} must start with {latest_header!r}")
if re.search(rf"^## {re.escape(version)}(?: \([^)]+\))?$", content, re.M):
raise RuntimeError(f"Release notes already contain a section for {version}")
current_version = get_current_version(content, release_notes_file)
if parse_version(version) <= parse_version(current_version):
raise RuntimeError(
f"New version {version} must be greater than current version "
f"{current_version}"
)
current_match = VERSION_HEADING_PATTERN.search(content)
assert current_match is not None
latest_changes = content[len(latest_header) : current_match.start()].strip()
if not latest_changes:
raise RuntimeError("The Latest Changes section is empty")
release_header = f"## {version} ({release_date.isoformat()})"
return content.replace(
latest_header,
f"{RELEASE_NOTES_HEADER}{LATEST_CHANGES_HEADER}\n\n{release_header}\n",
1,
)
def get_release_notes_body(content: str, version: str, release_notes_file: Path) -> str:
version_heading = re.compile(rf"(?m)^## {re.escape(version)}(?: \([^)]+\))?$")
match = version_heading.search(content)
if not match:
raise RuntimeError(
f"Could not find release notes section for {version} in "
f"{release_notes_file}"
)
next_match = VERSION_HEADING_PATTERN.search(content, match.end())
end = next_match.start() if next_match else len(content)
body = content[match.end() : end].strip()
if not body:
raise RuntimeError(
f"Release notes section for {version} in {release_notes_file} is empty"
)
return f"{body}\n"
def prepare_release(
bump: BumpType, release_date: date, release_notes_file: Path
) -> str:
content = release_notes_file.read_text(encoding="utf-8")
version = bump_version(get_current_version(content, release_notes_file), bump)
release_notes_file.write_text(
update_release_notes(content, version, release_date, release_notes_file),
encoding="utf-8",
)
return version
@app.command()
def prepare(
bump: Annotated[
BumpType,
typer.Argument(help="The release bump to make: major, minor, or patch."),
],
release_notes_file: Annotated[
Path,
typer.Option(
exists=True,
file_okay=True,
dir_okay=False,
readable=True,
writable=True,
help="Path to the release notes Markdown file.",
),
] = RELEASE_NOTES_FILE,
release_date: Annotated[
str,
typer.Option(
"--date",
help="Release date in YYYY-MM-DD format. Defaults to today.",
),
] = date.today().isoformat(),
) -> None:
parsed_release_date = date.fromisoformat(release_date or date.today().isoformat())
version = prepare_release(bump, parsed_release_date, release_notes_file)
typer.echo(f"Prepared release {version} ({parsed_release_date.isoformat()})")
@app.command()
def current_version(
release_notes_file: Annotated[
Path,
typer.Option(
exists=True,
file_okay=True,
dir_okay=False,
readable=True,
help="Path to the release notes Markdown file.",
),
] = RELEASE_NOTES_FILE,
) -> None:
typer.echo(
get_current_version(
release_notes_file.read_text(encoding="utf-8"), release_notes_file
)
)
@app.command()
def release_notes(
release_notes_file: Annotated[
Path,
typer.Option(
exists=True,
file_okay=True,
dir_okay=False,
readable=True,
help="Path to the release notes Markdown file.",
),
] = RELEASE_NOTES_FILE,
) -> None:
content = release_notes_file.read_text(encoding="utf-8")
version = get_current_version(content, release_notes_file)
typer.echo(
get_release_notes_body(content, version, release_notes_file),
nl=False,
)
if __name__ == "__main__":
app()
+1
View File
@@ -11,5 +11,6 @@ if [ $(uname -s) = "Linux" ]; then
fi fi
docker-compose build docker-compose build
docker-compose run --rm backend bash scripts/prestart.sh
docker-compose up -d docker-compose up -d
docker-compose exec -T backend bash scripts/tests-start.sh "$@" docker-compose exec -T backend bash scripts/tests-start.sh "$@"
+1
View File
@@ -6,6 +6,7 @@ set -x
docker compose build docker compose build
docker compose down -v --remove-orphans # Remove possibly previous broken stacks left hanging after an error docker compose down -v --remove-orphans # Remove possibly previous broken stacks left hanging after an error
docker compose run --rm backend bash scripts/prestart.sh
docker compose up -d docker compose up -d
docker compose exec -T backend bash scripts/tests-start.sh "$@" docker compose exec -T backend bash scripts/tests-start.sh "$@"
docker compose down -v --remove-orphans docker compose down -v --remove-orphans
Generated
+383 -288
View File
@@ -1,5 +1,5 @@
version = 1 version = 1
revision = 3 revision = 5
requires-python = ">=3.14, <4.0" requires-python = ">=3.14, <4.0"
resolution-markers = [ resolution-markers = [
"python_full_version >= '3.15'", "python_full_version >= '3.15'",
@@ -13,23 +13,24 @@ members = [
[manifest.dependency-groups] [manifest.dependency-groups]
dev = [ dev = [
{ name = "prek", specifier = ">=0.4.11,<1.0.0" }, { name = "prek", specifier = ">=0.5.3,<1.0.0" },
{ name = "zizmor", specifier = ">=1.28.0" }, { name = "typer", specifier = ">=0.27.2,<1.0.0" },
{ name = "zizmor", specifier = ">=1.30.1" },
] ]
github-actions = [{ name = "smokeshow", specifier = ">=0.5.0" }] github-actions = [{ name = "smokeshow", specifier = ">=0.5.0" }]
[[package]] [[package]]
name = "alembic" name = "alembic"
version = "1.18.5" version = "1.20.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "mako" }, { name = "mako" },
{ name = "sqlalchemy" }, { name = "sqlalchemy" },
{ name = "typing-extensions" }, { name = "typing-extensions" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/1a/cc/ac0bed8e562e7407fe55c3ba85a4dce86e6dbd8730887bd1e406a6c5c18a/alembic-1.18.5.tar.gz", hash = "sha256:1554982221dd17e9a749b53902407578eb305e453f71999e8c7f0a48389fff8e", size = 2060480, upload-time = "2026-06-25T15:20:54.888Z" } sdist = { url = "https://files.pythonhosted.org/packages/ed/aa/02910bdb8e2f1444f6654d5b296cd827d126f82209050ee7b1000f92ac4b/alembic-1.20.0.tar.gz", hash = "sha256:db505480647bc60386c5369402f4a57a506b7539c9e9ef5e270d45cbbe4939bf", size = 2093272, upload-time = "2026-09-11T19:09:11.126Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/96/78/5fe6dc3a3a5b2f5a2a4faef8bfe336d5fa049a38884ab3172e0098160c01/alembic-1.18.5-py3-none-any.whl", hash = "sha256:06d8ba9d04558022f5395e9317de03d270f3dced49cee01f89fe7a13c26f14bc", size = 264664, upload-time = "2026-06-25T15:20:56.673Z" }, { url = "https://files.pythonhosted.org/packages/3f/27/78a89b55b0904d222183164e079b4ca56208e94eff1d35ad1f1ad5be9b06/alembic-1.20.0-py3-none-any.whl", hash = "sha256:77eb101048d95f982c0353e9233404889dcd7a6fc244c107836c0e2fc9cf7d9d", size = 268719, upload-time = "2026-09-11T19:09:12.88Z" },
] ]
[[package]] [[package]]
@@ -52,14 +53,14 @@ wheels = [
[[package]] [[package]]
name = "anyio" name = "anyio"
version = "4.14.1" version = "4.14.2"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "idna" }, { name = "idna" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/3b/72/5562aabb8dd7181e8e860622a38bea08d17842b99ecd4c91f84ac95251b0/anyio-4.14.1.tar.gz", hash = "sha256:8d648a3544c1a700e3ff78615cd679e4c5c3f149904287e73687b2596963629e", size = 254831, upload-time = "2026-06-24T20:56:06.017Z" } sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl", hash = "sha256:4e5533c5b8ff0a24f5d7a176cbe6877129cd183893f66b537f8f227d10527d72", size = 124875, upload-time = "2026-06-24T20:56:04.413Z" }, { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" },
] ]
[[package]] [[package]]
@@ -81,7 +82,6 @@ dependencies = [
{ name = "python-multipart" }, { name = "python-multipart" },
{ name = "sentry-sdk", extra = ["fastapi"] }, { name = "sentry-sdk", extra = ["fastapi"] },
{ name = "sqlmodel" }, { name = "sqlmodel" },
{ name = "tenacity" },
] ]
[package.dev-dependencies] [package.dev-dependencies]
@@ -95,21 +95,20 @@ dev = [
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "alembic", specifier = ">=1.12.1,<2.0.0" }, { name = "alembic", specifier = ">=1.19.1,<2.0.0" },
{ name = "email-validator", specifier = ">=2.1.0.post1,<3.0.0.0" }, { name = "email-validator", specifier = ">=2.1.0.post1,<3.0.0.0" },
{ name = "emails", specifier = ">=1.1.2,<2.0" }, { name = "emails", specifier = ">=1.1.2,<2.0" },
{ name = "fastapi", extras = ["standard"], specifier = ">=0.141.1,<1.0.0" }, { name = "fastapi", extras = ["standard"], specifier = ">=0.141.1,<1.0.0" },
{ name = "httpx", specifier = ">=0.25.1,<1.0.0" }, { name = "httpx", specifier = ">=0.25.1,<1.0.0" },
{ name = "jinja2", specifier = ">=3.1.4,<4.0.0" }, { name = "jinja2", specifier = ">=3.1.4,<4.0.0" },
{ name = "psycopg", extras = ["binary"], specifier = ">=3.3.4,<4.0.0" }, { name = "psycopg", extras = ["binary"], specifier = ">=3.3.6,<4.0.0" },
{ name = "pwdlib", extras = ["argon2", "bcrypt"], specifier = ">=0.3.0" }, { name = "pwdlib", extras = ["argon2", "bcrypt"], specifier = ">=0.3.1" },
{ name = "pydantic", specifier = ">2.0" }, { name = "pydantic", specifier = ">=2.13.5" },
{ name = "pydantic-settings", specifier = ">=2.2.1,<3.0.0" }, { name = "pydantic-settings", specifier = ">=2.2.1,<3.0.0" },
{ name = "pyjwt", specifier = ">=2.13.0,<3.0.0" }, { name = "pyjwt", specifier = ">=2.15.0,<3.0.0" },
{ name = "python-multipart", specifier = ">=0.0.27,<1.0.0" }, { name = "python-multipart", specifier = ">=0.0.27,<1.0.0" },
{ name = "sentry-sdk", extras = ["fastapi"], specifier = ">=2.66.1,<3.0.0" }, { name = "sentry-sdk", extras = ["fastapi"], specifier = ">=2.70.0,<3.0.0" },
{ name = "sqlmodel", specifier = ">=0.0.39,<1.0.0" }, { name = "sqlmodel", specifier = ">=0.0.46,<1.0.0" },
{ name = "tenacity", specifier = ">=8.2.3,<10.0.0" },
] ]
[package.metadata.requires-dev] [package.metadata.requires-dev]
@@ -117,7 +116,7 @@ dev = [
{ name = "coverage", specifier = ">=7.4.3,<8.0.0" }, { name = "coverage", specifier = ">=7.4.3,<8.0.0" },
{ name = "mypy", specifier = ">=1.8.0,<3.0.0" }, { name = "mypy", specifier = ">=1.8.0,<3.0.0" },
{ name = "pytest", specifier = ">=7.4.3,<10.0.0" }, { name = "pytest", specifier = ">=7.4.3,<10.0.0" },
{ name = "ruff", specifier = ">=0.2.2,<1.0.0" }, { name = "ruff", specifier = ">=0.16.0,<1.0.0" },
{ name = "ty", specifier = ">=0.0.25" }, { name = "ty", specifier = ">=0.0.25" },
] ]
@@ -166,42 +165,66 @@ wheels = [
[[package]] [[package]]
name = "ast-serialize" name = "ast-serialize"
version = "0.5.0" version = "0.8.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/81/9d/09e27731bd5864a9ce04e3244074e674bb8936bf62b45e0357248717adac/ast_serialize-0.5.0.tar.gz", hash = "sha256:5880091bfe6f4f986f22866375c2e884843e7a0b6343ae41aeea659613d879b6", size = 61157, upload-time = "2026-05-17T17:48:29.429Z" } sdist = { url = "https://files.pythonhosted.org/packages/e1/a9/11851c3e02a3fea2ddc9932d1fdc7d2edaeecc0d2e11bc5f2a7fde2b0934/ast_serialize-0.8.0.tar.gz", hash = "sha256:6c37c43e4004dfb42d321ddedc569dc17ff4259296f3af577c9ea46a809bc010", size = 845638, upload-time = "2026-08-07T11:29:02.152Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/c0/9a/13dde51ba9e15f8b97957ab7cb0120d0e381524d651c6bd630b9c359227f/ast_serialize-0.5.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8f5c14f169eb0972c0c21bada5358b23d6047c76583b005234f865b11f1fa00a", size = 1183520, upload-time = "2026-05-17T17:47:30.831Z" }, { url = "https://files.pythonhosted.org/packages/34/16/6e520b57cd8c75914b38c670ad4593d13c22911e4306cc7165dab8b0789b/ast_serialize-0.8.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:3d822605fa7bb326ef868d25fafced7fc660fa46d9b90c02ea86d5e2f5d325f7", size = 863924, upload-time = "2026-08-07T11:27:34.579Z" },
{ url = "https://files.pythonhosted.org/packages/37/de/5a7f0a9fe68944f536632a5af84676739c7d2582be42deb082634bf3a754/ast_serialize-0.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7d1a2de9de5be04652f0ed60738356ef94f66db37924a9499fffe98dc491aa0b", size = 1175779, upload-time = "2026-05-17T17:47:32.551Z" }, { url = "https://files.pythonhosted.org/packages/03/e1/48802de9b22a2bcad42ec80601a17e3f69172fe4f590e6311bcc2b323aeb/ast_serialize-0.8.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:2efa40b068197d5efb62655b43baadb842ed71c4958cccd3e8b86a35726f0119", size = 1177662, upload-time = "2026-08-07T11:27:36.196Z" },
{ url = "https://files.pythonhosted.org/packages/9c/81/0bb853e76e4f6e9a1855d569003c59e19ffac45f7079d91505d1bb212f92/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be5173fb66f9b49026d9d5a2ff0fc7c7009077107c0eb285b2d60fdf1fe10bd1", size = 1233750, upload-time = "2026-05-17T17:47:34.731Z" }, { url = "https://files.pythonhosted.org/packages/38/d4/323438db76bded3a1f3523a3167b8325916b2ddceb2107a330c6ec9fcf4d/ast_serialize-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:db1b957291bca08c7e72f43a12357b2948e20775d970e3fc3dac0aa3160ab725", size = 1167072, upload-time = "2026-08-07T11:27:37.646Z" },
{ url = "https://files.pythonhosted.org/packages/e5/d3/4cf705beeccc08754d0bbda99aefff26110e209b9a07ac8a6b60eec48531/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8015cd071ac1339924ee2b8098c93e00e155f30a16f40ec9816fcf84f4753f6", size = 1235942, upload-time = "2026-05-17T17:47:36.287Z" }, { url = "https://files.pythonhosted.org/packages/77/82/53c5400b54144b56de8ed7f957fd1ccd97e42482009292ab46121d15f8dd/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdc0d5b18ff8fb364e87923e47c0a91d0d69dbcaeaa274591f7fd26892cc3a3a", size = 1225497, upload-time = "2026-08-07T11:27:39.225Z" },
{ url = "https://files.pythonhosted.org/packages/26/c8/ee097e437ea27dd2b8b227865c875492b585650a5802a22d82b304c8201b/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5499e8797edff2a9186aa313ed382c6b422e798e9332d9953badcee6e69a88f2", size = 1442517, upload-time = "2026-05-17T17:47:38.17Z" }, { url = "https://files.pythonhosted.org/packages/44/5f/36c07327a8b91303fbf1382c7c3e8a2902072dbe1b9546138a5288e75ff0/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9da7330f3e235bf7da89b8d39205c6350fc0c08a85379743f2df9fff87d6d980", size = 1227101, upload-time = "2026-08-07T11:27:40.799Z" },
{ url = "https://files.pythonhosted.org/packages/ff/bd/68063442838f1ba68ec72b5436430bc75b3bb17a1a3c3063f09b0c05ae2b/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6848f2a093fb5548751a9a09bff8fcd229e2bbeb0e3331f391b6ae6d26cd9903", size = 1254081, upload-time = "2026-05-17T17:47:39.826Z" }, { url = "https://files.pythonhosted.org/packages/9d/48/5adf5c67addc7ddb328122208c6d375a84cf154984f412b4087330a157bd/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3186969ee66a9863b00acc6523ace44c56974eecb348a7ea4b228d9f0b80e19", size = 1424001, upload-time = "2026-08-07T11:27:42.708Z" },
{ url = "https://files.pythonhosted.org/packages/50/e2/1e520793bc6a4e4524a6ab022391e827825eaa0c3811828bfdc6852eca26/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:832d4c998e0b091fd60a6d6bceee535483c4d490de9ba85003af835225719261", size = 1259910, upload-time = "2026-05-17T17:47:41.369Z" }, { url = "https://files.pythonhosted.org/packages/38/a1/70074dd3869d2b0e934f91891d8d6b734361cd3b80f85ca7ece2e668ecdd/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40a57b73731be45da4fa41430c4d5dc94a24b3a4faba7b9e069978c0402064ea", size = 1245545, upload-time = "2026-08-07T11:27:44.4Z" },
{ url = "https://files.pythonhosted.org/packages/4e/e1/49b60f467979979cfe6913b43948ff25bca971ad0591d181812f163a988e/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:16db7c62ec0b8efe1d7afd283a388d8f74f2605d56032e5a37747d2de8dba027", size = 1250678, upload-time = "2026-05-17T17:47:43.702Z" }, { url = "https://files.pythonhosted.org/packages/e3/be/53b9c0a8a6399950c2e3546bdfab96d2b299d5b114b47eb94fd3c49c4054/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5075b9da3ef807eda752502446dfecea3b381c4900b7e27a5d5f4f899eb39951", size = 1248961, upload-time = "2026-08-07T11:27:45.781Z" },
{ url = "https://files.pythonhosted.org/packages/74/ba/66ab9555de6275677566f6574e5ef6c29cb185ea866f643bc06f8280a8ee/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:baf5eb061eb5bccade4128ad42da33787d72f6013809cd1b590376ece8b3c937", size = 1301603, upload-time = "2026-05-17T17:47:46.256Z" }, { url = "https://files.pythonhosted.org/packages/eb/13/3651d3812548a2bda15e26e5dd51aadb48cf682d0865370255fcf0e367dd/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:293cc1c5bfa741f8e3fbe8175b9c07beee487c9a6fdbb25a5acad9f1df2d30a9", size = 1243877, upload-time = "2026-08-07T11:27:47.325Z" },
{ url = "https://files.pythonhosted.org/packages/66/42/6aca9b9abc710014b2be9059689e5dd1679339e78f567ffb4d255a9e2050/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:104e4a35bd7c124173c41760ef9aaea17ddb3f86c65cb643671d59afbe3ee94c", size = 1410332, upload-time = "2026-05-17T17:47:47.899Z" }, { url = "https://files.pythonhosted.org/packages/21/a0/521f0bf000f675e9312a4aae2c8ba7a992405d072a85c485e08fd59433b9/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0910c3442a75216dde0f102d854ba2aaa71d2482e0ee213630b9bf29584fba3", size = 1293903, upload-time = "2026-08-07T11:27:49.264Z" },
{ url = "https://files.pythonhosted.org/packages/47/68/2f76594432a22581ecf878b5e75a9b8601c24b2241cf0bbeb1e21fcf370c/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:36be371028fc1675acb38a331bde160dbab7ff907fdf00b67eb6911aa106951b", size = 1509979, upload-time = "2026-05-17T17:47:50.942Z" }, { url = "https://files.pythonhosted.org/packages/b1/7e/402fc902568aa2ee65865a3e151f000db0153da8ce6b1be4c9c349025f8d/ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:43dd6d596879bb1cb8a12cc9dae7bb10090a39a35883026c24f82488a195619a", size = 1401070, upload-time = "2026-08-07T11:27:50.947Z" },
{ url = "https://files.pythonhosted.org/packages/40/ac/a93c9b58292653f6c595752f677a08e608f903b710594909e9231a389b3b/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:061ee58bdb52341c8201a6df41182a977736bae3b7ded87ca7176ca25a8a47ab", size = 1505002, upload-time = "2026-05-17T17:47:54.093Z" }, { url = "https://files.pythonhosted.org/packages/ff/7c/97d4b66c057f1706fc8be6dd532cc77c988794357c8f4ffdb6adabb39562/ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8c9d537f59e936392cfd3597789d1390304dd659efc3c486ce7f40fb6b8a9f53", size = 1502602, upload-time = "2026-08-07T11:27:52.364Z" },
{ url = "https://files.pythonhosted.org/packages/14/2e/b278f68c497ee2f1d1576cbbef8db5281cd4a5f2db040537592ac9c8862e/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b15219e9cdc9f53f6f4cb51c009203507228226148c05c5e8fe451c28b435eb3", size = 1456231, upload-time = "2026-05-17T17:47:56.311Z" }, { url = "https://files.pythonhosted.org/packages/89/6f/72cc3b71562001bba46e898ccfbf1844f7939b3e28912736206102f2e5a8/ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:f0190a33d7f97c65e9069f7a7f40499eea6b5cbe260c558378109caf20ce934b", size = 1495848, upload-time = "2026-08-07T11:27:53.803Z" },
{ url = "https://files.pythonhosted.org/packages/0b/43/419be1c566a4c504cd8fd60ce2f84e790f295495c0f327cfaeadf3d51012/ast_serialize-0.5.0-cp314-cp314t-win32.whl", hash = "sha256:842d1c004bb466c7df036f95fabef789570541922b10976b12f5592a69cf0b38", size = 1058668, upload-time = "2026-05-17T17:47:58.305Z" }, { url = "https://files.pythonhosted.org/packages/a0/53/d6f629d1e49308b2f363dae028baa213ec222c9106fa1f7f0d1f7b41499a/ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:77308ae6c5cf5264cc0f01a7c556ec77a9e68eb1f61b093534d698139fdc3b14", size = 1556556, upload-time = "2026-08-07T11:27:55.342Z" },
{ url = "https://files.pythonhosted.org/packages/03/6f/c9d4d549295ed05111aeb8853232d1afd9d0a179fddb01eeffbb3a4a6842/ast_serialize-0.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b0c06d760909b095cc466356dfccd05a1c7233a6ca191c020dca2c6a6f16c24c", size = 1101075, upload-time = "2026-05-17T17:48:00.35Z" }, { url = "https://files.pythonhosted.org/packages/ee/22/340f35dd8dfc6d412d53dc20699ca014b8d228db923e8ed4759c512b162c/ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8d53a23f27e1ed3a36b2d26fd2a1a6228c8e85a1ed62ff7cdb44bd610769f20a", size = 1417822, upload-time = "2026-08-07T11:27:56.712Z" },
{ url = "https://files.pythonhosted.org/packages/d0/8e/d00c5ab30c58222e07d62956fca86c59d91b9ad32997e633c38b526623a3/ast_serialize-0.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:787baedb0262cc49e8ce37cc15c00ae818e46a165a3b36f5e21ed174998104cb", size = 1075347, upload-time = "2026-05-17T17:48:01.753Z" }, { url = "https://files.pythonhosted.org/packages/11/29/6dde5c13fbebc051d3a6df4ec0a6fd1d5359333cc1193f7f609f3410b4d8/ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ffa5e7cb08f96fed9121f77b224151e41caf88feab9d652bb46c78202b6fbeda", size = 1445153, upload-time = "2026-08-07T11:27:58.275Z" },
{ url = "https://files.pythonhosted.org/packages/e0/9e/dc2530acb3a60dc6e46d65abf27d1d9f86721694757906a148d90a6860de/ast_serialize-0.5.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:0668aa9459cfa8c9c49ddd2163ebcf43088ba045ef7492af6fe22e0098303101", size = 1191380, upload-time = "2026-05-17T17:48:03.738Z" }, { url = "https://files.pythonhosted.org/packages/62/c5/f473a8ed030f7a0ca24b9849cca184677a50c053867a7b808c2e1289bbd3/ast_serialize-0.8.0-cp314-cp314t-win32.whl", hash = "sha256:fa70ed4dea0bb18b30a1789c77baa701d0ef30c474f2ccabdea61e25623a8827", size = 1063711, upload-time = "2026-08-07T11:27:59.793Z" },
{ url = "https://files.pythonhosted.org/packages/26/0a/bd3d18a582f273d6c843d16bb9e22e9e16365ff7991e92f18f798e9f1224/ast_serialize-0.5.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:bf683d6363edf2b39eed6b6d4fe22d34b6203867a67e27134d9e2a2680c4bc4a", size = 1183879, upload-time = "2026-05-17T17:48:05.463Z" }, { url = "https://files.pythonhosted.org/packages/23/63/39e171fcd38ca057c2e1979d5ee81ac7a3502784abe3d83df7454f7a0978/ast_serialize-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d8b3c8eee4c1baef9d4e84d2a59a805501617127be42615cb48970b15b0892b6", size = 1103740, upload-time = "2026-08-07T11:28:01.405Z" },
{ url = "https://files.pythonhosted.org/packages/40/ae/1f919100f8620887af58fcc381c61a1f218cdf89c6e155f87b213e61010a/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cc22cf0c9be65e71cf88fda130af60d61eb4a79370ad4cfe7900d48a4aa2211", size = 1244529, upload-time = "2026-05-17T17:48:07.008Z" }, { url = "https://files.pythonhosted.org/packages/21/1c/d00762b399e7726d68d0a088cc946e3a4c60f1c6176f557608f672f627f3/ast_serialize-0.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:ac4f0a83c55a9b782f79ad55a5247b7db123c1db405959791c2ef886e9710c9f", size = 1076021, upload-time = "2026-08-07T11:28:02.947Z" },
{ url = "https://files.pythonhosted.org/packages/c6/ca/6376559dcce707cdbc1d0d9a13c8d3baaaa501e949ce0ebdc4230cd881aa/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f66173891548c9f2726bf27957b41cabce12fa679dc6da505ddbde4d4b3b31cf", size = 1240560, upload-time = "2026-05-17T17:48:08.46Z" }, { url = "https://files.pythonhosted.org/packages/4c/11/911210c3c78923273a9211a2b6cfc4c8aa723b30dab3e1c8d19afb983b40/ast_serialize-0.8.0-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:86b8a1e6d90467345356098b040150e82fbc26d24a7a202224b13dc1f6264ca0", size = 1177715, upload-time = "2026-08-07T11:28:04.654Z" },
{ url = "https://files.pythonhosted.org/packages/35/b2/a620e206b5aeb7efbf2710336df57d457cffbb3991076bbcc1147ef9abd4/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e42d729ef2be96a14efbad355093284739e3670ece3e534f82cc8832790911d9", size = 1451172, upload-time = "2026-05-17T17:48:09.922Z" }, { url = "https://files.pythonhosted.org/packages/77/89/6282881c8587606638db153cbe21e1e0c4d1f3970dee1aa0610a1c62a026/ast_serialize-0.8.0-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:39e92ff8e8cb45947fe9007174b2950e1fb098e6abd00266a13cd3bcf6675068", size = 1169347, upload-time = "2026-08-07T11:28:06.1Z" },
{ url = "https://files.pythonhosted.org/packages/fa/e0/4ad5c04c24a40481b2935ce9a0ccdb6023dc8b667167d06ae530cc3512f2/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b725026bafa801dbd7310eb13a75f0a2e370e7e51b2cb225f9d21fcfadf919ee", size = 1265072, upload-time = "2026-05-17T17:48:11.469Z" }, { url = "https://files.pythonhosted.org/packages/97/78/a9f846a03a340ff3728c915f23338ca742742f3292700559cdb3ad999b1e/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c85d8d18db5b2dfcb3b7e38a4d600ca35504c0ed8a6f75cd1c811e4ffe248a15", size = 1225916, upload-time = "2026-08-07T11:28:07.654Z" },
{ url = "https://files.pythonhosted.org/packages/b2/71/4d1d479aa56d0101c40e17720c3d6ac2af7269ea0487a80b18e7bfd1a5b7/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b54f60c1d78767a53b67eaa663f0dfac3afe606aa07f1301572f588b73d64809", size = 1270488, upload-time = "2026-05-17T17:48:13.575Z" }, { url = "https://files.pythonhosted.org/packages/c0/15/aba6ef8a988a6eceb6f0359589aac509e29ae2dba67fd9bfd5af0c3f13e7/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9830ff7e764f74d9eefb01170c61a9f0fd2c027dac5fcb72e064decd57d56371", size = 1227135, upload-time = "2026-08-07T11:28:09.504Z" },
{ url = "https://files.pythonhosted.org/packages/6d/4f/0de1bbe06f6edef9fde4ed12ca8e7b3ec7e6e2bd4e672c5af487f7957665/ast_serialize-0.5.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:27d51654fc240a1e87e742d353d98eb45b75f62f129086b3596ab53df2ac2a43", size = 1260702, upload-time = "2026-05-17T17:48:15.141Z" }, { url = "https://files.pythonhosted.org/packages/94/29/3f63d696ea7c5b8abadcecc3505be51bd900daaccc522ed8322fa5b05a93/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6479d9722a4cd21b578f5478074c41e6169f04811996ec881655560f703a5bba", size = 1425040, upload-time = "2026-08-07T11:28:11.044Z" },
{ url = "https://files.pythonhosted.org/packages/75/61/e00872439cfdddcc3c1b6cdaa6e5d904ba8e26a18807c67c4e14409d0ca8/ast_serialize-0.5.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c36237c46dd1674542f2109740ea5ea485a169bf1431939ada0434e17934", size = 1311182, upload-time = "2026-05-17T17:48:16.779Z" }, { url = "https://files.pythonhosted.org/packages/e2/5d/0aac338604ff59df5774d4304307898982252f325ff7cafe31d52fedcb65/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a63bed264e818cd83eec11feed0f50aa162542b91132ef58afebc857182763a5", size = 1246278, upload-time = "2026-08-07T11:28:12.519Z" },
{ url = "https://files.pythonhosted.org/packages/76/8e/699a5b955f7926956c95e9e1d74132acad73c2fe7a426f94da89123c20aa/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1943db345233cc7194a470f13afa9c59772c0b123dea0c9414c4d4ca54369759", size = 1421410, upload-time = "2026-05-17T17:48:18.527Z" }, { url = "https://files.pythonhosted.org/packages/23/ca/9f1ef795bb724719532bd86dbec11e5b66857d3fbe9b6772baec0191a6ed/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d187197d234aa45d6cfa2b096be5f666e8cc2e7eb3722d0ab8926293cf5720c", size = 1250029, upload-time = "2026-08-07T11:28:13.896Z" },
{ url = "https://files.pythonhosted.org/packages/a9/ae/d5b7626874478997adc7a29ab28accf21e596fb590c944290401dfd0b29e/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:df1c00022cbbcb064bfaa505aa9c9295362443ce5dacb459d1331d3da353f887", size = 1516587, upload-time = "2026-05-17T17:48:20.133Z" }, { url = "https://files.pythonhosted.org/packages/dc/25/5e061372d2ed953b9ba3b9c4f73de3b8e9234cda3f6c088db4686801d0e1/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:2d39a56282cfcc0d8eeea37267c754be59c98d48505c23b1dae5c6011f3813dd", size = 1243575, upload-time = "2026-08-07T11:28:15.37Z" },
{ url = "https://files.pythonhosted.org/packages/0c/ce/b59e02a82d9c4244d64cde502e0b00e83e38816abe19155ceb5437402c7f/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:cae65289fc456fde04af979a2be09302ef5d8ab92ef23e596d6746dc267ada27", size = 1515171, upload-time = "2026-05-17T17:48:21.921Z" }, { url = "https://files.pythonhosted.org/packages/a8/c1/ae7da218053120635a4ca802366c69f707203641af95372eeb83f70dfd52/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f7cc5f10386994c0f4844f1e6d6a97127e9b478660eb6dec2b257644f0acab64", size = 1294396, upload-time = "2026-08-07T11:28:16.813Z" },
{ url = "https://files.pythonhosted.org/packages/8b/38/d8d90042747d05aa08d4efcf1c99035a5f670a6bf4c214d31644392afbca/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:239a4c354e8d676e9d94631d1d4a64edc6b266f86ff3a5a80aedd344f342c01d", size = 1464668, upload-time = "2026-05-17T17:48:23.544Z" }, { url = "https://files.pythonhosted.org/packages/2e/89/271d1f49c5269fcddcc789ea3f25be401f6723fc1138aeda539f4d05516d/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:6102f2f985c2e542be85cd857678ec9356fefa792b93cadfadd31139f5696f27", size = 1401987, upload-time = "2026-08-07T11:28:18.333Z" },
{ url = "https://files.pythonhosted.org/packages/dd/51/5b840c4df7334104cecffa28f23904fe81ca89ca223d2450e288de39fd3c/ast_serialize-0.5.0-cp39-abi3-win32.whl", hash = "sha256:143a4ef63285a075871908fda3672dc21864b83a8ec3ee12304aa3e4c5387b9a", size = 1068311, upload-time = "2026-05-17T17:48:25.027Z" }, { url = "https://files.pythonhosted.org/packages/55/be/4e7d77fcf571ac7cb5cf7115a20c36642bd7d29473b45dfaaefeb9618f90/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:3a8660fe66667b76a6e9dccd1d33e66b229fde3b308db991c041609226c005b6", size = 1502904, upload-time = "2026-08-07T11:28:20.039Z" },
{ url = "https://files.pythonhosted.org/packages/41/11/ca5672c7d491825bc4cd6702dea106a6b60d928707712ec257c7833ae476/ast_serialize-0.5.0-cp39-abi3-win_amd64.whl", hash = "sha256:cf25572c526add400f26a4750dc6ce0c3bb93fc1f75e7ae0cad4ce4f2cd5c590", size = 1108931, upload-time = "2026-05-17T17:48:26.591Z" }, { url = "https://files.pythonhosted.org/packages/8b/ae/ed1de2db7e019d4236fbc164ffa5ef9a6022a300a342bbf142d21b7c141e/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:e7266307e5fba39836edb79def8608887af48820508bff3c5f2941e1e04d1534", size = 1496967, upload-time = "2026-08-07T11:28:21.734Z" },
{ url = "https://files.pythonhosted.org/packages/45/19/cc8bd127d28a43da249aa955cfd164cf8fd534e79e42cea96c4854d72fd0/ast_serialize-0.5.0-cp39-abi3-win_arm64.whl", hash = "sha256:92a31c9c20d25a076edaeec76b128a3535d74a24f340b9a8a7e96c9b86dc9642", size = 1081181, upload-time = "2026-05-17T17:48:28.122Z" }, { url = "https://files.pythonhosted.org/packages/92/89/5fea507fae5c5f18b7dc7f95e5c00956574b8c717b8fd2049c504fab0b18/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca7e6fd1ad845d1cc649dc2ecd499db2f8f46af5bf8da7b70dd858774cc038b", size = 1559041, upload-time = "2026-08-07T11:28:23.194Z" },
{ url = "https://files.pythonhosted.org/packages/42/71/478d69df21b64e064554a68134c94be304270316ca676a94e63c389a636a/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:2880350b13d3eae69a0d70bc1fb6c9bfaca4dbd0e20ba8cd1aa483080b56ff06", size = 1417367, upload-time = "2026-08-07T11:28:24.601Z" },
{ url = "https://files.pythonhosted.org/packages/5e/2d/8962dc8d5b3a9dc27b36f9db199afa25264c741505469d9ec10ffbfd2ba7/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:ab0f9a59f7d63d0d441b56b9a818b273705264352d5115cfee12e940e816d958", size = 1446178, upload-time = "2026-08-07T11:28:26.152Z" },
{ url = "https://files.pythonhosted.org/packages/4f/22/14d2ad4fd1d1bcd0dc687ca268e0630069f45162496260c0efb70ee0ea72/ast_serialize-0.8.0-cp315-abi3.abi3t-win32.whl", hash = "sha256:0485a25ef519c62e749ee3c1ad8070e591b380d67226349eb5a70b228dc1ac4a", size = 1063811, upload-time = "2026-08-07T11:28:27.864Z" },
{ url = "https://files.pythonhosted.org/packages/18/1d/84a327c0202a41aa5fdba3ade33904d6d8f3b9e6806fa83568d835395850/ast_serialize-0.8.0-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:bd84d60bca7079e741be4ac5dbe237751a59d7f6f9f0126b11880d63822cbe16", size = 1105518, upload-time = "2026-08-07T11:28:29.691Z" },
{ url = "https://files.pythonhosted.org/packages/8c/92/74556dec52fde85a2ad84ed159991b916241043788609c15d8b77e14570b/ast_serialize-0.8.0-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:057769b5921336eb2d9124f2a731b42ed05ffdac559b840dbdf6f3937cf153dc", size = 1076319, upload-time = "2026-08-07T11:28:31.282Z" },
{ url = "https://files.pythonhosted.org/packages/d1/5d/c650b1f2cc1e75193358da95a080261422e8cd10b66d7370b1688c9915c5/ast_serialize-0.8.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:a02cbed7d8bfdcdee88edaac12bd50d53d9953aaa2e1852ef078625be5f1c0b5", size = 852914, upload-time = "2026-08-07T11:28:32.929Z" },
{ url = "https://files.pythonhosted.org/packages/d9/e3/6142e920fec6ef7bccabd8c24ed8ed99f8bdc6cb8b065e1df7c6a3b2d667/ast_serialize-0.8.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e1bd223df0f6c96b396975fa604cb33bce53d9b4a0185490be4c4a289f7c9c87", size = 1184007, upload-time = "2026-08-07T11:28:34.654Z" },
{ url = "https://files.pythonhosted.org/packages/a6/e9/6e8be8df02b35d85e2b8809f7f1cfa290bdf5882b55127a539d049482db0/ast_serialize-0.8.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ddd3b61f45c132da66c5476b281891e08c1fd87fbdabe8a6973e1622efc85f06", size = 1177588, upload-time = "2026-08-07T11:28:36.318Z" },
{ url = "https://files.pythonhosted.org/packages/8c/80/7e0fd2e2e2aba257820db4a8657c4c356844d36b914b20a4af294bcfb902/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f9caa63fad8241257ae401b5ff0a64026c6adb36b8e86cbe8782d9ea505daf6", size = 1234575, upload-time = "2026-08-07T11:28:37.772Z" },
{ url = "https://files.pythonhosted.org/packages/b0/6a/3bae0af06f9b1bae3001c44d64215f5b567877e7aae9ffd45db11c3a7647/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3926fa117b5e65019853a2969966d11c7175af377a3425991f3fe73784412405", size = 1236015, upload-time = "2026-08-07T11:28:39.14Z" },
{ url = "https://files.pythonhosted.org/packages/6f/c4/ce2d41a1bc22508e82618901f7e10f2a5e2f9556553fea90624daf9875e2/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:485f1113af805e9e170b95ef993ca3fbd4f89c04bab25c58b4fc632d854801ab", size = 1432808, upload-time = "2026-08-07T11:28:40.664Z" },
{ url = "https://files.pythonhosted.org/packages/1a/90/f5058f209756dd70e958b7538aaa82d25d24944baf9ec8ae6f27b06fcacc/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ccebbed24f1281062d5852353c72c47502955926cfcb8345ffb3a44d87ff3d3", size = 1256251, upload-time = "2026-08-07T11:28:42.223Z" },
{ url = "https://files.pythonhosted.org/packages/bf/32/7f77ea87fa0836daab706ed5cb7f903bb25fa26a77439011aee626af11d8/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:252f883290d1cdb728eb7fe1d9a7221b88af5a329aae0bc91ddee4dafb820331", size = 1258574, upload-time = "2026-08-07T11:28:43.751Z" },
{ url = "https://files.pythonhosted.org/packages/eb/5a/75b82ad2725b5e8e8c742732f9e76c6738a292d0709e1f60d10a973730b4/ast_serialize-0.8.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:96abc072ad29db8d02194afd47d68987322622787daceae82398d7b69f3ba2e6", size = 1254075, upload-time = "2026-08-07T11:28:45.28Z" },
{ url = "https://files.pythonhosted.org/packages/4e/54/8c20ed4eea805516a3fd23dd4a721ce28c64f50f0e4b359969f60a8c97a6/ast_serialize-0.8.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9118ad3e369727060b2696fc4078f250ecffca4248ba87f537f55cea9f9dce06", size = 1301018, upload-time = "2026-08-07T11:28:46.851Z" },
{ url = "https://files.pythonhosted.org/packages/cb/5b/9f14430f12fe830b656fb38f8e2e05ee13b02a88967660bef46af0ab22a8/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f359df4bd921918af8bebd142a376c77511d7151cc8ba852760b587b5a4a54f3", size = 1409951, upload-time = "2026-08-07T11:28:48.312Z" },
{ url = "https://files.pythonhosted.org/packages/2d/3d/084882eca93c842bd4262591a071ec7f825340644035e51501208cc5a8d4/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:e94f9121d13fa36cbf21314783c77d05ae3a0868decd18cf5233fdcc6de49ac8", size = 1509544, upload-time = "2026-08-07T11:28:49.847Z" },
{ url = "https://files.pythonhosted.org/packages/ce/73/ea84852096c2036c61cc0b2f97b90242207419f534dc671060ee1c8e05cb/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:54f95b486018d262bcb387a9afd96f0da74508b442762b80c769454a6fbb3ee3", size = 1505671, upload-time = "2026-08-07T11:28:51.239Z" },
{ url = "https://files.pythonhosted.org/packages/cb/88/287b9a5300c1f2f651d259f670931b63110adc265b7613c885b44c5bc53d/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4c38b915511e32bc718c49dbce98ff9af36bac0ad6a604f58000cd5e3aecdba7", size = 1563685, upload-time = "2026-08-07T11:28:53.112Z" },
{ url = "https://files.pythonhosted.org/packages/ee/f3/1bc3a79afcf0c2a8d2c37182d0d659d1545a9d7f7f6dc9cf3e63d6c17135/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:9a2ef9cf12f2de4f1028c42c1dd7d775255e0fb3e5bb48896c97e35ef52366fe", size = 1427977, upload-time = "2026-08-07T11:28:54.418Z" },
{ url = "https://files.pythonhosted.org/packages/5c/cd/440c798957e14e31776bfeb024d8fafe0bb1d5b89c51c2f067e69938f7b0/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6f18048fe9f6dd266bd577cdec48bdcecb74faaa01fe941324435483b013ed2a", size = 1454335, upload-time = "2026-08-07T11:28:55.968Z" },
{ url = "https://files.pythonhosted.org/packages/4f/4a/587eb36dcc240a54c8660f599464516b469ecad96f0dbdb6bccbedb50745/ast_serialize-0.8.0-cp39-abi3-win32.whl", hash = "sha256:31883542dd6c94d178f5db3d32fbd69c5eb88b3a7c018e7ac8cc0c45195ddbed", size = 1068858, upload-time = "2026-08-07T11:28:57.541Z" },
{ url = "https://files.pythonhosted.org/packages/5f/a4/3e887bbd92164e183cb6e412c6a3e9198ddd446d7fe405958293ef5ef49c/ast_serialize-0.8.0-cp39-abi3-win_amd64.whl", hash = "sha256:861794565b06337005c1447ef23103a3d5a627d08bdc827870d00d0b28ef5f51", size = 1111839, upload-time = "2026-08-07T11:28:59Z" },
{ url = "https://files.pythonhosted.org/packages/25/6c/b400476d3ceba681ab929787edc9554f6d88fcc69435eb681b00fc0457a5/ast_serialize-0.8.0-cp39-abi3-win_arm64.whl", hash = "sha256:b2a5978662fd4db463dfb4b974d2b10ac6430b98f5333aabc7051909df3561d0", size = 1083655, upload-time = "2026-08-07T11:29:00.349Z" },
] ]
[[package]] [[package]]
@@ -257,11 +280,11 @@ wheels = [
[[package]] [[package]]
name = "certifi" name = "certifi"
version = "2026.6.17" version = "2026.10.19"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" } sdist = { url = "https://files.pythonhosted.org/packages/a6/bd/1436c50cc46699de0230fbaacc1d1de7bfec413176099949e9d5112e88f2/certifi-2026.10.19.tar.gz", hash = "sha256:035c1e85572b141bf1c3a3eff0e33a8eb74917522f2559075ff8d2fea780a1ae", size = 137598, upload-time = "2026-10-20T02:24:16.604Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" }, { url = "https://files.pythonhosted.org/packages/c4/51/29046e48f693c54e5b7412216d447a1d8362c9653148d98c02cdf65fa733/certifi-2026.10.19-py3-none-any.whl", hash = "sha256:42d35340cc5b9ba78e818ddbff0c5b1c2b779de448a340fd1b989f5fa8670e1b", size = 136464, upload-time = "2026-10-20T02:24:15.072Z" },
] ]
[[package]] [[package]]
@@ -320,41 +343,71 @@ wheels = [
[[package]] [[package]]
name = "coverage" name = "coverage"
version = "7.14.3" version = "7.16.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b4/91/0a7c28934e50d8ac9a7b117712d176f2953c3170bccced5eaacfa3e96175/coverage-7.14.3.tar.gz", hash = "sha256:1a7563a443f3d53fdeb040ec8c9f7466aed7ca3dc5891aa09d3ca3625fa4387f", size = 924398, upload-time = "2026-06-22T23:10:25.584Z" } sdist = { url = "https://files.pythonhosted.org/packages/65/2d/c738872f477f5687152acae68635790387425d407ae37dd3d3a8a6692307/coverage-7.16.1.tar.gz", hash = "sha256:f83981779bcf9dfa06fa0a8d4cb43e0faec1706328ce07aa3e7b665b4ac0f210", size = 969651, upload-time = "2026-09-13T19:12:21.422Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/61/56/14e3b97facbfa1304dd19e676e26599ad359f04714bed32f7f1c5a88efdc/coverage-7.14.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:605ab2b566a22bd94834529d66d295c364aba84afd3e5498285c7a524017b1fc", size = 220741, upload-time = "2026-06-22T23:09:21.616Z" }, { url = "https://files.pythonhosted.org/packages/8e/b4/2a7c793965bae9f067aabab793a44d7a2f3ee7fb16b01ce1976bbd4a0218/coverage-7.16.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:cc0b37fe6f5ce5f1ccc62ad4fa9b1ad201d8e9b6027fd5e0170877beee4b2d15", size = 223546, upload-time = "2026-09-13T19:10:06.019Z" },
{ url = "https://files.pythonhosted.org/packages/12/1d/db378b5cca433b90b893f26dab728b280ddd89f272a1fdfed4aeaa05c686/coverage-7.14.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a3c2134809e80fac091bfed18a6991b5a5eb5df5ae32b17ac4f4f99864b73dd7", size = 221068, upload-time = "2026-06-22T23:09:23.452Z" }, { url = "https://files.pythonhosted.org/packages/ef/e2/633469076a2dbbea036cc15a268a3a5d6b2c7dd5d9a9567b2553dfc5ad61/coverage-7.16.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6618f481053b63fc6121faf8fc676bd9b7163c2a19d9e984a2e850002c28ab57", size = 223881, upload-time = "2026-09-13T19:10:08.246Z" },
{ url = "https://files.pythonhosted.org/packages/47/f0/3f8421b20d9c4fcd39be9a8ca3c3fda8bc204b44efbd09fede153afd3e2f/coverage-7.14.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c02efd507227bde9969cab0db8f48890eb3b5dcad6afac57a4792df4133543ce", size = 252117, upload-time = "2026-06-22T23:09:25.458Z" }, { url = "https://files.pythonhosted.org/packages/de/c3/f06150c13284569d53273b909f31222874276a595637b7852571dfeb2c18/coverage-7.16.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fa02d561eb1d8d2f8ba43ba6e3cef4c6c402a3b632a9460fa329fcadcd5df6a3", size = 254919, upload-time = "2026-09-13T19:10:10.254Z" },
{ url = "https://files.pythonhosted.org/packages/27/ca/59ea35fb99743549ec8b37eff141ece4431fea590c89e536ed8032ef45cf/coverage-7.14.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1bb93c2aa61d2a5b38f1526546d95cf4132cb681e541a337bf8dfd092be816e5", size = 254622, upload-time = "2026-06-22T23:09:27.523Z" }, { url = "https://files.pythonhosted.org/packages/d5/40/47e25b215ae18a29010c8e29be8782a6e04d18ba6224be2bf6cebfce6427/coverage-7.16.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bc5354a124799f1f87b7637bbe6f18cd4bc66a1f37f6aa2b5db40f9adad531dc", size = 257428, upload-time = "2026-09-13T19:10:12.124Z" },
{ url = "https://files.pythonhosted.org/packages/c8/25/ec6de51ae7493b92a1cf74d1b763121c29636759167e2a593ba4db5881e4/coverage-7.14.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f502e948e03e866538048bba081c075caaa62e5bda6ea5b7432e45f587eb462a", size = 255968, upload-time = "2026-06-22T23:09:29.43Z" }, { url = "https://files.pythonhosted.org/packages/27/4b/1e2a4267d14cbd12a8489364a9d40020233e6be836d929b363f0e77209e2/coverage-7.16.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34bafe9f4094315248573e6223e11af0ec1b25f9cbca43bf0e9a26a189ba2751", size = 258771, upload-time = "2026-09-13T19:10:14.031Z" },
{ url = "https://files.pythonhosted.org/packages/5d/05/c8bfc77823f42b4664fb25842f13b567022f6f84a4c83c8ecbb16734b7cb/coverage-7.14.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9973ef2463f8e6cfb61a6324126bb3e17d67a85f22f58d856e583ea2e3ca6501", size = 258284, upload-time = "2026-06-22T23:09:31.397Z" }, { url = "https://files.pythonhosted.org/packages/be/2e/9aa6146cea929fab9185bb2642ffef7f47520a6e5efe407f75f9b12f4cf0/coverage-7.16.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:29c4d3e32a3b5efa420a3dc627c7e570deb80ef997def52c7686a474f5edc7ab", size = 261086, upload-time = "2026-09-13T19:10:16.213Z" },
{ url = "https://files.pythonhosted.org/packages/f6/15/1d1b242027124a32b26ef01f82018b8c4ef34ef174aa6aeba7b1eeef48e8/coverage-7.14.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9be4e7d4c5ca0427889f8f9d614bd630c2be741b1de7699bca3b2b6c0e41003e", size = 252143, upload-time = "2026-06-22T23:09:33.256Z" }, { url = "https://files.pythonhosted.org/packages/13/3c/f9ad8bcd4fb3d21c9d20a16d6d6c6f999eee8f4498ed7659a3dbd2f4b74a/coverage-7.16.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2066c447fdd0bca39a9633a082d8ce67bf9a539a203b85059a364a405dc9fe9", size = 254895, upload-time = "2026-09-13T19:10:18.602Z" },
{ url = "https://files.pythonhosted.org/packages/74/b6/d2a9842fd2a5d7d27f1ac851c043a734a494ad75402c5331db3da79ed691/coverage-7.14.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a574912f3bde4b0619f6e97d01aa590b70998859244793769eb3a6df78ee56d3", size = 253976, upload-time = "2026-06-22T23:09:35.351Z" }, { url = "https://files.pythonhosted.org/packages/b7/d1/47eda9fd1eaeea39fa7b5b13a63b2bed92ab901841fb120b3f9f5e1dc30c/coverage-7.16.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fd8ac10cd2458b3c6343aac082fb9bd0e3fa806cb2c4975f2280153474b88412", size = 256783, upload-time = "2026-09-13T19:10:20.778Z" },
{ url = "https://files.pythonhosted.org/packages/fd/30/e1600ddf7e226db5558bb5323d2186fff00f505c4b764643ec89ce5d8175/coverage-7.14.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:e343fb086c9cd780b38622fea7c369acd64c1a0724312149b5d769c387a2b1f5", size = 251942, upload-time = "2026-06-22T23:09:37.313Z" }, { url = "https://files.pythonhosted.org/packages/38/c3/565edf044877cb8cd3373c56885347ffc38f0edfd1f1679a487b208c19a8/coverage-7.16.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9d8c54ec32e5c102b9241f75d88ae26538b53662868ca491736611db448d9c7a", size = 254742, upload-time = "2026-09-13T19:10:22.733Z" },
{ url = "https://files.pythonhosted.org/packages/d9/2c/9159de64f9dd648e324328d588a44cfab1e331eb5259ce1141afe2a92dfb/coverage-7.14.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:3c68df8e61f1e09633fefc7538297145623957a048534368c9d212782aa5e845", size = 256220, upload-time = "2026-06-22T23:09:39.165Z" }, { url = "https://files.pythonhosted.org/packages/fd/88/87d2b2aeaba719192b2089ff1c2cf89a06cf73a6d2e9f1f145626617700c/coverage-7.16.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6dd8dda3402a01a1a8fe8b753a282466f615128574a5590a9108acd07b1f8540", size = 259016, upload-time = "2026-09-13T19:10:24.769Z" },
{ url = "https://files.pythonhosted.org/packages/91/67/b7f536cc2c124f48e91b22fbb741d2261f4e3d310faf6f76007f47566e5d/coverage-7.14.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3e5b550a128419373c2f6cec28a244207013ef15f5cbcff6a5ca09d1dfaaf027", size = 251756, upload-time = "2026-06-22T23:09:41.056Z" }, { url = "https://files.pythonhosted.org/packages/fc/1b/70813185b125768abdcf7899fec4d37edc2e5fc9b60c7045c8f4271ec757/coverage-7.16.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:79afa9726438912e5cddd1fe541815cea9763c92935f594835e4c432565b68a9", size = 254559, upload-time = "2026-09-13T19:10:26.781Z" },
{ url = "https://files.pythonhosted.org/packages/dd/ec/f3718038e2d4860c715a55428377ca7f6c75872caf98cabd982e1d76967d/coverage-7.14.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2bfc4dd0a912329eccc7484a7d0b2a38032b38c40663b1e1ac595f10c457954b", size = 253413, upload-time = "2026-06-22T23:09:43.306Z" }, { url = "https://files.pythonhosted.org/packages/d8/fa/e7aa5af279aafda633a1ede8bfd7d6916b0c8b2082be86759e0b52e73a61/coverage-7.16.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3db3978211c3cead5437a80136ca0556bab8bc7828de15a762884b0598c41361", size = 256215, upload-time = "2026-09-13T19:10:28.714Z" },
{ url = "https://files.pythonhosted.org/packages/b8/a5/91f11efeef89b3cc9b30461128db15b0511ef813ab889a7b7ab636b3a497/coverage-7.14.3-cp314-cp314-win32.whl", hash = "sha256:0423d64c013057a06e70f070f073cec4b0cbc7d2b27f3c7007292f2ff1d52965", size = 222946, upload-time = "2026-06-22T23:09:45.261Z" }, { url = "https://files.pythonhosted.org/packages/38/87/7a894fa4f8c6662d2b6a87a3436950e15b1fa56e01765c9d6634fb2cbeb8/coverage-7.16.1-cp314-cp314-win32.whl", hash = "sha256:49c39c7068a494f8eb427155f5682f44feee43f9b3107fd54b1e52465379c54b", size = 225719, upload-time = "2026-09-13T19:10:30.743Z" },
{ url = "https://files.pythonhosted.org/packages/58/fd/98ac9f524d9ec378de831c034dbdeb544ca7ef7d2d9c9996daf232a037fd/coverage-7.14.3-cp314-cp314-win_amd64.whl", hash = "sha256:92c22e19ce64ca3f2ad751f16f14df1468b4c231bd6af97185063a9c292a0cb3", size = 223436, upload-time = "2026-06-22T23:09:47.177Z" }, { url = "https://files.pythonhosted.org/packages/8b/01/fa7193c8005fb85488f02b0e1cc3c05a233cf2640206dd978af447aeecbf/coverage-7.16.1-cp314-cp314-win_amd64.whl", hash = "sha256:c510dad19552d912058e4c3e3cbec3fb155dbe8d0ce0ceb7e7dbf5c5822bae0b", size = 226208, upload-time = "2026-09-13T19:10:32.698Z" },
{ url = "https://files.pythonhosted.org/packages/b4/a0/7cd612d650a772a0ae80144443406bf61981c896c3d57c9e6e79fb2cdbd1/coverage-7.14.3-cp314-cp314-win_arm64.whl", hash = "sha256:41de778bd41780586e2b04912079c73089ab5d839624e28db3bdb26de638da92", size = 222861, upload-time = "2026-06-22T23:09:49.384Z" }, { url = "https://files.pythonhosted.org/packages/da/5c/a08634c714924c3eaef811bb3576c044128aa5e7dfa86c75e52f0761849e/coverage-7.16.1-cp314-cp314-win_arm64.whl", hash = "sha256:b7d4d7e6dcaf33e85f1919f03346403bdcc27437c420a78835f3805bca0ab71f", size = 225633, upload-time = "2026-09-13T19:10:34.79Z" },
{ url = "https://files.pythonhosted.org/packages/55/57/017353fab573779c0d00448e47d102edd36c792f7b6f233a4d89a7a08384/coverage-7.14.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8427f370ca67db4c975d2a26acfc0e5783ca0b52444dbc50278ace0f35445949", size = 221474, upload-time = "2026-06-22T23:09:51.417Z" }, { url = "https://files.pythonhosted.org/packages/43/df/ddb8a4c664046b1a0ee29c9c2d25b993e5dbc8fbde715df3694a64532781/coverage-7.16.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:3d0a3681c12d3e0bcdea3d9414b04087828d6c1a482802d6f7f42c37ed530152", size = 224281, upload-time = "2026-09-13T19:10:36.853Z" },
{ url = "https://files.pythonhosted.org/packages/69/92/90cf1f1a5c468a9c1b7ba2716e0e205293ad9b02f5f573a6de4318b15ba1/coverage-7.14.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d8e88f335544a47e22ae2e45b344772925ec65166555c958720d5ed971880891", size = 221738, upload-time = "2026-06-22T23:09:53.487Z" }, { url = "https://files.pythonhosted.org/packages/e2/d0/9076e0c762d8afd91182e60a520fa5c92c4a334785eeb9fd6b8ef8fe7e3c/coverage-7.16.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3f3b4469d3da3ecced775d1a8c9c5d9fc80f259e30b7b89f9fed0700d6035ecb", size = 224547, upload-time = "2026-09-13T19:10:39.359Z" },
{ url = "https://files.pythonhosted.org/packages/a4/c0/4df964fa539f8399fd7679c09c472d73744de334686fd3f01e3a2465ce4e/coverage-7.14.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:beaab199b9e5ceaf5a225e16a9d4df136f2a1eae0a5c20de1e277c8a5225f388", size = 263101, upload-time = "2026-06-22T23:09:55.895Z" }, { url = "https://files.pythonhosted.org/packages/03/e5/9c59e64b6161704f35fe91549bb19b2bb355e95caf596c26a2065564807c/coverage-7.16.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c08ae35c1be2fe1ce4b4c628df5c6fc0dc9a87f8e5fe8e20238d249678984741", size = 265906, upload-time = "2026-09-13T19:10:41.434Z" },
{ url = "https://files.pythonhosted.org/packages/06/76/e5d33b2576ae3bf2be2058cd1cae57774b61e400f2c3c58f3783dc2ffb4a/coverage-7.14.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3ff255799f5a1676c71c1c32ec01fd043aa09d57b3d95764b24992757184784", size = 265225, upload-time = "2026-06-22T23:09:57.904Z" }, { url = "https://files.pythonhosted.org/packages/57/5a/13ccaffb77f766101bf6f38be9dba9e468b02cc92da4552a57877dbf1c1f/coverage-7.16.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8ee71a38c54bb2676bbe762b8b0943a79ccb1c2fd6a52054f66e63eda392f8c1", size = 268023, upload-time = "2026-09-13T19:10:43.533Z" },
{ url = "https://files.pythonhosted.org/packages/61/d2/e52419afe391a39ba27fdefaf0737d8e34bf03faef6ab3b3006545bbd0d0/coverage-7.14.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:878832eaac515b62decfa76965aed558775f86bf1fc8cca76993c0c84ae31aed", size = 267643, upload-time = "2026-06-22T23:09:59.938Z" }, { url = "https://files.pythonhosted.org/packages/ad/a1/05cfcf01d3c7c922832698ad46e51d3441d820ce87a943014bb5cf5710dd/coverage-7.16.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76491917771f179f9772efe218c5ccc65950dbdb35f4439298d8a8dfc6ec1f72", size = 270442, upload-time = "2026-09-13T19:10:45.895Z" },
{ url = "https://files.pythonhosted.org/packages/58/7a/f2625d8d5006b6b20fba5afaef00b24a763fe96476ea798a3076cbc1f84e/coverage-7.14.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:611e62cb9386096d81b63e0a05330750268617231e7bd598e1fe77482a2c58a5", size = 268762, upload-time = "2026-06-22T23:10:01.943Z" }, { url = "https://files.pythonhosted.org/packages/72/15/a2f1544b8e3835d7b769f7dabcc9ac0283e0b646ef3344703ff8f18d83e6/coverage-7.16.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4aa0b0a6f81fa3deb211e643f6954e78b4376b62b9c218271236cfa757664e8", size = 271565, upload-time = "2026-09-13T19:10:48.123Z" },
{ url = "https://files.pythonhosted.org/packages/7d/bf/908024006bba57127354d74e938954b9c3cd765cc2e0412dc9c37b415cda/coverage-7.14.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:02c41de2a88011b893050fc9830267d927a50a215f7ad5ec17349db7090ccf26", size = 262208, upload-time = "2026-06-22T23:10:03.954Z" }, { url = "https://files.pythonhosted.org/packages/df/5b/963c2993a82bd313f298d663afe03e164b96ace4d9d4c7561740a559e13d/coverage-7.16.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:756ba2d96d073c5a2a55d67fa22784763710fadbe22c41adde2d9cfa4dd78a8c", size = 264959, upload-time = "2026-09-13T19:10:50.195Z" },
{ url = "https://files.pythonhosted.org/packages/34/a0/d4f9296441b909817442fdb26bd77a698f08272ec683a7394b00eb2e47a0/coverage-7.14.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:526ce9721116af23b1065089f0b75046fe521e7772ab94b641cd66b7a0421889", size = 265096, upload-time = "2026-06-22T23:10:05.936Z" }, { url = "https://files.pythonhosted.org/packages/12/59/5eba06d1943735d7cd61d46d8c8a20ffe8ddd2da06b3c94366078dadeb9b/coverage-7.16.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:99bf9ea435cefcefd220f8687c3ddbbf78dc2de0bd11b57c3ae9fbbdf8d5561a", size = 267897, upload-time = "2026-09-13T19:10:52.252Z" },
{ url = "https://files.pythonhosted.org/packages/e8/da/4ae4f3f4e477b56a4ce1e5c48a35eff38a94b50130ce5bdc897024741cfc/coverage-7.14.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e4ed44705ca4bead6fc977a8b741f2145608289b33c8a9b42a95d0f15aedbf4d", size = 262699, upload-time = "2026-06-22T23:10:07.973Z" }, { url = "https://files.pythonhosted.org/packages/bd/48/af6c30f6ea431bb9b83f9070d268a9cc4fc97490abd32080164177ea999f/coverage-7.16.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:35cbc81f937fc402971df45c897d2df2bfb2014efcd990360032aa0a651635da", size = 265504, upload-time = "2026-09-13T19:10:54.432Z" },
{ url = "https://files.pythonhosted.org/packages/d8/7a/6927148073ff32856d78baa77b4ddc07a9be7e90020f9db0661c4ca523a1/coverage-7.14.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2415902f385a23dcc4ccd26e0ba803249a169af6a930c003a4c715eeb9a5444e", size = 266433, upload-time = "2026-06-22T23:10:10.145Z" }, { url = "https://files.pythonhosted.org/packages/80/f2/6e13852a8656d05fa83284567dd5a5b1e6d89bef79fe3effca2787159eab/coverage-7.16.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:8fae08e85b334ac6ac886002b5041396a31bcf805225bbe19847627203da99e2", size = 269235, upload-time = "2026-09-13T19:10:56.563Z" },
{ url = "https://files.pythonhosted.org/packages/f7/a7/774f658dbe9c4c3f5daa86a87e0459ac3832e4e3cc67affe078547f727b9/coverage-7.14.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:b75ee850fc2d7c831e883220c445b035f2224de2ba6103f1e56dbd237ab913f7", size = 261547, upload-time = "2026-06-22T23:10:12.191Z" }, { url = "https://files.pythonhosted.org/packages/c2/32/b4fe465daa64ece674f83a750dfa4ba0fa3c5c74d6ef5dbb8dfce892cf0d/coverage-7.16.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:83362b64e215ef00b0ba33fcf13655ace6c9fdd144d5ad2ab59ac86c2daf166e", size = 264347, upload-time = "2026-09-13T19:10:58.634Z" },
{ url = "https://files.pythonhosted.org/packages/3d/14/a0c18c0376c43cbf973f43ef6ca20019c950597180e6396232f7b6a27102/coverage-7.14.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dc9b4e35e7c3920e925ba7f14886fd5fbe481232754624e832ddba66c7535635", size = 263859, upload-time = "2026-06-22T23:10:14.492Z" }, { url = "https://files.pythonhosted.org/packages/54/f3/88b5c0e4ca3994c6d5feb7b1bf4c9a62cee205553159184968426930a7b1/coverage-7.16.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:33300f2e140ccf26af3d8152e62bff71993f9310cfc63ba7a20940b0d246a0ae", size = 266660, upload-time = "2026-09-13T19:11:00.746Z" },
{ url = "https://files.pythonhosted.org/packages/10/ac/43a3d0f460af524b131a6191805bc5d18b806ab4e828fbf82e8c8c3af446/coverage-7.14.3-cp314-cp314t-win32.whl", hash = "sha256:7b27c822a8161afbe48e99f1adfb098d270ae7e0f7d7b0555ce110529bdb69cc", size = 223250, upload-time = "2026-06-22T23:10:16.758Z" }, { url = "https://files.pythonhosted.org/packages/97/72/6eff5456d7ba7f1c4678af531c33f9d957cae3201bd229b056fd13a204a3/coverage-7.16.1-cp314-cp314t-win32.whl", hash = "sha256:5539304fdbb2cc144df684d35a33b81145334d23e1c2367b5a923d25107f70b2", size = 226026, upload-time = "2026-09-13T19:11:02.846Z" },
{ url = "https://files.pythonhosted.org/packages/3f/5f/d5e5c56b0712e96ce8f69fe7dbf229ff938b437bc50862743c8a0d2cea84/coverage-7.14.3-cp314-cp314t-win_amd64.whl", hash = "sha256:39e1dbbb6ff2c338e0196a482558a792a1de3aa64261196f5cdb3da016ad9cda", size = 224082, upload-time = "2026-06-22T23:10:19.23Z" }, { url = "https://files.pythonhosted.org/packages/8e/c8/6e5ae3d8d4d0f2c0078985bf4db55fafd90e8107b1bf91ee3547a13f5694/coverage-7.16.1-cp314-cp314t-win_amd64.whl", hash = "sha256:715dcb72c3280c428c3a20134b87e42c29acec9669136e899ab2de69ca86218d", size = 226862, upload-time = "2026-09-13T19:11:04.921Z" },
{ url = "https://files.pythonhosted.org/packages/62/35/947cbd5be1d3bcbbdc43d6791de8a56c6501903311d42915ae06a82815f0/coverage-7.14.3-cp314-cp314t-win_arm64.whl", hash = "sha256:68520c90babfa2d560eca6d497921ed3a4f469623bd709733124491b2aa8ef3f", size = 223400, upload-time = "2026-06-22T23:10:21.24Z" }, { url = "https://files.pythonhosted.org/packages/be/c7/68f9f0734afc904a92b974b489545b6a15700f3b1c4bd36eae764561e661/coverage-7.16.1-cp314-cp314t-win_arm64.whl", hash = "sha256:dac8b84c03e6029d272b8249c77018db83de59ca009a9adef7c144b4a62ee5e6", size = 226171, upload-time = "2026-09-13T19:11:06.969Z" },
{ url = "https://files.pythonhosted.org/packages/eb/e3/a0aa32bfa3a081951f60a23bc0e7b512891ef0eecda1153cf1d8ba36c6b1/coverage-7.14.3-py3-none-any.whl", hash = "sha256:fb7e18afb6e903c1a92401a2f0501ac277dca527bb9ca6fe1f691a8a0026a0e8", size = 212469, upload-time = "2026-06-22T23:10:23.405Z" }, { url = "https://files.pythonhosted.org/packages/ae/16/e11addf5322d98e86307da9e00c94644b97cb72c534c74cda89705bebeef/coverage-7.16.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:a337dc2d54c74430cd2febb8ee04f7c508ba8b3b412bf0463f077a66cfc73743", size = 223544, upload-time = "2026-09-13T19:11:09.108Z" },
{ url = "https://files.pythonhosted.org/packages/92/d9/7ccd6484f615961d94b09c75904f87d86375109596069ae3a495a205dbbe/coverage-7.16.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:3acd1d78397dead78dd1b011b5fc19cc823c190349acd549e63856dff649c80e", size = 223882, upload-time = "2026-09-13T19:11:11.132Z" },
{ url = "https://files.pythonhosted.org/packages/59/37/2fd78152a557df4a7e4ad78d6851ede90c211838d526e4916c6016f45144/coverage-7.16.1-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:73a32694603a34ad01d7e51a481a4023410d8099e1d0757e067945695c10f0ae", size = 254987, upload-time = "2026-09-13T19:11:13.735Z" },
{ url = "https://files.pythonhosted.org/packages/7a/4a/58e2f9b8b13cc422aa7f474a498945ec7f446ef473b5b6bae18d2981f2a9/coverage-7.16.1-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fbbe8265736659a6be2e6042b6a35be13545d14b243cc1d7ecf65f90d788a370", size = 257902, upload-time = "2026-09-13T19:11:16.025Z" },
{ url = "https://files.pythonhosted.org/packages/3c/74/63f8f6a67c03b86455c726be273b2aa5b57fd1122f22350d141bbb142e91/coverage-7.16.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b0359eb4c62f9993e176bc8f50450fc736a6b90dbcc05bb8584e948812699ae", size = 259522, upload-time = "2026-09-13T19:11:18.206Z" },
{ url = "https://files.pythonhosted.org/packages/e2/3b/1f2261da7483e345fe55de11e41ffc16a543511e37c0912134c0dccc4f6c/coverage-7.16.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:da506e669a8a851b59e122b4b219ea70996a6296f44f3a9348a852526ff961de", size = 261723, upload-time = "2026-09-13T19:11:20.628Z" },
{ url = "https://files.pythonhosted.org/packages/a7/8a/7eb1a361e044b7293f49c921a974b532627ed37f3bef5e7e9f58bcd0d5a2/coverage-7.16.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:64a2a5985d81810ed605ff0dc4ccd6555efcb5700353825532a9a0aea65826e1", size = 255462, upload-time = "2026-09-13T19:11:22.733Z" },
{ url = "https://files.pythonhosted.org/packages/67/66/6d94f7ea87e99c519def5cfa5d4a1ab20247d6b403eb6e6b5b63d6aa5985/coverage-7.16.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:66d70132b69b861805dc1ca46cdd733e54c416890e8f1371d2fd103f70b59c9c", size = 257617, upload-time = "2026-09-13T19:11:24.933Z" },
{ url = "https://files.pythonhosted.org/packages/3d/52/c3de0a3868589a1463a4f1cb0222eca0e5fa9f91da865ccf20d2e19ec19c/coverage-7.16.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:db651a9cf325a542bc2b7b8cc8f1b2bdc6492739bae3103731b2f1c85b96cff6", size = 255495, upload-time = "2026-09-13T19:11:27.158Z" },
{ url = "https://files.pythonhosted.org/packages/9e/a3/ea10e75f20fc1e7be10b166a494826a965d19b1dc9c8a81c511a01153311/coverage-7.16.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:4184e78a4465dcda359fb403172b8951dd220929cb0984c02fabca1742fff06f", size = 259728, upload-time = "2026-09-13T19:11:29.33Z" },
{ url = "https://files.pythonhosted.org/packages/0e/96/9d8020d97de46f0390739beda196322586cdd87e1fc357223e6a28bc4135/coverage-7.16.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:bc53c3f3adaa939b7a063533ffe0ae1259e7073393c618043a99a6970a87e3df", size = 254904, upload-time = "2026-09-13T19:11:31.86Z" },
{ url = "https://files.pythonhosted.org/packages/5b/21/2ff8867d4560f4f639a82d8fcf13c27eea88a96193daba096027c48bb929/coverage-7.16.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:b44308854ef210b9b78df9cdfd4e159513382a859f5ef8464306d14a54c2a040", size = 256828, upload-time = "2026-09-13T19:11:34.078Z" },
{ url = "https://files.pythonhosted.org/packages/6a/4b/feacad51cb5163e3baa13281f8a3098f15d7934cc1f28cfe656557eb4e46/coverage-7.16.1-cp315-cp315-win32.whl", hash = "sha256:dccc142614d3419ed71857deb43f1d757829a4c7fce9994464b71e7e38309827", size = 225722, upload-time = "2026-09-13T19:11:36.314Z" },
{ url = "https://files.pythonhosted.org/packages/39/34/66bbc6ffd3c51fa67604c566920e772c5e3baa57f88dcf7b109c7f18b2cb/coverage-7.16.1-cp315-cp315-win_amd64.whl", hash = "sha256:961fc424e9d5229a99f8f1189942d8e7f4e1519147c3af64842f944aca03914d", size = 226195, upload-time = "2026-09-13T19:11:38.493Z" },
{ url = "https://files.pythonhosted.org/packages/f7/96/4bad920d4caed127c37c136a64b0730fa495517d2eb58809e0d81e6e7dff/coverage-7.16.1-cp315-cp315-win_arm64.whl", hash = "sha256:681a9488c5a234397c4f013da065aa9e53eb7af4c78f1f80c6f15e7208acb855", size = 225625, upload-time = "2026-09-13T19:11:40.664Z" },
{ url = "https://files.pythonhosted.org/packages/f5/2b/a88c7906eeb7da4394e932060d7008e153b3a2cd8d11782d82faa03bb7a3/coverage-7.16.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:8bb09a2d19b04db1fa0e087a7ca4f12458f7e0e7364cfcd838441d86fb1c61f6", size = 224271, upload-time = "2026-09-13T19:11:42.876Z" },
{ url = "https://files.pythonhosted.org/packages/1f/1f/98db59c595680d16f553890deac6d72610e04f192c1964ec9b69cbe790ab/coverage-7.16.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:2270a794600b635ca9452ce4c32e2fe81a35f9caa17ffac0eba99f14f275bd4d", size = 224564, upload-time = "2026-09-13T19:11:45.071Z" },
{ url = "https://files.pythonhosted.org/packages/b5/e8/699e236d4cb97ec282b0655afc85acf12349e71a02fff0f1a7d0fb643079/coverage-7.16.1-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a4eff405b545dfcf79cf0d9d3ff750e5c5a887aa066114175193a81d249c5ee6", size = 265423, upload-time = "2026-09-13T19:11:47.666Z" },
{ url = "https://files.pythonhosted.org/packages/96/1b/6eaa21912863b3e3bf23cfe605fb62929bb1a41dc33d2bfa8e92000232bd/coverage-7.16.1-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ee1d5fc9e3bd6a217906929cc97880239a91d20dae7746f538eb0eefee705ab1", size = 268503, upload-time = "2026-09-13T19:11:50.12Z" },
{ url = "https://files.pythonhosted.org/packages/9f/ac/4eb46bffa98c28a80559a58c12f17e19308d52b31174af1bd49decf76606/coverage-7.16.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4ec944947de098ad5a1738413f9364689a57067ecbc328e9de37218aa1e5cc1", size = 271059, upload-time = "2026-09-13T19:11:52.363Z" },
{ url = "https://files.pythonhosted.org/packages/ed/d4/17a51ef1f6a084c9abbead522cfbee0fe9d29a593128781e724b7a0795b3/coverage-7.16.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3f73ee3956fde2d461c9e2955dd48166e4821fc8587d135e8780fb84da2a098b", size = 272039, upload-time = "2026-09-13T19:11:54.935Z" },
{ url = "https://files.pythonhosted.org/packages/4a/ff/9fdeba8b8aa0848030f45f869ddedc306795a8c6e53e901b8553afbf5415/coverage-7.16.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1ec9a4ee989c0d06ad95add0dbfdbb72b00ef53f43431ca0b612384e7878e5de", size = 265869, upload-time = "2026-09-13T19:11:57.423Z" },
{ url = "https://files.pythonhosted.org/packages/95/eb/ab3eb506b2e4dd278440fbbbc7ed18424a86d8a981db0b90fdba66f5d34d/coverage-7.16.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:7e5727b2508f817f3126d6c33327dda32fe69d15514badfcd61db8bc4209ecef", size = 268883, upload-time = "2026-09-13T19:11:59.71Z" },
{ url = "https://files.pythonhosted.org/packages/d5/0c/f4edefdce33f01954a74237df5ee83ccef5dcd1165e04abf0adf47543fca/coverage-7.16.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:19a3ea2f364012ef06678118fffdc92442a16bef4a5c8ad4f4019dd8f9ac8876", size = 265359, upload-time = "2026-09-13T19:12:01.953Z" },
{ url = "https://files.pythonhosted.org/packages/df/86/ce5ed885fc7ce2adc9a5971134ace257d0f84f19c3d117da89c179b9662e/coverage-7.16.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:996c2b891b441ec2b39725ee3e8386e2f11b4894b92be225fdfd54a3eeada2c8", size = 270056, upload-time = "2026-09-13T19:12:04.46Z" },
{ url = "https://files.pythonhosted.org/packages/48/a1/5dbd5f95070cece25c6df9e6ea4ded2a87dc695ce82a49bcb2116452b342/coverage-7.16.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:a125fac1f6b1e88488d208a86b578e1790e3c4937f2e1568d23356141d236220", size = 265498, upload-time = "2026-09-13T19:12:06.811Z" },
{ url = "https://files.pythonhosted.org/packages/8e/ff/254b26f3300f87c7c53ac97da41ae75f0e2ac066aa964bc073c0f3750b38/coverage-7.16.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:b10095528b866d322d33d6bf1709b7f8cbf959f12e8cb2ba22fc59c8717866b0", size = 267459, upload-time = "2026-09-13T19:12:09.479Z" },
{ url = "https://files.pythonhosted.org/packages/61/20/bf9958f8ddbbf6a2d39da3d4009f33e0fcf19c2d2fd99715c6494cdc8e8c/coverage-7.16.1-cp315-cp315t-win32.whl", hash = "sha256:531d9be377fdcc05593b974656872eb82e808ebeb42a72515e3aaeb8bb7166f5", size = 226021, upload-time = "2026-09-13T19:12:11.933Z" },
{ url = "https://files.pythonhosted.org/packages/35/8f/12d46948704d9e437c8dca2718e5d54ecc2f85396b64b500c29d8dbe10db/coverage-7.16.1-cp315-cp315t-win_amd64.whl", hash = "sha256:46a88f51770df7c9bc376bd57d3f86cdc7624b8e16ac4b585a655c22b7a1b4db", size = 226853, upload-time = "2026-09-13T19:12:14.254Z" },
{ url = "https://files.pythonhosted.org/packages/a5/7f/1ca5fd0601054fce5a3539375b997e67683646e2bf62f60c41e70b529fcc/coverage-7.16.1-cp315-cp315t-win_arm64.whl", hash = "sha256:7580432cbe1e8b762660ae5806f04f869e1c02e519836a43f8094437e561e9f0", size = 226163, upload-time = "2026-09-13T19:12:16.589Z" },
{ url = "https://files.pythonhosted.org/packages/96/1a/d6d16babd0a5fe4c3fae40702158c570351694e74516d8d81b86c5637448/coverage-7.16.1-py3-none-any.whl", hash = "sha256:3d8bd4e58b6a5c2018d808f297905393c6c61da466a48c3f0596a76a4900ebe4", size = 215264, upload-time = "2026-09-13T19:12:18.895Z" },
] ]
[[package]] [[package]]
@@ -377,11 +430,11 @@ sdist = { url = "https://files.pythonhosted.org/packages/f0/6f/84e91828186bbfced
[[package]] [[package]]
name = "dnspython" name = "dnspython"
version = "2.8.0" version = "2.9.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } sdist = { url = "https://files.pythonhosted.org/packages/ef/4a/50822184bd67cc6493f0fb6a880749158fcd31ab3fa07409acfd91f9fc85/dnspython-2.9.0.tar.gz", hash = "sha256:b44dc6b18f07a8b1c56676a19fbfdb5209415b046a9cece286baafa87ff3f7f1", size = 423560, upload-time = "2026-10-09T00:07:24.352Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, { url = "https://files.pythonhosted.org/packages/10/02/cdcc9b7c051786a103c3b09e1003a82fa0c66bcb91ffbdabcfbf7b4163b9/dnspython-2.9.0-py3-none-any.whl", hash = "sha256:9a4aedb833c3c1b49214d04d44d3032ab7a9135f7c1d29a549b4ff78fd82fda9", size = 354822, upload-time = "2026-10-09T00:07:22.622Z" },
] ]
[[package]] [[package]]
@@ -650,48 +703,84 @@ wheels = [
[[package]] [[package]]
name = "librt" name = "librt"
version = "0.11.0" version = "0.15.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/40/08/9e7f6b5d2b5bed6ad055cdd5925f192bb403a51280f86b56554d9d0699a2/librt-0.11.0.tar.gz", hash = "sha256:075dc3ef4458a278e0195cbf6ac9d38808d9b906c5a6c7f7f79c3888276a3fb1", size = 200139, upload-time = "2026-05-10T18:17:25.138Z" } sdist = { url = "https://files.pythonhosted.org/packages/36/9b/356320fbae2ac8467e21c5e73e1389c80468e4998c62cc7d3536cc51b614/librt-0.15.0.tar.gz", hash = "sha256:4e66cbe84437497d951b799d3e1551291b6fb3d643820a7014b3655d57a59162", size = 214338, upload-time = "2026-08-07T10:49:42.663Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/29/eb/dbce197da4e227779e56b5735f2decc3eb36e55a1cdbf1bd65d6639d76c1/librt-0.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4a017a95e5837dc15a8c5661d60e05daa96b90908b1aa6b7acdf443cd25c8ebd", size = 143345, upload-time = "2026-05-10T18:16:30.674Z" }, { url = "https://files.pythonhosted.org/packages/a8/93/949053fb462eecc4a9a5ee770a81f4b40be7b79538b245545d4aebc6b58b/librt-0.15.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f5de7feedc56337a088eb15cd9fafa9938367362221d8cc62c642b7f94821993", size = 149833, upload-time = "2026-08-07T10:47:39.86Z" },
{ url = "https://files.pythonhosted.org/packages/76/a3/254bebd0c11c8ba684018efb8006ff22e466abce445215cca6c778e7d9de/librt-0.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b1ecbd9819deccc39b7542bf4d2a740d8a620694d39989e58661d3763458f8d4", size = 143131, upload-time = "2026-05-10T18:16:32.037Z" }, { url = "https://files.pythonhosted.org/packages/61/ca/8281aa6cd560a3420e4497729f6b704b53be3eeaaef82d5aeadddaf7441f/librt-0.15.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c0eb900c0e91f4aebe680845242e614f1864edfd44106380d0752ac29522bf8", size = 154088, upload-time = "2026-08-07T10:47:41.065Z" },
{ url = "https://files.pythonhosted.org/packages/f1/3f/f77d6122d21ac7bf6ae8a7dfced1bd2a7ac545d3273ebdcaf8042f6d619f/librt-0.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7da327dacd7be8f8ec36547373550744a3cc0e536d54665cd83f8bcd961200e8", size = 477024, upload-time = "2026-05-10T18:16:33.493Z" }, { url = "https://files.pythonhosted.org/packages/dd/02/1a1662dceaba6a086360891448d5ce9a7d3555976cae59a31a39d744b9c7/librt-0.15.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e8c9a650a188e38bac005048cbe6342e81407782944d01934540ab75e417df21", size = 494215, upload-time = "2026-08-07T10:47:42.388Z" },
{ url = "https://files.pythonhosted.org/packages/ac/0a/2c996dadebaa7d9bbbd43ef2d4f3e66b6da545f838a41694ef6172cebec8/librt-0.11.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0dc56b1f8d06e60db362cc3fdae206681817f86ce4725d34511473487f12a34b", size = 474221, upload-time = "2026-05-10T18:16:34.864Z" }, { url = "https://files.pythonhosted.org/packages/69/84/99211619dc656370a3740c33d2b0b6d5a3fb1e73689314f6ed477a397dc4/librt-0.15.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:92bfed8deec93df30286b9fe9e3b1dd17329cc076a192b4ee5ec223841d54953", size = 491173, upload-time = "2026-08-07T10:47:43.683Z" },
{ url = "https://files.pythonhosted.org/packages/0a/7e/f5d92af8486b8272c23b3e686b46ff72d89c8169585eb61eef01a2ac7147/librt-0.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05fb8fb2ab90e21c8d12ea240d744ad514da9baf381ebfa70d91d20d21713175", size = 505174, upload-time = "2026-05-10T18:16:36.705Z" }, { url = "https://files.pythonhosted.org/packages/d4/aa/5448d0b05f4579b635d3899176817ebf561af0e57bacd425b5b1887264c1/librt-0.15.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ec4b19788f835711a2072f9dbe6b03b3bf32ed1f0fb30cf399bdd59d9f0c33fa", size = 505512, upload-time = "2026-08-07T10:47:45.314Z" },
{ url = "https://files.pythonhosted.org/packages/af/1a/cb0734fe86398eb33193ab753b7326255c74cac5eb09e76b9b16536e7adb/librt-0.11.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cae74872be221df4374d10fec61f93ed1513b9546ea84f2c0bf73ab3e9bd0b03", size = 497216, upload-time = "2026-05-10T18:16:38.418Z" }, { url = "https://files.pythonhosted.org/packages/95/82/01940e40b83c43a546c4a3c896cf34ca272a9690899d55914e4827b3dcce/librt-0.15.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4c7bacb70930f3d0a56f4ecf1be474a1f0d941b01dd73b756f3c256d42cb879", size = 523073, upload-time = "2026-08-07T10:47:46.66Z" },
{ url = "https://files.pythonhosted.org/packages/18/06/094820f91558b66e29943c0ec41c9914f460f48dd51fc503c3101e10842d/librt-0.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:32bcc918c0148eb7e3d57385125bac7e5f9e4359d05f07448b09f6f778c2f31c", size = 513921, upload-time = "2026-05-10T18:16:39.848Z" }, { url = "https://files.pythonhosted.org/packages/88/fa/759c0030f3ee371439eb26de34fc745807caf0abb878af7af4b8b7c3dd3d/librt-0.15.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3e79f05e4a08b4d880342673312bbc895b56df7765605796f15902eb5367d3ae", size = 515080, upload-time = "2026-08-07T10:47:48.319Z" },
{ url = "https://files.pythonhosted.org/packages/0b/c2/00de9018871a282f530cacb457d5ec0428f6ac7e6fedde9aff7468d9fb04/librt-0.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f9743fc99135d5f78d2454435615f6dec0473ca507c26ce9d92b10b562a280d3", size = 520850, upload-time = "2026-05-10T18:16:41.471Z" }, { url = "https://files.pythonhosted.org/packages/0b/27/894e072228fcb159703c655da69f8cd10dbed489c36e3df7dd032a2483be/librt-0.15.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a417149c0cba4d50b61e992e5a15e69eaf96746609b461cc4ed168aeef6b79dd", size = 534164, upload-time = "2026-08-07T10:47:49.875Z" },
{ url = "https://files.pythonhosted.org/packages/51/9d/64631832348fd1834fb3a61b996434edddaaf25a31d03b0a76273159d2cf/librt-0.11.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5ba067f4aadae8fda802d91d2124c90c42195ff32d9161d3549e6d05cfe26f96", size = 504237, upload-time = "2026-05-10T18:16:43.15Z" }, { url = "https://files.pythonhosted.org/packages/98/a3/0078e91c1f36f8815db17827de15650b9a3fe56c55fbf998c854b34e40d3/librt-0.15.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:da7a94d6a3411f579d72aa3e3bc5fbca7ed4549f3dbd7e5de3aa567333374285", size = 540616, upload-time = "2026-08-07T10:47:51.408Z" },
{ url = "https://files.pythonhosted.org/packages/a5/ec/ae5525eb16edc827a044e7bb8777a455ff95d4bca9379e7e6bddd7383647/librt-0.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:de3bf945454d032f9e390b85c4072e0a0570bf825421c8be0e71209fa65e1abe", size = 546261, upload-time = "2026-05-10T18:16:44.408Z" }, { url = "https://files.pythonhosted.org/packages/86/33/81a29b796dd52a45e9ef7974c7732926e8f10f15b8d2be505665979f896d/librt-0.15.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:856f743ae607f2c1380eccb566c0038a9fb3eabf0fc2be2704d76d9f73557239", size = 545890, upload-time = "2026-08-07T10:47:52.818Z" },
{ url = "https://files.pythonhosted.org/packages/5a/09/adce371f27ca039411da9659f7430fcc2ba6cd0c7b3e4467a0f091be7fa9/librt-0.11.0-cp314-cp314-win32.whl", hash = "sha256:d2277a05f6dcb9fd13db9566aac4fabd68c3ea1ea46ee5567d4eef8efa495a2f", size = 96965, upload-time = "2026-05-10T18:16:46.039Z" }, { url = "https://files.pythonhosted.org/packages/05/82/8be1baa1350e5d30cfd70ae79d0a6f4dc5862ef47f7bb2808aabc9bb86e5/librt-0.15.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:779a6e7c894737e5983e7790a9c78c4000c30e23c9aada08081bdbea53b0fa60", size = 523287, upload-time = "2026-08-07T10:47:54.165Z" },
{ url = "https://files.pythonhosted.org/packages/d6/ee/8ac720d98548f173c7ce2e632a7ca94673f74cacd5c8162a84af5b35958a/librt-0.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:ab73e8db5e3f564d812c1f5c3a175930a5f9bc96ccb5e3b22a34d7858b401cf7", size = 115151, upload-time = "2026-05-10T18:16:47.133Z" }, { url = "https://files.pythonhosted.org/packages/c6/4f/d1be6a01a35c20ef734e0e44113f87d4af756a9354a89dcfbe3b4f8af5e1/librt-0.15.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:96bb17dbe8bab3c0954fbebfc69ed395599de75b6bbc35e3270a878e15d4dd65", size = 565868, upload-time = "2026-08-07T10:47:55.566Z" },
{ url = "https://files.pythonhosted.org/packages/94/20/c900cf14efeb09b6bef2b2dff20779f73464b97fd58d1c6bccc379588ae3/librt-0.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:aea3caa317752e3a466fa8af45d91ee0ea8c7fdd96e42b0a8dd9b76a7931eba1", size = 98850, upload-time = "2026-05-10T18:16:48.597Z" }, { url = "https://files.pythonhosted.org/packages/67/88/649cfa33f5825927b160610f670bdab012a64d627eddb94fa795ea4292fd/librt-0.15.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:7220697efaa6e5348fc3d18ee7f8563d4bfecd9872b37ffb915bfc1d08840622", size = 81619, upload-time = "2026-08-07T10:47:56.886Z" },
{ url = "https://files.pythonhosted.org/packages/0c/71/944bfe4b64e12abffcd3c15e1cce07f72f3d55655083786285f4dedeb532/librt-0.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d1b36540d7aaf9b9101b3a6f376c8d8e9f7a9aec93ed05918f2c69d493ffef72", size = 151138, upload-time = "2026-05-10T18:16:49.839Z" }, { url = "https://files.pythonhosted.org/packages/22/31/8e88a8d5e48fc8d1a817787fb6811dfff6499acd6c8683dd83934aa6ede0/librt-0.15.0-cp314-cp314-win32.whl", hash = "sha256:f54598964d357b1c5ab77cf5d92f21e598fe0e23cdbe9618480807f81b4eba15", size = 100138, upload-time = "2026-08-07T10:47:58.093Z" },
{ url = "https://files.pythonhosted.org/packages/b6/10/99e64a5c86989357fda078c8143c533389585f6473b7439172dd8f3b3b2d/librt-0.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:efbb343ab2ce3540f4ecbe6315d677ed70f37cd9a72b1e58066c918ca83acbaa", size = 151976, upload-time = "2026-05-10T18:16:51.062Z" }, { url = "https://files.pythonhosted.org/packages/80/92/20fd6c4b6a1b1a564b076d55cd3d427d8428217d7638dc25a654cc4791d4/librt-0.15.0-cp314-cp314-win_amd64.whl", hash = "sha256:3ff5893a2c23d886aa9ce786de5ac6ddc74aeeaf90743682b74d920e117d2e28", size = 121258, upload-time = "2026-08-07T10:47:59.564Z" },
{ url = "https://files.pythonhosted.org/packages/21/31/5072ad880946d83e5ea4147d6d018c78eefce85b77819b19bdd0ee229435/librt-0.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0dd688aab3f7914d3e6e5e3554978e0383312fb8e771d84be008a35b9ee548", size = 557927, upload-time = "2026-05-10T18:16:52.632Z" }, { url = "https://files.pythonhosted.org/packages/fc/28/6af430b44d9ebb897b865a3c363b6dcace51357be2347cc0f8f869656a86/librt-0.15.0-cp314-cp314-win_arm64.whl", hash = "sha256:3722a099730704c9a3d70c879fc0f51daec25fe5f1555672d97bc595abeafb95", size = 106467, upload-time = "2026-08-07T10:48:01.097Z" },
{ url = "https://files.pythonhosted.org/packages/5e/8d/70b5fb7cfbab60edbe7381614ab985da58e144fbf465c86d44c95f43cdca/librt-0.11.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f5fb36b8c6c63fdcbb1d526d94c0d1331610d43f4118cc1beb4efef4f3faacb2", size = 539698, upload-time = "2026-05-10T18:16:53.934Z" }, { url = "https://files.pythonhosted.org/packages/7e/aa/b42bb798942ced219f6d63b27e07f91237887a8d0bd0921666db79a13790/librt-0.15.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:38c0c7d4b6fc06c3324b3f9162c8391bfc4fd9dde53afe1033ce7edb48d5a714", size = 159523, upload-time = "2026-08-07T10:48:02.442Z" },
{ url = "https://files.pythonhosted.org/packages/fa/a3/ba3495a0b3edbd24a4cae0d1d3c64f39a9fc45d06e812101289b50c1a619/librt-0.11.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4a9a237d13addb93715b6fee74023d5ee3469b53fce527626c0e088aa585805f", size = 577162, upload-time = "2026-05-10T18:16:55.589Z" }, { url = "https://files.pythonhosted.org/packages/75/03/1b53cd4ef904e73b1d828a5f90143bf94a2967d7cfff0b9ccf93e12aa9b4/librt-0.15.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8b2fdd7ead3c995c37940a790690660d0ca006c302db26cc51933f6766866fc3", size = 161638, upload-time = "2026-08-07T10:48:03.725Z" },
{ url = "https://files.pythonhosted.org/packages/f7/db/36e25fb81f99937ff1b96612a1dc9fd66f039cb9cc3aee12c01fac31aab9/librt-0.11.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5ddd17bd87b2c56ddd60e546a7984a2e64c4e8eab92fb4cf3830a48ad5469d51", size = 566494, upload-time = "2026-05-10T18:16:56.975Z" }, { url = "https://files.pythonhosted.org/packages/ac/c4/9f9c9fba097d49e9e694c2b4dc331df31884645ecbc58a93b4b5fc69d2c5/librt-0.15.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2fde98cf1fc4bac144ce23c2c4c017b924ba714509ea9334977b0b27050c837d", size = 701795, upload-time = "2026-08-07T10:48:05.135Z" },
{ url = "https://files.pythonhosted.org/packages/33/0d/3f622b47f0b013eeb9cf4cc07ae9bfe378d832a4eec998b2b209fe84244d/librt-0.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd43992b4473d42f12ff9e68326079f0696d9d4e6000e8f39a0238d482ba6ee2", size = 596858, upload-time = "2026-05-10T18:16:58.374Z" }, { url = "https://files.pythonhosted.org/packages/4c/05/0966840bda0380c8ae167b9043c6230202941cc90ea29c48e096964c765e/librt-0.15.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:e3b461183c5fa7681b48560f91515f53a953122fb30c71e07abc67d7ddf58c38", size = 682147, upload-time = "2026-08-07T10:48:06.555Z" },
{ url = "https://files.pythonhosted.org/packages/a9/02/71b90bc93039c46a2000651f6ad60122b114c8f54c4ad306e0e96f5b75ad/librt-0.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:f8e3e8056dd674e279741485e2e512d6e9a751c7455809d0114e6ebf8d781085", size = 590318, upload-time = "2026-05-10T18:16:59.676Z" }, { url = "https://files.pythonhosted.org/packages/18/af/1c47ca573c30ea47d195aec26133af522fea1104afaace028d7b32247ea8/librt-0.15.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4bbcc257e3babea20a91715c361b24554ec4e8f51aa578568afc230799fe1a19", size = 696397, upload-time = "2026-08-07T10:48:08.03Z" },
{ url = "https://files.pythonhosted.org/packages/04/04/418cb3f75621e2b761fb1ab0f017f4d70a1a72a6e7c74ee4f7e8d198c2f3/librt-0.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c1f708d8ae9c56cf38a903c44297243d2ec83fd82b396b977e0144a3e76217e3", size = 575115, upload-time = "2026-05-10T18:17:01.007Z" }, { url = "https://files.pythonhosted.org/packages/2e/0f/1aed6223d4f9f9d1171a8596ff100ea4c3f7699fea7a4ba657c3e60daa6c/librt-0.15.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b845b8d48088fad0cadc84be4b8fda63203be7e9237b71015b3925443c1f35ab", size = 722542, upload-time = "2026-08-07T10:48:09.569Z" },
{ url = "https://files.pythonhosted.org/packages/cc/2c/5a2183ac58dd911f26b5d7e7d7d8f1d87fcecdddd99d6c12169a258ff62c/librt-0.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0add982e0e7b9fc14cf4b33789d5f13f66581889b88c2f58099f6ce8f92617bd", size = 617918, upload-time = "2026-05-10T18:17:02.682Z" }, { url = "https://files.pythonhosted.org/packages/c6/22/9e3a929aea456c97d69e6ef3884efea56d4807f97399471cc946baebd8af/librt-0.15.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b30e600e8f337b9bd7f39b86d9fdfedc73cc46e3d0f745931a23a234220bb7e2", size = 729709, upload-time = "2026-08-07T10:48:11.129Z" },
{ url = "https://files.pythonhosted.org/packages/15/1f/dc6771a52592a4451be6effa200cbfc9cec61e4393d3033d81a9d307961d/librt-0.11.0-cp314-cp314t-win32.whl", hash = "sha256:2b481d846ac894c4e8403c5fd0e87c5d11d6499e404b474602508a224ff531c8", size = 103562, upload-time = "2026-05-10T18:17:03.99Z" }, { url = "https://files.pythonhosted.org/packages/e9/1b/c327ef6018e3a9ca0b8e7c5eddeeb331ba8f9b76c24e126d37d0f6d62faf/librt-0.15.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:64b0c8c35aa4c4ed79896359f3e0b285cbe4e610042106500da4811c322cc108", size = 752891, upload-time = "2026-08-07T10:48:12.558Z" },
{ url = "https://files.pythonhosted.org/packages/62/4a/7d1415567027286a75ba1093ec4aca11f073e0f559c530cf3e0a757ad55c/librt-0.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:28edb433edde181112a908c78907af28f964eabc15f4dd16c9d66c834302677c", size = 124327, upload-time = "2026-05-10T18:17:05.465Z" }, { url = "https://files.pythonhosted.org/packages/d7/d1/d5f1ea02c56930087009e39db9b70660a663e76c730b27b925d786718457/librt-0.15.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0da0d94cb802f32a0524653e7201f2cef72d5f700a5407678f5290483d4fcd08", size = 745301, upload-time = "2026-08-07T10:48:14.55Z" },
{ url = "https://files.pythonhosted.org/packages/ce/62/b40b382fa0c66fee1478073eb8db352a4a6beda4a1adccf1df911d8c289c/librt-0.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dee008f20b542e3cd162ba338a7f9ec0f6d23d395f66fe8aeeec3c9d067ea253", size = 102572, upload-time = "2026-05-10T18:17:06.809Z" }, { url = "https://files.pythonhosted.org/packages/d9/3c/5f7c585d15ebb2250c73e7c0ee4e9e47be72c65d520c07ddbcdc62037674/librt-0.15.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:4a6369168d371207339b1e50d4532b06a7121586141f82599505a3f315751d47", size = 747921, upload-time = "2026-08-07T10:48:16.453Z" },
{ url = "https://files.pythonhosted.org/packages/7f/52/1443a446486eba966bcbca1696b472e4f210320ec42f490a47f48fbf0fdc/librt-0.15.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c434e072557ade9cbc642d052c89d031efe47d5c9614523619d0d74a02378e81", size = 727561, upload-time = "2026-08-07T10:48:18.089Z" },
{ url = "https://files.pythonhosted.org/packages/79/91/2270a9380f11725cf83ce1925a5e32dd1dde2be9bba597f25c10a38644e7/librt-0.15.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7eec6a42018bc1d45763b1c162d3d2bf7c3b9a1b0ed30d3e91dcba390efefcc", size = 774417, upload-time = "2026-08-07T10:48:19.611Z" },
{ url = "https://files.pythonhosted.org/packages/9e/3b/f4b1548d4f5b99186737fe27aec238e9823e8d5d23bf4df007c030689dc5/librt-0.15.0-cp314-cp314t-win32.whl", hash = "sha256:6912fa5e635d74529ac7cdb1bdf6ca3af4453da8d1edbe0110ee1cb4ad407ebf", size = 104381, upload-time = "2026-08-07T10:48:21.048Z" },
{ url = "https://files.pythonhosted.org/packages/80/b6/134afad262def1de04c0843c376d02135f1168af43f22e09a52bd8394727/librt-0.15.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8e11699ed745931c395acd3621b07062e0f840efa6935aad87a64ed0995f0915", size = 127034, upload-time = "2026-08-07T10:48:22.561Z" },
{ url = "https://files.pythonhosted.org/packages/99/5f/1b6846b20572bd699c9e9ec321a5f781845bee477df2aa2a43b28bc40119/librt-0.15.0-cp314-cp314t-win_arm64.whl", hash = "sha256:5d2a91724463bfed4f573cd7a9fdc856d2e230d0c0e5a61416a93481dccd8605", size = 110827, upload-time = "2026-08-07T10:48:23.804Z" },
{ url = "https://files.pythonhosted.org/packages/c6/44/4de9f4ddadb009a55c7758eb5736d62534a7daaf27bd71bc50e64b606b06/librt-0.15.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:8443e38dcfcfdbcf5add5118c623efd788d65ac2e25756d6251a54a06a4d0aca", size = 149843, upload-time = "2026-08-07T10:48:25.148Z" },
{ url = "https://files.pythonhosted.org/packages/1f/eb/5d9ab71e30119c44094e0275f38b47dd327aea0f843a080396677029d508/librt-0.15.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:6d15a29033c57490cfe2069097c6fc4049e4e65ffbb749be7dc453b7c4c68965", size = 154510, upload-time = "2026-08-07T10:48:26.485Z" },
{ url = "https://files.pythonhosted.org/packages/d0/9c/8505d1b8f5e8c19587bd03f7429993b3e9ce5c06819d856bfb11d919374c/librt-0.15.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d2c05c729b589e734c09578bf5964be48a911765484840d017bbc84f49d4c4ad", size = 497543, upload-time = "2026-08-07T10:48:28.045Z" },
{ url = "https://files.pythonhosted.org/packages/1d/9a/3a8390775cb095765aded027ac9c63e7c8ea74e731498607544c6505de0e/librt-0.15.0-cp315-cp315-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:fa60887537e1d0cd2d9982269d33a709bf54b195cd2b9364fc0a758022af5bd9", size = 480452, upload-time = "2026-08-07T10:48:29.531Z" },
{ url = "https://files.pythonhosted.org/packages/e7/40/258a4a7117ee915d66de5cd9b8ade65a440993161107ce3a686f1859955c/librt-0.15.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d8bc24219b24c0af375718942ab75e3544b2763085f40f965be4326734ae8328", size = 507768, upload-time = "2026-08-07T10:48:31.007Z" },
{ url = "https://files.pythonhosted.org/packages/6b/c6/2f4dd296c97a0b85b98894519b279408ec9dd602d4f692b1ea0e25dee670/librt-0.15.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:86a21a7bd3fe3a419512ef424cc1c020f6771d0b29cfddff36d1635a855e63f0", size = 525122, upload-time = "2026-08-07T10:48:32.7Z" },
{ url = "https://files.pythonhosted.org/packages/49/dd/29eab42be13b2bf0ea8cb227135a45d44693e30a7e8b92871981ff56b82b/librt-0.15.0-cp315-cp315-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dbab647e88d90b3167b91efe7091e248653688ed4337e4f90907a722c7361bb9", size = 520371, upload-time = "2026-08-07T10:48:34.294Z" },
{ url = "https://files.pythonhosted.org/packages/91/ed/4bad71adeca8fe208b775c2a35417fa5a2584c8f4791daaf89a89450fea1/librt-0.15.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:d8edcf6f550e918dca779c069b9e156385c60b406f99fc7641f32c52f7193659", size = 537258, upload-time = "2026-08-07T10:48:35.88Z" },
{ url = "https://files.pythonhosted.org/packages/4c/63/59dba6143fdcc7240c54458b629f3250000a61b8945890fc9efd451b19c5/librt-0.15.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:8b62076030baa2d8b1501a46bf0e19c27a489aa90671c55665bff7887f7660b0", size = 527432, upload-time = "2026-08-07T10:48:37.466Z" },
{ url = "https://files.pythonhosted.org/packages/ec/21/21a24c6a2327d8362580efebe77286bf47b0f4062ec5ea41766e609d3c7d/librt-0.15.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:d00d20d1818e82a07a0ee0aa89a98b17ed7916b92441090b683719cb20a59b6d", size = 548108, upload-time = "2026-08-07T10:48:39.384Z" },
{ url = "https://files.pythonhosted.org/packages/5a/6d/fc68c89a7971418b41f9a873623ff935cb864097544c6a2f8ce491c8ef5d/librt-0.15.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:4e6ee93fc3cf848dcbf0cce2eca73d8e7dcd0cc2b6df3a529d57750b30a4c55c", size = 529681, upload-time = "2026-08-07T10:48:41.392Z" },
{ url = "https://files.pythonhosted.org/packages/65/7e/c2d98766124400d722063a630b0fde38a9fc768705d37eecca15c47dc192/librt-0.15.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:32896a0af72508ea979e0acb4e4c04cbeeae04938167950d535c83c45597167d", size = 567736, upload-time = "2026-08-07T10:48:43.124Z" },
{ url = "https://files.pythonhosted.org/packages/55/6c/f8c34a95e3a515c6e1c192b89511e7253c89a7760c6b500d57ffdb8d2dc8/librt-0.15.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:ec3ba415afaf951f6951b1dd16d3c8e4f540065fc382d7e70b823a79567ca374", size = 81673, upload-time = "2026-08-07T10:48:44.645Z" },
{ url = "https://files.pythonhosted.org/packages/c9/9e/e23fa8e78679ec45728188650b39e8ff476c83b691c96f749217df3b1b7c/librt-0.15.0-cp315-cp315-win32.whl", hash = "sha256:d2813ba2503764f0450680c533d13df7cff9b49df1411062eded5f67db4195b9", size = 100081, upload-time = "2026-08-07T10:48:46.171Z" },
{ url = "https://files.pythonhosted.org/packages/e1/dc/3eb4c5e297343f0620a55532cd7c8d764d3001fa2159212dadf480464827/librt-0.15.0-cp315-cp315-win_amd64.whl", hash = "sha256:b87d67e33afaf265262f2a66db578284b88ee2e6fcd224579cb5c15518677ad8", size = 121228, upload-time = "2026-08-07T10:48:47.631Z" },
{ url = "https://files.pythonhosted.org/packages/97/70/43abce19f04e49762f8ec834c8fafee13cc40fd6b94a72a24e534febfcd0/librt-0.15.0-cp315-cp315-win_arm64.whl", hash = "sha256:713bd7df21170b982e729e46870f31d6b437bd1a9b4648cffb529bd3c2ec5c4b", size = 106487, upload-time = "2026-08-07T10:48:49.095Z" },
{ url = "https://files.pythonhosted.org/packages/de/15/83f2deddb9368b8951ec8c9477269b5b9b8bd9bbf15e57402d0f38817dca/librt-0.15.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:3de789c82752730f94782a5ee518baf9c05edf85733aeaf73bb6e518755cdf54", size = 159448, upload-time = "2026-08-07T10:48:50.649Z" },
{ url = "https://files.pythonhosted.org/packages/06/bf/043097353f9b3c73b583d07f6b8e552795463f4bfc8caf85e42eee50c26a/librt-0.15.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:e0b5deec9a8664eb722c797241970fd4aa1894d25fda36a1ddac0f7407606bd6", size = 161686, upload-time = "2026-08-07T10:48:52.174Z" },
{ url = "https://files.pythonhosted.org/packages/f4/2a/8ae77f9719d42ce71cd708560a3557b38ac3c17a0383e57f87084de45bbe/librt-0.15.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5563302a8359bc2295bb7084d1a8ed1519df96afb30eb2aa4e0bff7b54228988", size = 710668, upload-time = "2026-08-07T10:48:53.782Z" },
{ url = "https://files.pythonhosted.org/packages/61/34/c0436ea134deb9a0d6da80a396a2739a81cb31e0418f7227239e23140898/librt-0.15.0-cp315-cp315t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:22d6263b9d39d7bbb286fa791945646e3218f1be2d693e36fb630f1d0e59cd13", size = 679396, upload-time = "2026-08-07T10:48:55.645Z" },
{ url = "https://files.pythonhosted.org/packages/4a/9f/001e0d99aa9250d5cd5715a9081291a20656083459f9019cda15255329e1/librt-0.15.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:39ffd14646190c454f0d86e0d256b33f00a87a26ab410e619773b841d0e41416", size = 704313, upload-time = "2026-08-07T10:48:57.46Z" },
{ url = "https://files.pythonhosted.org/packages/2d/53/b34fa9d0ff00f136f4d58ebb4c411ff634baed1eb412bb602a2bc8dcafcb/librt-0.15.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c47318cd3a61401452de11282242937e3e057c4fd3dbaf601e269d0928a06c0a", size = 729847, upload-time = "2026-08-07T10:48:59.231Z" },
{ url = "https://files.pythonhosted.org/packages/86/ac/fa4d7a424665040e95baf480a6d523446057684b6758624c85338e8a23b2/librt-0.15.0-cp315-cp315t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a56a1d4f859a82ca5b99fc4b82c9b027b15e3c455c5cd99e7d0719f27bb20b6c", size = 742736, upload-time = "2026-08-07T10:49:01.151Z" },
{ url = "https://files.pythonhosted.org/packages/8a/f1/e17a9bb5de6fb8c3186ed1a7d68d21618b027ac2d3633e03d3b6109c67ae/librt-0.15.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:077471b3182db4e17c36ae91555f36a4d2c00080b267f749bcad34a478a9a302", size = 763454, upload-time = "2026-08-07T10:49:03.039Z" },
{ url = "https://files.pythonhosted.org/packages/1d/ec/ecd02cd30935b931b9cdbfed6ab5a099c51b280b4e7baa274da80978ed27/librt-0.15.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:411ca4d1b905b860ceba7570dd6717a71dedaddcc4b0f77ece710aa41ee11f8d", size = 743296, upload-time = "2026-08-07T10:49:04.941Z" },
{ url = "https://files.pythonhosted.org/packages/e6/b5/b3c2b8353ce820a4854f78d19321344242f89fa71c975b71132ba9bf242a/librt-0.15.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:1256589e0b0adb31751d685a68bce29d73407ddf4ef05d4188f49d5dcf9566d9", size = 756217, upload-time = "2026-08-07T10:49:06.825Z" },
{ url = "https://files.pythonhosted.org/packages/3c/52/6cc22542ba59146b05cca2a656f9ff8bb67e38e63d12c3b0cc183d837bf1/librt-0.15.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:f42b74a53e5f26a0ba0007411a7455b66c67ce4022a39cc1f56fc4efd65bcbab", size = 741934, upload-time = "2026-08-07T10:49:08.839Z" },
{ url = "https://files.pythonhosted.org/packages/40/32/a04b72b1aa86e3be23b2ecff8c1aad2dcc955bd3956d6d26e7e34267e57a/librt-0.15.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:291bf73caf78b9e88d6fae9bfd693207ff7d832e2fdbe2cf8e746bc13f5f892b", size = 783763, upload-time = "2026-08-07T10:49:10.661Z" },
{ url = "https://files.pythonhosted.org/packages/6c/f0/89eb11dffbe9279ff37144dec786927314502ae0b114f1449dc78c458aab/librt-0.15.0-cp315-cp315t-win32.whl", hash = "sha256:c16d15ee371643ab48dc8248a3e680ebbeca573a13af2c3dd0c985b142d77162", size = 104313, upload-time = "2026-08-07T10:49:12.305Z" },
{ url = "https://files.pythonhosted.org/packages/6d/4a/1f1978c200f563beda63c36adff2d65bbecb81e365e8e69e572f5f70fbc6/librt-0.15.0-cp315-cp315t-win_amd64.whl", hash = "sha256:dbd605739f228912dc49027cb764456b9757750bdc2b6b7773164db7096c6fd1", size = 126889, upload-time = "2026-08-07T10:49:13.881Z" },
{ url = "https://files.pythonhosted.org/packages/38/a6/800800bfed7b1fb10fc3f3d557785c3854e80d3f7a9800d784b176a1fc2d/librt-0.15.0-cp315-cp315t-win_arm64.whl", hash = "sha256:84d244b00604d17df3fc7736c327892d6bba66181254aa4087be807b6c342bdc", size = 110700, upload-time = "2026-08-07T10:49:15.499Z" },
] ]
[[package]] [[package]]
name = "mako" name = "mako"
version = "1.3.12" version = "1.4.2"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "markupsafe" }, { name = "markupsafe" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/00/62/791b31e69ae182791ec67f04850f2f062716bbd205483d63a215f3e062d3/mako-1.3.12.tar.gz", hash = "sha256:9f778e93289bd410bb35daadeb4fc66d95a746f0b75777b942088b7fd7af550a", size = 400219, upload-time = "2026-04-28T19:01:08.512Z" } sdist = { url = "https://files.pythonhosted.org/packages/05/7f/0b891a82379ffd96d2d8cc6d435fb1055cdaed52829287f81d2a43fd71cb/mako-1.4.2.tar.gz", hash = "sha256:7227a060ee3af55a493af61d73d6beef13044b81dba9586736c13945f09464ce", size = 412231, upload-time = "2026-09-22T20:40:55.06Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/bc/b1/a0ec7a5a9db730a08daef1fdfb8090435b82465abbf758a596f0ea88727e/mako-1.3.12-py3-none-any.whl", hash = "sha256:8f61569480282dbf557145ce441e4ba888be453c30989f879f0d652e39f53ea9", size = 78521, upload-time = "2026-04-28T19:01:10.393Z" }, { url = "https://files.pythonhosted.org/packages/f0/bb/0e4c30a18858adebc75496ea8a8c3fb65cbf4b36ea1c5028ed8a0a5fa196/mako-1.4.2-py3-none-any.whl", hash = "sha256:e70a2a4308952b33a21208bfd6725da79fae5a8c224578f8cfdeb60e5ace03df", size = 80163, upload-time = "2026-09-22T20:40:56.423Z" },
] ]
[[package]] [[package]]
@@ -747,7 +836,7 @@ wheels = [
[[package]] [[package]]
name = "mypy" name = "mypy"
version = "2.1.0" version = "2.3.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "ast-serialize" }, { name = "ast-serialize" },
@@ -756,23 +845,35 @@ dependencies = [
{ name = "pathspec" }, { name = "pathspec" },
{ name = "typing-extensions" }, { name = "typing-extensions" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/82/15/cca9d88503549ed6fedeaa1d448cdddd542ee8a490232d732e278036fbf2/mypy-2.1.0.tar.gz", hash = "sha256:81e76ad12c2d804512e9b13240d1588316531bfba07558286078bfbce9613633", size = 3898359, upload-time = "2026-05-11T18:37:36.237Z" } sdist = { url = "https://files.pythonhosted.org/packages/82/6a/878cc1097d4035f82bd516658d0c528d2a9955bc7b363afcbd0b07fea11b/mypy-2.3.1.tar.gz", hash = "sha256:47c1b1207258513a9d93495f69c8be9de73916186f0e52703e8c461b7a623419", size = 3992554, upload-time = "2026-08-15T03:03:38.549Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/b0/ca/b279a672e874aedd5498ae25f722dacc8aa86bbffb939b3f97cbb1cf6686/mypy-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7354c5a7f69d9345c3d6e69921d57088eea3ddeeb6b20d34c1b3855b02c36ec2", size = 14848422, upload-time = "2026-05-11T18:35:45.984Z" }, { url = "https://files.pythonhosted.org/packages/be/c4/42a49d44aeff804edf1b19acce0b49e8bd1a9c57dee9605dd8d980aa43d7/mypy-2.3.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:192abaedf75da1bc0b1cef104927e70ec49c1ef0031cc4825c7ee10a438ed24d", size = 13986778, upload-time = "2026-08-15T03:01:33.69Z" },
{ url = "https://files.pythonhosted.org/packages/27/e6/3efe56c631d959b9b4454e208b0ac4b7f4f58b404c89f8bec7b49efdfc21/mypy-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:49890d4f76ac9e06ec117f9e09f3174da70a620a0c300953d8595c926e80947f", size = 13677374, upload-time = "2026-05-11T18:36:57.188Z" }, { url = "https://files.pythonhosted.org/packages/45/13/9331fd2dfed7194d66c5304072894a8be3e51e9deda6863c1eceaa35a43d/mypy-2.3.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf678dffd16efcda2c15cbd30e9ecc0081388e29ea23687a88e686ed92638dc3", size = 14188467, upload-time = "2026-08-15T03:02:40.554Z" },
{ url = "https://files.pythonhosted.org/packages/84/7f/8107ea87a44fd1f1b59882442f033c9c3488c127201b1d1d15f1cbd6022e/mypy-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:761be68e023ef5d94678772396a8af1220030f80837a3afd8d0aef3b419666f4", size = 14055743, upload-time = "2026-05-11T18:35:18.361Z" }, { url = "https://files.pythonhosted.org/packages/78/f7/f4a34edab45667c5465855dc585a20e87978ffa8aee711445b7239d120c6/mypy-2.3.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e036f06b41630f4c8a1d48f9ac6aa26acc65f8be089973f5519da643318f03f", size = 15225538, upload-time = "2026-08-15T03:03:09.761Z" },
{ url = "https://files.pythonhosted.org/packages/51/4d/b6d34db183133b83761b9199a82d31557cdbb70a380d8c3b3438e11882a3/mypy-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c90345fc182dc363b891350457ec69c35140858538f38b4540845afcc32b1aef", size = 15020937, upload-time = "2026-05-11T18:34:59.618Z" }, { url = "https://files.pythonhosted.org/packages/40/05/534b3590757bd05794f73e07f6666c2a77b8597ffed795c94ce570096aa0/mypy-2.3.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:71af9c8a894e862b58e92abb08e53b05a384a1e5e5d6dc7cda59126211a53d82", size = 15480805, upload-time = "2026-08-15T03:01:41.134Z" },
{ url = "https://files.pythonhosted.org/packages/ff/d7/f08360c691d758acb02f45022c34d98b92892f4ea756644e1000d4b9f3d8/mypy-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b84802e7b5a6daf1f5e15bc9fcd7ddae77be13981ffab037f1c67bb84d67d135", size = 15253371, upload-time = "2026-05-11T18:36:41.081Z" }, { url = "https://files.pythonhosted.org/packages/55/da/bdfba852e2562f599624af5bb7d29e36b0b4f526f2b8bac85efe0dd1803d/mypy-2.3.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:3c80cd23d85368bdd9f37d5231dfd97d35bcbf5bf41af96ef3a9b078ad1957f9", size = 7761712, upload-time = "2026-08-15T03:02:36.008Z" },
{ url = "https://files.pythonhosted.org/packages/67/1b/09460a13719530a19bce27bd3bc8449e83569dd2ba7faf51c9c3c30c0b61/mypy-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:022c771234936ceac541ebaf836fe9e2abeb3f5e09aff21588fe543ff006fe21", size = 11326429, upload-time = "2026-05-11T18:34:13.526Z" }, { url = "https://files.pythonhosted.org/packages/98/31/60fc64a74cdba4f2a5d642d32317993e479163e1ac7d91b695e5d15e2264/mypy-2.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:4956f34d145e145562a0a0bf367f642bbc85c04ec2baf47ae015947c3169a85d", size = 11423968, upload-time = "2026-08-15T03:02:06.931Z" },
{ url = "https://files.pythonhosted.org/packages/40/62/75dbf0f82f7b6680340efc614af29dd0b3c17b8a4f1cd09b8bd2fd6bc814/mypy-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:498207db725cec88829a6a5c2fc771205fd043719ef98bc49aba8fb9fc4e6d57", size = 10218799, upload-time = "2026-05-11T18:32:23.491Z" }, { url = "https://files.pythonhosted.org/packages/a9/23/eb5950b24cd26ba3b78f87707a275568d633c77dae8e61c9661be6055ca6/mypy-2.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:cfb12e360242d23d91f5e978d94f58ea66acf5804c4fb6f2f794a20d4cb1b595", size = 10399323, upload-time = "2026-08-15T03:02:33.671Z" },
{ url = "https://files.pythonhosted.org/packages/b2/66/caca04ed7d972fb6eb6dd1ccd6df1de5c38fae8c5b3dc1c4e8e0d85ee6b9/mypy-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7d5e5cad0efeba72b93cd17490cc0d69c5ac9ca132994fe3fb0314808aeeb83e", size = 15923458, upload-time = "2026-05-11T18:35:28.64Z" }, { url = "https://files.pythonhosted.org/packages/82/c7/f80f4e46c0b9a00eb5f78a79d49dda8bdf56a5230f7257fb33e76be04da7/mypy-2.3.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5f1c50bb05b64e2026b52867e8d21106f01313c744a2c4ecc34c90d12e8d6e2", size = 15121308, upload-time = "2026-08-15T03:01:46.053Z" },
{ url = "https://files.pythonhosted.org/packages/ed/52/2d90cbe49d014b13ed7ff337930c30bad35893fe38a1e4641e756bb62191/mypy-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ff715050c127d724fd260a2e666e7747fdd83511c0c47d449d98238970aef780", size = 14757697, upload-time = "2026-05-11T18:36:14.208Z" }, { url = "https://files.pythonhosted.org/packages/5d/74/9b04f17c7074cc5188f02fb63a2ca1d43fedf479e84fe3091c39061a1d7f/mypy-2.3.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:667196b352f4cf304ded4c10f90cfc179263a1acfb3cdcfa984bdfd340d498bc", size = 15536590, upload-time = "2026-08-15T03:01:35.941Z" },
{ url = "https://files.pythonhosted.org/packages/ac/37/d98f4a14e081b238992d0ed96b6d39c7cc0148c9699eb71eaa68629665ea/mypy-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82208da9e09414d520e912d3e462d454854bed0810b71540bb016dcbca7308fd", size = 15405638, upload-time = "2026-05-11T18:33:48.249Z" }, { url = "https://files.pythonhosted.org/packages/26/04/c837ef6208e567774e2ed1f863f8ba6ec4817b1b6dd426315e5d559b6ec9/mypy-2.3.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9c53e395c12cad2c6d4b67d5da7c6057638a132d85c08b73646b18f802a0045", size = 16791074, upload-time = "2026-08-15T03:01:31.073Z" },
{ url = "https://files.pythonhosted.org/packages/a3/c2/15c46613b24a84fad2aea1248bf9619b99c2767ae9071fe224c179a0b7d4/mypy-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e79ebc1b904b84f0310dff7469655a9c36c7a68bddb37bdd42b67a332df61d08", size = 16215852, upload-time = "2026-05-11T18:32:50.296Z" }, { url = "https://files.pythonhosted.org/packages/37/68/48730230afa45192d5bd429a6a2ff24a6f8dedda90fdf2b221792b54518f/mypy-2.3.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:18162b128c3f9c703cd35f5537446900b0d21a2549aa7a95d21380d2ef643fb0", size = 17069183, upload-time = "2026-08-15T03:02:28.566Z" },
{ url = "https://files.pythonhosted.org/packages/5c/90/9c16a57f482c76d25f6379762b56bbf65c711d8158cf271fb2802cfb0640/mypy-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e583edc957cfb0deb142079162ae826f58449b116c1d442f2d91c69d9fced081", size = 16452695, upload-time = "2026-05-11T18:33:38.182Z" }, { url = "https://files.pythonhosted.org/packages/1c/ea/ca23fc9c20eeda09a15c9cbcf50015d0e73f409f6ead059e42aa69a608ff/mypy-2.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:30c0477d4aab7b7f39c8397dc877f2c96b9fe5588ec379f372c56eb63d599f63", size = 12154679, upload-time = "2026-08-15T03:02:04.809Z" },
{ url = "https://files.pythonhosted.org/packages/0f/4c/215a4eeb63cacc5f17f516691ea7285d11e249802b942476bff15922a314/mypy-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b33b6cd332695bba180d55e717a79d3038e479a2c49cc5eb3d53603409b9a5d7", size = 12866622, upload-time = "2026-05-11T18:34:39.945Z" }, { url = "https://files.pythonhosted.org/packages/3b/67/8d982126034990869466f73b8db80dcb2234a7ac39b4dad093e047a79835/mypy-2.3.1-cp314-cp314t-win_arm64.whl", hash = "sha256:6941ab3619377bc3f32ca02876b07d27f216f5201604b664d3937ea0fdd23bb4", size = 10969159, upload-time = "2026-08-15T03:02:38.152Z" },
{ url = "https://files.pythonhosted.org/packages/4b/50/1043e1db5f455ffe4c9ab22747cd8ca2bc492b1e4f4e21b130a44ee2b217/mypy-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:4f910fe825376a7b66ef7ca8c98e5a149e8cd64c19ae71d84047a74ee060d4e6", size = 10610798, upload-time = "2026-05-11T18:36:31.444Z" }, { url = "https://files.pythonhosted.org/packages/ee/f7/41e7f2d8117fbc7a7587286162ffe2f688984b69c46ed63cf5f2e4fc3bae/mypy-2.3.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:6f041a6de52c9217ca125e78ba0a335cb7fd98a1c0580978e49ab2b126f70b57", size = 13990694, upload-time = "2026-08-15T03:03:21.919Z" },
{ url = "https://files.pythonhosted.org/packages/0d/2a/13ca1f292f6db1b98ff495ef3467736b331621c5917cad984b7043e7348d/mypy-2.1.0-py3-none-any.whl", hash = "sha256:a663814603a5c563fb87a4f96fb473eeb30d1f5a4885afcf44f9db000a366289", size = 2693302, upload-time = "2026-05-11T18:31:29.246Z" }, { url = "https://files.pythonhosted.org/packages/06/85/8f665811a0c8f3bf6fa1d9acd665ec2d97a2bcc453ae68dcd92340941cd6/mypy-2.3.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5159ae60f5dbc3a498af5ba8365505808ac8031bc63f9e00304ad545d40bdd9b", size = 14203518, upload-time = "2026-08-15T03:01:48.455Z" },
{ url = "https://files.pythonhosted.org/packages/2d/82/91b866c8546b120bff83b73a439d90d2d63ef3aff113599e6b8e4d566848/mypy-2.3.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47a8a7a0a7f6f6e63995c0ac36fa0c07b127413fdc81f0439b7f3dccafd33561", size = 15220224, upload-time = "2026-08-15T03:01:23.577Z" },
{ url = "https://files.pythonhosted.org/packages/c8/78/c226c99208ee40de7c768369fa533f933afa003dfdc606ff021450724e91/mypy-2.3.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2329c0501293d4e1f33bc15d04d6304d65a1cdda967ee93a05c1e681a3923133", size = 15501512, upload-time = "2026-08-15T03:02:09.453Z" },
{ url = "https://files.pythonhosted.org/packages/a9/e7/7cfb3f106c393979f4cc37ad6c0586044d50401e3c35b0c003e4f3ba6bc9/mypy-2.3.1-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:bb26deed807bdb0457cf3e3f1cd7c4a1cf9d66864eaf1b4a61e06805d4c6b1f9", size = 7761913, upload-time = "2026-08-15T03:01:55.65Z" },
{ url = "https://files.pythonhosted.org/packages/99/3c/52affefa273b97939a1f474ae4a349c8718635c15b941112dfab4291b0c1/mypy-2.3.1-cp315-cp315-win_amd64.whl", hash = "sha256:375d7013876a8233b2d05be185bfa09f689696cd999ce8b1cfe6acac5c80e8a3", size = 11422533, upload-time = "2026-08-15T03:03:24.101Z" },
{ url = "https://files.pythonhosted.org/packages/2a/b7/75643e70c72a5b346d8a9b1543c967ea8824df2ee3fb7ccba652c272b7bb/mypy-2.3.1-cp315-cp315-win_arm64.whl", hash = "sha256:586b3612214cceabb3c0f588c97e7d1e535393f06a60e912e994f6b3ace97523", size = 10397931, upload-time = "2026-08-15T03:02:55.265Z" },
{ url = "https://files.pythonhosted.org/packages/10/ce/53be21f2d4adfcd26f63f1184a13ed797015ab463853f117e2e11e4d726f/mypy-2.3.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:ef0c6335cda9d807f8193d8ff6204a72bc909fa9882aacbca14f43cdb7188306", size = 15118669, upload-time = "2026-08-15T03:02:51.479Z" },
{ url = "https://files.pythonhosted.org/packages/62/43/20de757cd42989d291a17fad607742c4c74e875ce5cea00e5a5225020ac1/mypy-2.3.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e598c8c66401d26b150872154a286e6d484cf2789c3bb28a7556806298423021", size = 15545627, upload-time = "2026-08-15T03:03:05.132Z" },
{ url = "https://files.pythonhosted.org/packages/7e/fc/092bdf77ad280eaf501422f0f3b966012b528076cc13e41a774861c907d1/mypy-2.3.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eda22fd4efa9dcd39331d1dede9b5b8b8a7fd69af07592e778433da98610d29e", size = 16764157, upload-time = "2026-08-15T03:02:23.958Z" },
{ url = "https://files.pythonhosted.org/packages/94/5c/c94c4d62d909b07f552d0d9356d7acc943825558e602a64822ffa2231536/mypy-2.3.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:2a0ba2e57847849fb0d1fcdabb32786d223095ed8bc121dfe322bcdb3d9c46bc", size = 17073258, upload-time = "2026-08-15T03:02:14.573Z" },
{ url = "https://files.pythonhosted.org/packages/c0/f7/511a88b89e478053c02d22039bb8f3ce4183efe8fd7a4f0a5910a8bb0a32/mypy-2.3.1-cp315-cp315t-win_amd64.whl", hash = "sha256:3f7e865dd51f235f60a2dbcd8728a1c095f5ca28f095d48a725b84cd935735c4", size = 12135505, upload-time = "2026-08-15T03:02:16.714Z" },
{ url = "https://files.pythonhosted.org/packages/71/bf/02573b56964ecb0f7c644f915f53c325ae15c3faec521c5adf11599a32df/mypy-2.3.1-cp315-cp315t-win_arm64.whl", hash = "sha256:8ad80807dc3ab8ea978b1b2b6e4a657194ace1d4ef03e0e731aff1abd517da29", size = 10962647, upload-time = "2026-08-15T03:01:43.712Z" },
{ url = "https://files.pythonhosted.org/packages/8e/41/9675c7a1e78edecfba0b79e587a52594c56e189368261dc7b3a7fffb9527/mypy-2.3.1-py3-none-any.whl", hash = "sha256:6ed5c7e3419083268e5c9258bd1c1ef91af44a9e89374dbcaf37b775716e72eb", size = 2754338, upload-time = "2026-08-15T03:02:53.4Z" },
] ]
[[package]] [[package]]
@@ -813,38 +914,30 @@ wheels = [
[[package]] [[package]]
name = "prek" name = "prek"
version = "0.4.11" version = "0.5.3"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c4/1a/73b6dae5ce7e997cb35a69bfe1d25a798e85fa3d2eabf95324563f461b30/prek-0.4.11.tar.gz", hash = "sha256:4a14cb9bbae850605ae3904fbdbb12f0e00c12455efaa2266da8fb8e5c0350d7", size = 516254, upload-time = "2026-07-24T17:05:35.107Z" } sdist = { url = "https://files.pythonhosted.org/packages/54/93/d1e5afc996b9fde04d71c37fd28bdd404e54daf1da6c76b883c5cdbdb411/prek-0.5.3.tar.gz", hash = "sha256:06d88bed9a5b2886cd3796957e4cecf05fa9b06724b625df31dbf0a48ff2330c", size = 551929, upload-time = "2026-09-13T08:38:37.823Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/8e/d7/a00b2de492a80e99b1698e72c1d196ac3ed544dc7ee0ada261ac066e78e0/prek-0.4.11-py3-none-linux_armv6l.whl", hash = "sha256:3830cb7cc47e837888b8b464ecb21355a69235cfacef0fd89101e17f09345d63", size = 5770511, upload-time = "2026-07-24T17:05:12.829Z" }, { url = "https://files.pythonhosted.org/packages/db/5d/8731dc49cb5424d37424db3ea2f3ff6a0b20caa971db72796e822ce48b24/prek-0.5.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2d7240c5e6a996ef5bebe5d57d43049803e41c225b343681c020bf0c87dab2b2", size = 6003518, upload-time = "2026-09-13T08:38:20.584Z" },
{ url = "https://files.pythonhosted.org/packages/c1/1e/f97c74defcd5d5645888cb99fcdd9b8b48cc7247cf31e64414d106d56d66/prek-0.4.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:45facadf9c2332b28e6ab2744312ae0275a93ab5a437da8bcc53a8c7260cb4b0", size = 6118049, upload-time = "2026-07-24T17:05:14.451Z" }, { url = "https://files.pythonhosted.org/packages/51/45/ca413aefb3ea2411bbf0adb6eebfac1e9bfc5ac7679dbe26336fc651c347/prek-0.5.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1dd6df8235ca361dbbeec89c305ccb41a138088a2fc548484b4cb89f66a80d20", size = 5538754, upload-time = "2026-09-13T08:38:23.004Z" },
{ url = "https://files.pythonhosted.org/packages/d9/92/8367d26421ee6fe6019a63928fb0ed31179cd0d6199879c524f89ef4c95c/prek-0.4.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2f8a194b1d00d24dff8baff691c99e2668339da2da3482b4ee99c1a0f2409378", size = 5601478, upload-time = "2026-07-24T17:05:15.81Z" }, { url = "https://files.pythonhosted.org/packages/dc/92/874d58ba40d2fe7cfec2789ae8bf415f3751666ad5c59928821fe53c3c01/prek-0.5.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:61ff791bb850ba52cc0d86576498df7e1bd1e9ed20df63dae113a4a6aa495e19", size = 5841568, upload-time = "2026-09-13T08:38:25.008Z" },
{ url = "https://files.pythonhosted.org/packages/e1/6f/7617c9b87afaadede4167720aae7ef12d7e51167db903bc8fbac0cadef7b/prek-0.4.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:21d93e6d76bf3d7a9bb70c6ac86ef372adaee50068c45749e6b9e1c2ac4ec939", size = 5932071, upload-time = "2026-07-24T17:05:17.217Z" }, { url = "https://files.pythonhosted.org/packages/8f/40/167037b8eef75f6f036c60ff7e93f42defebb7c3733677e2f30e5a479257/prek-0.5.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a863379a2668c4ef079d820ffd5b14073145380dfbe0e70fef498dc73c568f86", size = 6226960, upload-time = "2026-09-13T08:38:26.788Z" },
{ url = "https://files.pythonhosted.org/packages/ef/41/6796a4011b04212333259064aa885a71d03d9581ba9bff52db3ce58d1f06/prek-0.4.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7fb07cde2d2156efa6980122b3f13dc88f20c84b933c6205675d0f1e7be2cde8", size = 5677617, upload-time = "2026-07-24T17:05:18.658Z" }, { url = "https://files.pythonhosted.org/packages/42/9a/bfcd3c1fe1fdefee8dc144a92c4b0b93b59f31c7df60ee1720c49223a90a/prek-0.5.3-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:086d1b77557f0a089568dc5b93c22ccd793351524f3eeb29d36781421a350e72", size = 5854832, upload-time = "2026-09-13T08:38:28.954Z" },
{ url = "https://files.pythonhosted.org/packages/03/5f/3e339901f8460b6073313619b4fd9bf4135e48430695c53640b83ace88ea/prek-0.4.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6b7f5e446d2aca739bd18b380578e9c78bb4c086299abc3e167d51c47840c56f", size = 6106370, upload-time = "2026-07-24T17:05:20.219Z" }, { url = "https://files.pythonhosted.org/packages/15/41/1450b995b18bbf0d8ab65016860198052baad5c79c231151fa1dcca4a2de/prek-0.5.3-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:4f09b947255124e9591a7f7ef16eb5340a36ba94a275d788d7ea804fab35f0e2", size = 6343055, upload-time = "2026-09-13T08:38:30.96Z" },
{ url = "https://files.pythonhosted.org/packages/8e/4e/94e24b5c1910ec15692ecaf33d0d8ef0d02a02a8b5e40d3c976396880cba/prek-0.4.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7059d640e595d098600e2d97af961f46292b4112670edbe632de84f6828389e", size = 6884342, upload-time = "2026-07-24T17:05:21.587Z" }, { url = "https://files.pythonhosted.org/packages/9c/e8/8f45950b8a8ddcf22efc7183e9d6b1d7e30373229ff7d9e56f918a55922c/prek-0.5.3-py3-none-win_amd64.whl", hash = "sha256:5b74a9742c3e8f8688d5dad1439688f112e2e1746fcb91016015dae38df3eb42", size = 5744293, upload-time = "2026-09-13T08:38:33.104Z" },
{ url = "https://files.pythonhosted.org/packages/a2/7c/fc0daa033dcafe74990c00af2da1e16922790b9c1b8da7e8eebf1123838b/prek-0.4.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85a4df33998fcac878bce3b2c624e1caeb9609f3d562c640702c1234ed815daf", size = 6331365, upload-time = "2026-07-24T17:05:22.87Z" }, { url = "https://files.pythonhosted.org/packages/24/cd/ce637f4b6cc6c583516dc16b539408dab593672f7c6fe7723a37caf378ff/prek-0.5.3-py3-none-win_arm64.whl", hash = "sha256:20d92aef53a5e237f4659ecc6669c0fbc98a1b83f0e2476530400a72d42ca390", size = 5512958, upload-time = "2026-09-13T08:38:36.123Z" },
{ url = "https://files.pythonhosted.org/packages/d7/36/49f152b8f539930e9685cff0509695ce4054abcecc22f4c16c4e1e5c23d0/prek-0.4.11-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:866991f387527c5f880ce2cc3ddea9b33cc5b986881f8c7f92524cf0969c1350", size = 5939075, upload-time = "2026-07-24T17:05:24.249Z" },
{ url = "https://files.pythonhosted.org/packages/4f/fe/7b097af9161edae7ddedcc9f5cda4a5f31346a492ce3f92583d96c46f628/prek-0.4.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:247e5d8740e137ebdf24fa96f01a95b2d2f1892e956a1ab00c7b1474a59ebcc0", size = 5799029, upload-time = "2026-07-24T17:05:25.652Z" },
{ url = "https://files.pythonhosted.org/packages/49/63/dc955ff99e1002d3cd375b21467c87046bc41deddfce86d898240757b151/prek-0.4.11-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:603ba9f2fd9d666dddb3ae190a25a5c55091b843cde90ed52e0a1116f50d4062", size = 5651211, upload-time = "2026-07-24T17:05:27.027Z" },
{ url = "https://files.pythonhosted.org/packages/8f/b8/bf2139ec25eefb5afef43afac7620c5181f2c2661f220598beacb1771207/prek-0.4.11-py3-none-musllinux_1_1_i686.whl", hash = "sha256:f2682ece3c5fc7201106c4fdd84b0587ccea4b8a2ecefa3e94d3074d2841f1df", size = 5954784, upload-time = "2026-07-24T17:05:28.391Z" },
{ url = "https://files.pythonhosted.org/packages/fc/29/3fe5990aee1bd7c4d50a03358ad867c5e912d9510aafb83a359c7347e5fa/prek-0.4.11-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:22721d30394192931fecc80d6fc6dd47e8ddf8db8b9693805aba8ec0f50087ec", size = 6448916, upload-time = "2026-07-24T17:05:29.837Z" },
{ url = "https://files.pythonhosted.org/packages/0f/fb/abddacf43738302242ecd237236c7c80cd7c1d27545e16e803770aee76e9/prek-0.4.11-py3-none-win32.whl", hash = "sha256:8b093e7624522146049e994d5cf283d01d71632b638620b79ae0f96afeaa2624", size = 5483539, upload-time = "2026-07-24T17:05:31.228Z" },
{ url = "https://files.pythonhosted.org/packages/00/1e/c293f7a15cb93963c4be36a02e144b93f43906bc02644a3f04e5708e7453/prek-0.4.11-py3-none-win_amd64.whl", hash = "sha256:5a3d7c80b970b456e5f1bcec8382008ee1ae6a3f324a6b9bb4ff7e666ab0f3c4", size = 5861119, upload-time = "2026-07-24T17:05:32.479Z" },
{ url = "https://files.pythonhosted.org/packages/cd/0c/05fe6eb9d6a54d0e02dfa8cc5ad6f86869bf953419ec15909a32466a28ee/prek-0.4.11-py3-none-win_arm64.whl", hash = "sha256:e7b0df37ce05e45a14a9da39ab104691474d72f139bf4f6c860f754763a322cb", size = 5626386, upload-time = "2026-07-24T17:05:33.813Z" },
] ]
[[package]] [[package]]
name = "psycopg" name = "psycopg"
version = "3.3.4" version = "3.3.6"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "tzdata", marker = "sys_platform == 'win32'" }, { name = "tzdata", marker = "sys_platform == 'win32'" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/db/2f/cb91e5502ec9de1de6f1b76cfbf69531932725361168bb06963620c77e2e/psycopg-3.3.4.tar.gz", hash = "sha256:e21207764952cff81b6b8bdacad9a3939f2793367fdac2987b3aac36a651b5bc", size = 165799, upload-time = "2026-05-01T23:31:55.179Z" } sdist = { url = "https://files.pythonhosted.org/packages/76/26/3ea4ca5eaea1c0debcdf7ee7c1613fbe721dc27a03c461c0817ffd8a0601/psycopg-3.3.6.tar.gz", hash = "sha256:c081f2250df751a943036e42db6df4571c66cd0aabe8291a7a506512b12007d2", size = 168171, upload-time = "2026-09-18T13:22:55.152Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/5c/e0/7b3dee031daae7743609ce3c746565d4a3ed7c2c186479eb48e34e838c64/psycopg-3.3.4-py3-none-any.whl", hash = "sha256:b6bbc25ccf05c8fad3b061d9db2ef0909a555171b84b07f29458a447253d679a", size = 213001, upload-time = "2026-05-01T23:20:50.816Z" }, { url = "https://files.pythonhosted.org/packages/4e/de/748bd7609c71cae5d737f0ba9192f19329f70180ecda8fff3cac02c5abe3/psycopg-3.3.6-py3-none-any.whl", hash = "sha256:a1db9f7148b06a28606767efaca51fa6f9398c5c0a3810519be69d7000bdb631", size = 215490, upload-time = "2026-09-18T13:15:29.374Z" },
] ]
[package.optional-dependencies] [package.optional-dependencies]
@@ -854,20 +947,31 @@ binary = [
[[package]] [[package]]
name = "psycopg-binary" name = "psycopg-binary"
version = "3.3.4" version = "3.3.6"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/48/a6/828c9185701dab71b234c2a76c38a08b098ebfec5020716b4e93807492b5/psycopg_binary-3.3.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:28b7398fdd19db3232c884fb24550bdfe951221f510e195e233299e4c9b78f97", size = 4607292, upload-time = "2026-05-01T23:30:38.962Z" }, { url = "https://files.pythonhosted.org/packages/6d/b9/60711317c284a442511644ea7185b56ebe627606d6741e732cd16108c47b/psycopg_binary-3.3.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:b3f75dee0f9afafabe4edc52c4842f1e1878ed2069bd05b22d6fe961e97e4dba", size = 4720512, upload-time = "2026-09-18T13:20:29.278Z" },
{ url = "https://files.pythonhosted.org/packages/92/58/5b40dbc9d839045c9dae956960e4fb6d20bcabe6c59a2aa34fc3a371913f/psycopg_binary-3.3.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1fbaa292a3c8bb61b45df1ad3da1908ccee7cb889db9425e3557d9e34e2a4829", size = 4687023, upload-time = "2026-05-01T23:30:47.227Z" }, { url = "https://files.pythonhosted.org/packages/63/da/28befc84454cbc6374550de7746f591f8fe1b6165c1fce249652cc8291c4/psycopg_binary-3.3.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5927b7ba63153cd8e9862987290a2b783a5c590daf2a4ef981700cc3569166d4", size = 4782318, upload-time = "2026-09-18T13:20:35.401Z" },
{ url = "https://files.pythonhosted.org/packages/85/a9/793f0ac107a9003b48441d0d1f9f616d96e0f37458dd8dc12528ceff55fb/psycopg_binary-3.3.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:94596f9e7633ee3f6440711d43bb70aa31cc0a46a900ab8b4201a366ace5c9e7", size = 5486985, upload-time = "2026-05-01T23:30:55.517Z" }, { url = "https://files.pythonhosted.org/packages/a4/8a/0d21c2c833cdc0d4244c77e858e0ed37fa2abec2623be4fd686f617109ce/psycopg_binary-3.3.6-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:0bf08b749cc144f33b44a91b78e3f71c60eb07963746a0df5a100b36ce3d7475", size = 5567460, upload-time = "2026-09-18T13:20:41.902Z" },
{ url = "https://files.pythonhosted.org/packages/8f/26/42e8533497e2592334f68ec529cf5f840f7fa4e99575a4bb61aa184dbfbf/psycopg_binary-3.3.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8c0056529e68dbe9184cd4019a1f3d8f3a4ead2f6fc7a5afcf27d3314edd1277", size = 5168745, upload-time = "2026-05-01T23:31:01.904Z" }, { url = "https://files.pythonhosted.org/packages/49/6d/7692d0d4e656b6cc9868d8acc2e3b42f17a0db4a625400a6d093cb0533a1/psycopg_binary-3.3.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:31cd942c23f613276b81a6e6598cefa12960058b0f46e1e874b540c793f6aca5", size = 5246902, upload-time = "2026-09-18T13:20:47.661Z" },
{ url = "https://files.pythonhosted.org/packages/15/af/b7151776cc08d5935d45c833ec818a9beb417cf7c08239af1aafbdae78ee/psycopg_binary-3.3.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c09aad7051326e7603c14e50636db9c01f78272dc54b3accff03d46370461e6", size = 6761486, upload-time = "2026-05-01T23:31:14.511Z" }, { url = "https://files.pythonhosted.org/packages/d4/c1/b8a1f18fb1b7558a17f57f7cb3fc8bc93189feea2958925950b3acb15743/psycopg_binary-3.3.6-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4690cf67738f0e0e49a32aeec99bf0e4595cc2b4f1af984a4345394b1dcff91a", size = 6847192, upload-time = "2026-09-18T13:20:56.874Z" },
{ url = "https://files.pythonhosted.org/packages/d0/ed/c92533b9124712d592cbf1cd6c76da933a2e0acea81dfe1fbe7e735f0cff/psycopg_binary-3.3.4-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:514404ed543efd620c85602b747df2a23cf1241b4067199e1a66f2d2757aaa41", size = 4997427, upload-time = "2026-05-01T23:31:20.901Z" }, { url = "https://files.pythonhosted.org/packages/a5/76/404f33519167c65cca88ec4998776f1dbebccc301ee977f0e62c47fb0826/psycopg_binary-3.3.6-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ad1c785e784cfd87e8436c6b7702f2d321fc39601bbaf29bc63a41a867091638", size = 5079573, upload-time = "2026-09-18T13:21:04.155Z" },
{ url = "https://files.pythonhosted.org/packages/a2/23/ccadfd0de416aa188356daa199453af24087b042e296088706d190ae0295/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:46893c26858be12cc49ca4226ed6a60b4bfccadd946b3bebb783a60b38788228", size = 4533549, upload-time = "2026-05-01T23:31:26.204Z" }, { url = "https://files.pythonhosted.org/packages/f0/d9/79e8fbc8f37262a415f3550f0bcc5f98037442bf3d12ef6cbae2056655ae/psycopg_binary-3.3.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:79a2a1c3449f6c3409427078ed1cec10de79f3023cb5f2504f0597d350ad46c7", size = 4613633, upload-time = "2026-09-18T13:21:10.664Z" },
{ url = "https://files.pythonhosted.org/packages/fd/a0/c8f43cee36386f7bc891ab41a9d31ea07cf9826038e732da79f26b1e5f34/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:df1d567fc430f6df15c9fcf67d87685fc49bdb325adc0db5af1adfb2f44eb5c9", size = 4210256, upload-time = "2026-05-01T23:31:33.884Z" }, { url = "https://files.pythonhosted.org/packages/d4/47/96225db74be7d2ce04b3a58678b53cda610225055edf5faa775c9f501d8b/psycopg_binary-3.3.6-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:86147cb5d140341c3363fb5bacce31f8d5543902a46699d3c536b101bbceaf9e", size = 4293375, upload-time = "2026-09-18T13:21:16.027Z" },
{ url = "https://files.pythonhosted.org/packages/4e/2c/c1547871be3790676e8868b38655496422f94f0978dfb66b74bdba2f1676/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:6b9016b1714da4dd5ecaaa75b82098aa5a0b87854ce9b092e21c27c4ae23e014", size = 3946204, upload-time = "2026-05-01T23:31:39.626Z" }, { url = "https://files.pythonhosted.org/packages/2a/d2/18e9c779a5efd565250329adaf529ecc2b8b2ed5be5cb0f6ccee208cbfd9/psycopg_binary-3.3.6-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:7308c93cf0b19bbaf8e6ff0a6ad50d3c442385739245fe15a8d593bf841734a6", size = 4019883, upload-time = "2026-09-18T13:21:21.587Z" },
{ url = "https://files.pythonhosted.org/packages/c4/b1/f6670f00fa7ea601584623f6c11602ab92117d83eaff885e0210f6de7418/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:47c656a8a7ba6eb0cff1801a4caaa9c8bdc12d03080e273aff1c8ac39971a77e", size = 4255811, upload-time = "2026-05-01T23:31:44.986Z" }, { url = "https://files.pythonhosted.org/packages/ef/28/0cc654afc6c2cda982767f5679d3646b30b1ec86545bdaa9402202d6776c/psycopg_binary-3.3.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:05a83ac9fd52b9bca7cb5ab04b3691163170bd16f53defa27216ea3aa07ee781", size = 4332607, upload-time = "2026-09-18T13:21:27.63Z" },
{ url = "https://files.pythonhosted.org/packages/eb/e6/5fff07a70d1f945ed90ae131c3bd76cab32beff7c58c6db15ad5820b6d1f/psycopg_binary-3.3.4-cp314-cp314-win_amd64.whl", hash = "sha256:c37e024c07308cd06cf3ec51bfd0e7f6157585a4d84d1bce4a7f5f7913719bf8", size = 3666849, upload-time = "2026-05-01T23:31:51.165Z" }, { url = "https://files.pythonhosted.org/packages/f1/3e/0a753a74fbd7aef120f286c016e09d3cc3f1daf7688f4a145d27281260b2/psycopg_binary-3.3.6-cp314-cp314-win_amd64.whl", hash = "sha256:1fbd30e537dab22cafdf080608f10148fe2a5f3a61294ddb5113caac8a623840", size = 3755671, upload-time = "2026-09-18T13:21:33.855Z" },
{ url = "https://files.pythonhosted.org/packages/0e/b1/a372b9c02aea50148e71c9853e19efca8fa5ae2010a8e27243b9b8f790c0/psycopg_binary-3.3.6-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:bf8c8481d026b85dd70c5fa7dde85b2333aed0b32a2602bcd38a900cbd78a49c", size = 4719571, upload-time = "2026-09-18T13:21:41.437Z" },
{ url = "https://files.pythonhosted.org/packages/65/7c/811e3828c6b82e2f10c6c9cdd963cfc66f3e024026e5a69ac18530bad984/psycopg_binary-3.3.6-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:b599defe9190b17e9907c8b4d114c181e702c87efcd1b8a0ad40971cdcc4634a", size = 4781230, upload-time = "2026-09-18T13:21:49.516Z" },
{ url = "https://files.pythonhosted.org/packages/3e/15/9a784eed813ea9e97c294af3ead63d02b7b203502c66380336c50065e441/psycopg_binary-3.3.6-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:b8ece331509f7a975b90501f41e83ad905e4141753fedf3f2711b2bc70a8efbc", size = 5566111, upload-time = "2026-09-18T13:21:58.089Z" },
{ url = "https://files.pythonhosted.org/packages/68/16/47194e002007c27337b11e49bf459c4b19727463f9aff2e1a90917bcc806/psycopg_binary-3.3.6-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c61617eaae0112ca154da87ffb99b73af2c74067acac28dfb9a4455b019dff2e", size = 5249963, upload-time = "2026-09-18T13:22:06.695Z" },
{ url = "https://files.pythonhosted.org/packages/53/84/5dcf9f310b11f0675cd860c6b2c70f58ce61798a3ee3f6f962b53fa358ca/psycopg_binary-3.3.6-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c6d19cb4999d03231e8730a5f66c8f5068bc3b532677eb39dab0f600bff3e312", size = 6847925, upload-time = "2026-09-18T13:22:13.088Z" },
{ url = "https://files.pythonhosted.org/packages/f3/06/1957a06dc22963c418c27b284929579de84f29c37ad1abe6dc6ee9e8cf25/psycopg_binary-3.3.6-cp315-cp315-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8cbb54454dbf1bbf2ff08dd7693e8d94ac94b1a20f70f4b3b813d52ecb5cbc1", size = 5087720, upload-time = "2026-09-18T13:22:17.959Z" },
{ url = "https://files.pythonhosted.org/packages/21/43/ac07d042bae99b57bf123bb473632f29af544008094da0ffd285ab8011e2/psycopg_binary-3.3.6-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:dc75da5a20951049f7b773145f998f69d181adad9c58a0ff36e0cf1d73c10e10", size = 4613412, upload-time = "2026-09-18T13:22:26.719Z" },
{ url = "https://files.pythonhosted.org/packages/aa/b1/019156fbeafcefb4cccc9d109de4699493bceb8313c7545c8349e089dfbc/psycopg_binary-3.3.6-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:955e3dd94da361e052d2e49acf591017158dc8f8ed2c8a42c2e3943403c39dc2", size = 4292618, upload-time = "2026-09-18T13:22:33.042Z" },
{ url = "https://files.pythonhosted.org/packages/5d/0f/62113dc6b1df65983a1f2fc816c04b1edfa22f2ae9d4abee74ed267f4a96/psycopg_binary-3.3.6-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:c7753871eb57e6a5f4646f6168590c6653073dea5e9e720b201c8875332df4c8", size = 4027121, upload-time = "2026-09-18T13:22:38.334Z" },
{ url = "https://files.pythonhosted.org/packages/5d/d5/cf0cbd1ea5a7d8167fe2c6953efde19101f7b193bd61a23e6d622ad6854c/psycopg_binary-3.3.6-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:303732e798fe6729f8e12021b9c96107df8e95ecec4dd487c67b98ec2a59435e", size = 4336388, upload-time = "2026-09-18T13:22:45.576Z" },
{ url = "https://files.pythonhosted.org/packages/98/33/e2a5b36edf8aa422f6fa4b894756eb33dc93b36df5f65121280bb8b929c4/psycopg_binary-3.3.6-cp315-cp315-win_amd64.whl", hash = "sha256:2f122603f36050937982abf9668d8bc4769a79f7c93a65013b1c49f1cab7b56b", size = 3756154, upload-time = "2026-09-18T13:22:51.283Z" },
] ]
[[package]] [[package]]
@@ -881,11 +985,11 @@ wheels = [
[[package]] [[package]]
name = "pwdlib" name = "pwdlib"
version = "0.3.0" version = "0.3.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5f/41/a7c0d8a003c36ce3828ae3ed0391fe6a15aad65f082dbd6bec817ea95c0b/pwdlib-0.3.0.tar.gz", hash = "sha256:6ca30f9642a1467d4f5d0a4d18619de1c77f17dfccb42dd200b144127d3c83fc", size = 215810, upload-time = "2025-10-25T12:44:24.395Z" } sdist = { url = "https://files.pythonhosted.org/packages/78/a6/644f28a21cb68920d06b264e72b864ac9bff2377f4757dd4648b5110b3ad/pwdlib-0.3.1.tar.gz", hash = "sha256:2ffd1955c92190d5bfbdc5071bf7d422568b83f943453ebd3d43d031ccba37b1", size = 216951, upload-time = "2026-08-12T08:38:38.383Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/62/0c/9086a357d02a050fbb3270bf5043ac284dbfb845670e16c9389a41defc9e/pwdlib-0.3.0-py3-none-any.whl", hash = "sha256:f86c15c138858c09f3bba0a10984d4f9178158c55deaa72eac0210849b1a140d", size = 8633, upload-time = "2025-10-25T12:44:23.406Z" }, { url = "https://files.pythonhosted.org/packages/6b/2e/51979293b2337f508597d13ee20db1ac1d43e48086b2ed7de6da213ed46b/pwdlib-0.3.1-py3-none-any.whl", hash = "sha256:944fa439198bed8ac3ea63dac76ea4ae43e2a35f4f21e9e725e82d3c2b97d6a5", size = 9272, upload-time = "2026-08-12T08:38:37.483Z" },
] ]
[package.optional-dependencies] [package.optional-dependencies]
@@ -907,7 +1011,7 @@ wheels = [
[[package]] [[package]]
name = "pydantic" name = "pydantic"
version = "2.13.4" version = "2.13.5"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "annotated-types" }, { name = "annotated-types" },
@@ -915,9 +1019,9 @@ dependencies = [
{ name = "typing-extensions" }, { name = "typing-extensions" },
{ name = "typing-inspection" }, { name = "typing-inspection" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } sdist = { url = "https://files.pythonhosted.org/packages/53/ef/fc4f868f4e2cee79f863883abffceff107875f569b848507319842d2a681/pydantic-2.13.5.tar.gz", hash = "sha256:51a9c5f7b2f8e636f04c6cada605d9b6a3bf1348fdf945a3d8869b19bba0ee08", size = 845750, upload-time = "2026-08-28T14:04:00.916Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, { url = "https://files.pythonhosted.org/packages/eb/47/c95ffc2009878c7aac0c5e08528022dcb885933252a88b5f170058014464/pydantic-2.13.5-py3-none-any.whl", hash = "sha256:346a034f080da3755d8e9cb5e00e8b07de1d39e4f6e2c87d8ab7cafa0b269a73", size = 472589, upload-time = "2026-08-28T14:03:59.136Z" },
] ]
[package.optional-dependencies] [package.optional-dependencies]
@@ -927,43 +1031,43 @@ email = [
[[package]] [[package]]
name = "pydantic-core" name = "pydantic-core"
version = "2.46.4" version = "2.46.5"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "typing-extensions" }, { name = "typing-extensions" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } sdist = { url = "https://files.pythonhosted.org/packages/af/f9/8a06bea35ef8daf588f707784c973a7046e0034c8d8cfb08828eeffb8b75/pydantic_core-2.46.5.tar.gz", hash = "sha256:10416c15b8839ecc4ef4d0885da76da6fd0f67333a0eb8aff6d93c4b8f2910fc", size = 472262, upload-time = "2026-08-28T10:01:31.677Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, { url = "https://files.pythonhosted.org/packages/8e/8a/14596f2a8367da50cf7cbac48169ee5d9c8e11d486a3b527082384630c72/pydantic_core-2.46.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c1c43ad4339643d70ebb8124e1305a7dab423001eff58bb41a0f731adbc98355", size = 2074081, upload-time = "2026-08-28T09:59:16.141Z" },
{ url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, { url = "https://files.pythonhosted.org/packages/ae/d5/d8a4eb6d6c7f66b91dd37c576d76e9e60fba900caf5372c17bcf949febc2/pydantic_core-2.46.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1a353f84de772f423b5ffb11d7ae352fbbef0f446f3c0b0af0f8236d7233606e", size = 1920497, upload-time = "2026-08-28T09:59:18.065Z" },
{ url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, { url = "https://files.pythonhosted.org/packages/8e/26/092079428f86e927e030b2c0ced87df69dbb1c875cdeaa67bf42ea2be746/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5086029a57366b8cf81b130a43908738095c270c21a8d7f0e8bdfdb89718e2f3", size = 1952130, upload-time = "2026-08-28T09:59:20.476Z" },
{ url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, { url = "https://files.pythonhosted.org/packages/08/c3/8ec0e290a9ebaebd64047bf5fda94be835c6b1551b02437e4b76778fbcd7/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46c25dda9d092a06c08db76ffe0a197107904d0dfac653f7d5306bbcd6d6119c", size = 2026371, upload-time = "2026-08-28T09:59:22.227Z" },
{ url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, { url = "https://files.pythonhosted.org/packages/01/72/4fd20ad520fb8da0157f95b27a7eb05a72790ef08138e7701ac972c342ea/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:37ea7b83c935e5b0d68c9449b82651accf78a10828b2c02b2f2d9e9496446c21", size = 2202822, upload-time = "2026-08-28T09:59:24.277Z" },
{ url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, { url = "https://files.pythonhosted.org/packages/31/b0/d16e0771206b29314f0d52198b720be21e8a99ab2bf11e3bc0d7c9cebdff/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e64e88d5585bea9ce95861079de72006c7fa6d3df4e3a3b65ba31eb979c15c9f", size = 2262756, upload-time = "2026-08-28T09:59:26.608Z" },
{ url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, { url = "https://files.pythonhosted.org/packages/2c/9b/59634b7ac631c63b2a37760eb6943af3e29573d6b59a4abc5e7f019d4cee/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d510bac3ee52247af28ed4bb18a1e799f040ac60fd2bf5ccd4c92f1fbe786f", size = 2068352, upload-time = "2026-08-28T09:59:29.044Z" },
{ url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, { url = "https://files.pythonhosted.org/packages/08/7c/570abb1ad2155348dc754ea91be22e5aaa18eb6d69a6068f7c6f2679a6ed/pydantic_core-2.46.5-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:a2a5e1d0ff29adddc9f6d6821a66302e4493f8ca898b715b6b1182c2c201ea0a", size = 2104777, upload-time = "2026-08-28T09:59:30.95Z" },
{ url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, { url = "https://files.pythonhosted.org/packages/8e/25/5bf74adc65a1ac5b7be3f6cb0bcb5433615c1598a801c19d830d84c98ded/pydantic_core-2.46.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:03b9666e41e35d8909852ba191a0607520f81b74eaf12ccf8737005dbb313821", size = 2156312, upload-time = "2026-08-28T09:59:32.604Z" },
{ url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, { url = "https://files.pythonhosted.org/packages/90/6a/2ef38830675e050121040618135564ed56b860b45433b02d9b4ebece46f3/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:a91c17edf6eea2402cb5457b4c89e99bc5ed1004aa34c4adf1d4258c1a5c22c2", size = 2150067, upload-time = "2026-08-28T09:59:34.453Z" },
{ url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, { url = "https://files.pythonhosted.org/packages/90/ef/a7dbb03a14a64c2a4621f989c615ed9a892535a6cad938fc27079f919d80/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b49924c73a235e969511bf2aabdff3beebf9820931f646c80274d5d780010c47", size = 2304516, upload-time = "2026-08-28T09:59:36.194Z" },
{ url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, { url = "https://files.pythonhosted.org/packages/68/f8/6bb4c4b80e8a6fde1904c64a51c62a1d04fcdfa3ea521a66b2ddefa1d885/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:2cbd9a5eff05e51c447c34dfa4632145b26b09120cf04bd0c871e44c1a5e1c9a", size = 2335223, upload-time = "2026-08-28T09:59:37.931Z" },
{ url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, { url = "https://files.pythonhosted.org/packages/2a/80/f46b8c681195190b2c1f1c7c0a81abce60663e987613e09ef64d433dd96b/pydantic_core-2.46.5-cp314-cp314-win32.whl", hash = "sha256:2d5d76654becf5efd62c9e51c3756c67b49498b0c9a40884934c40807adbd074", size = 1934827, upload-time = "2026-08-28T09:59:39.836Z" },
{ url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, { url = "https://files.pythonhosted.org/packages/f7/3c/60674207246bc0a4009d2391b7c7251c7159f279c8d2ab8aae8ef46f3dee/pydantic_core-2.46.5-cp314-cp314-win_amd64.whl", hash = "sha256:fa10ef4112775900e7a0661068635eb67b2ab824fbde764de6e0e21982a93db0", size = 2042648, upload-time = "2026-08-28T09:59:41.792Z" },
{ url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, { url = "https://files.pythonhosted.org/packages/69/0c/117c562c7c1babdf44576b72a5e496906506c93690387ecfbca7c729ae2e/pydantic_core-2.46.5-cp314-cp314-win_arm64.whl", hash = "sha256:045ab3b6d308439e32b81cc173bba5b9018bc6ed896afd0c65b3b009b1699af5", size = 1989652, upload-time = "2026-08-28T09:59:43.702Z" },
{ url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, { url = "https://files.pythonhosted.org/packages/e8/66/9336ae58f9eb68c41d121894e52c4c89eccb07eb8f602a04ee9c3f37736a/pydantic_core-2.46.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8816f3d218beb4b787de5c9759c259b8fa61f9dec42dc7811f320a33771778b7", size = 2065829, upload-time = "2026-08-28T09:59:45.364Z" },
{ url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, { url = "https://files.pythonhosted.org/packages/c5/02/bc19b47a96c2d3109760711acf22369e56bd7e405ca52f7ade164d2ead57/pydantic_core-2.46.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bce57638e08ac148e5778cce7feb968307a727d66f8e2274a543d0cf0c9ad6a3", size = 1905716, upload-time = "2026-08-28T09:59:47.18Z" },
{ url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, { url = "https://files.pythonhosted.org/packages/52/a4/70b47c0509923dd98ccfed04fb3e32ea3849c82a0ff2205bb41009b43c00/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:976e1128455aa595ea04c79ccfedff1aaeab96ee013fcc916bed120c4f0ad94f", size = 1934216, upload-time = "2026-08-28T09:59:49.241Z" },
{ url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, { url = "https://files.pythonhosted.org/packages/52/ab/aa03b65f7bb198585edf806b906c3223ecf1795543e39e23aec4cce27ad2/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b891faeedeafba41b2983e5001a81b6a915b69544c7e7570d1989ce1c36ac7", size = 2010635, upload-time = "2026-08-28T09:59:51.692Z" },
{ url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, { url = "https://files.pythonhosted.org/packages/3c/8b/0da06343f30b84ec549aafd309c6456223d5dc8bd36af504c573faad561d/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f194189415698233dd1114a093a9b56e61e2c57e11b469be3b0506f46f0771c", size = 2209369, upload-time = "2026-08-28T09:59:53.582Z" },
{ url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, { url = "https://files.pythonhosted.org/packages/d6/5b/844c4defaa34a3df66eb9257087d121d70c201298b96abdf9f492fc2f1bf/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82a36973cf8a2ef5406f4fe2edbf8ed0c99629535d959e0b100c76a32535a111", size = 2253238, upload-time = "2026-08-28T09:59:55.484Z" },
{ url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, { url = "https://files.pythonhosted.org/packages/f4/64/a4e536cb16d7f61a7fd3120b46c577fc7fa7325992f69c4f52bc786d77d8/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdbb78909f52b981d3b2d56b97328d71eb0b974c36bd77c920123a7ebb192829", size = 2065740, upload-time = "2026-08-28T09:59:58.038Z" },
{ url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, { url = "https://files.pythonhosted.org/packages/5f/75/aaa38c6bc2d085f6605b34eabdc6a8a4e0b2e61fc9c8e6e52b28e97b3125/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:52e24eacdb536cade636aa90fb851835222becff8484b7001fdc78cb0290f2aa", size = 2087425, upload-time = "2026-08-28T09:59:59.898Z" },
{ url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, { url = "https://files.pythonhosted.org/packages/55/ae/fcab4cfc39aba3689e1d20c8b5250ad280957022c09af2ed9cd585602a5e/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:37ae34309d7bd8c0d61ab839668058f2a7962ea1fc51d105d2db228fe0618034", size = 2139306, upload-time = "2026-08-28T10:00:03.057Z" },
{ url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, { url = "https://files.pythonhosted.org/packages/2d/f4/f1d03a4bc9d9acbc62f4d742b8a319af52f71885079868b2ff8e48a651ee/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:0cdbada856a1c69a7624a64d3d9aefe79300bd6ef827b43a4f265010b9b55184", size = 2144589, upload-time = "2026-08-28T10:00:05.645Z" },
{ url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, { url = "https://files.pythonhosted.org/packages/83/f3/7a53bb1356de514a4cd295f25b6ac39237895620c0462d2592b76c16e114/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:545f26c504b27c3758439a5e6d9349931f0a04f855668d5fe323c89e82300a38", size = 2288882, upload-time = "2026-08-28T10:00:07.931Z" },
{ url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, { url = "https://files.pythonhosted.org/packages/cd/94/5a81583660c175c59d49ffb09f4b3a44debeaf86a19fca664ae1cdd9ee32/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:ff218293c9c806138dca139765e3b067621be52bcd93cdc14c7711be7ddc90a9", size = 2335210, upload-time = "2026-08-28T10:00:10.177Z" },
{ url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, { url = "https://files.pythonhosted.org/packages/5a/9f/5d685c2693b972d1a59c998586e8823712b66603aeff47ee60a4bdaafd37/pydantic_core-2.46.5-cp314-cp314t-win32.whl", hash = "sha256:97cf3eb53a8cccacf9d46686a0926186c9bfb5574f2ed66d3639d5fe117cd3a9", size = 1921180, upload-time = "2026-08-28T10:00:12.35Z" },
{ url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, { url = "https://files.pythonhosted.org/packages/70/12/5c94ee16d65a37a15f9e869f5e6256df111154491173801a4c5e800ab548/pydantic_core-2.46.5-cp314-cp314t-win_amd64.whl", hash = "sha256:d2f9fc07a8042a8f95925b35c4f04f469707c981fc33245b6ca187cf5d2dd290", size = 2020515, upload-time = "2026-08-28T10:00:14.774Z" },
{ url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, { url = "https://files.pythonhosted.org/packages/63/19/67830dda664e6bdf9285ee2e40f355d0d7d6b92aa0c42e8d217bb8d33d36/pydantic_core-2.46.5-cp314-cp314t-win_arm64.whl", hash = "sha256:acf8a67ba51f4ca9ddbd0e6b3000a65ac51ab734661778b3e7ba64d99a710f2f", size = 1989276, upload-time = "2026-08-28T10:00:16.984Z" },
] ]
[[package]] [[package]]
@@ -981,16 +1085,16 @@ wheels = [
[[package]] [[package]]
name = "pydantic-settings" name = "pydantic-settings"
version = "2.14.2" version = "2.15.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "pydantic" }, { name = "pydantic" },
{ name = "python-dotenv" }, { name = "python-dotenv" },
{ name = "typing-inspection" }, { name = "typing-inspection" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/5c/b5/8f48e906c3e0205276e8bd8cb7512217a87b2685304d64be27cad5b3019f/pydantic_settings-2.14.2.tar.gz", hash = "sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f", size = 237700, upload-time = "2026-06-19T13:44:56.324Z" } sdist = { url = "https://files.pythonhosted.org/packages/68/ca/31c57507b13119d7d3cfa1576dad2911a4861e3be07b579395f4e9d393f9/pydantic_settings-2.15.0.tar.gz", hash = "sha256:694b793e84f766ba76a90ebdefc01d0a9a045dab0382bee70393da93712ad117", size = 261253, upload-time = "2026-08-07T09:24:57.419Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/77/c1/6e422f34e569cf8e18df68d1939c81c099d2b61e4f7d9621c8a77560799c/pydantic_settings-2.14.2-py3-none-any.whl", hash = "sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440", size = 61715, upload-time = "2026-06-19T13:44:55.02Z" }, { url = "https://files.pythonhosted.org/packages/30/a4/2bffa9f8e804325a09867f0e9d30795c80ea9f8d62560bd1b6ad6220eb2f/pydantic_settings-2.15.0-py3-none-any.whl", hash = "sha256:0ba092c291c94baceb5eff768aa0d56400a457585bc0175925a5a5510303da42", size = 69413, upload-time = "2026-08-07T09:24:55.839Z" },
] ]
[[package]] [[package]]
@@ -1004,11 +1108,11 @@ wheels = [
[[package]] [[package]]
name = "pyjwt" name = "pyjwt"
version = "2.13.0" version = "2.15.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/3b/81/58d0ac84e1ef3a3843791d6954d94c0b33d526c75eeb1efbce9d0a4c4077/pyjwt-2.13.0.tar.gz", hash = "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423", size = 107515, upload-time = "2026-05-21T19:54:36.618Z" } sdist = { url = "https://files.pythonhosted.org/packages/02/a5/5197bfd06417837ac079921c66fa6393f1dea3557272a263cebfef69e432/pyjwt-2.15.0.tar.gz", hash = "sha256:b11c5f9791d7bf51c2b39a81ed669f6b2dbbd669df2942f6c60167e9e3d1abe4", size = 120513, upload-time = "2026-09-23T16:56:00.689Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728", size = 31274, upload-time = "2026-05-21T19:54:35.362Z" }, { url = "https://files.pythonhosted.org/packages/e8/55/40e45bf052ee8ee12a4dfd785519660f8effa7b065442b91646ec6828619/pyjwt-2.15.0-py3-none-any.whl", hash = "sha256:7a3742debf6b879e912dbb9819ceec1594be812452b78c5f2e2dfc56564954f8", size = 33680, upload-time = "2026-09-23T16:55:59.241Z" },
] ]
[[package]] [[package]]
@@ -1150,40 +1254,40 @@ wheels = [
[[package]] [[package]]
name = "ruff" name = "ruff"
version = "0.15.20" version = "0.16.9"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/43/dc/35b341fc554ba02f217fc10da57d1a75168cfbcf75b0ef2202176d4c4f2d/ruff-0.15.20.tar.gz", hash = "sha256:1416eb04349192646b54de98f146c4f59afe37d0decfc02c3cbbf396f3a28566", size = 4755489, upload-time = "2026-06-25T17:20:37.578Z" } sdist = { url = "https://files.pythonhosted.org/packages/96/bf/c935ca98e73fe8ce65b87ef08a280c0c1e85295d569228c15e87d8fdfaf1/ruff-0.16.9.tar.gz", hash = "sha256:12b625c6cfba78d285d9f48eda5f053374f1e53cb10ef17342a383750db99161", size = 4948764, upload-time = "2026-09-24T20:37:49.416Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/94/d9/2d5014f0253ba541d2061d9fa7193f48e941c8b21bb88a7ff9bbe0bd0596/ruff-0.15.20-py3-none-linux_armv6l.whl", hash = "sha256:00e188c53e499c3c1637f73c91dcf2fb56d576cab76ce1be50a27c4e80e37078", size = 10839665, upload-time = "2026-06-25T17:19:44.702Z" }, { url = "https://files.pythonhosted.org/packages/0d/26/df51322b52ee1ada7eff2d071ea09d11d5c2d1dcc9f02594f5785c4d1635/ruff-0.16.9-py3-none-linux_armv6l.whl", hash = "sha256:95e6f022090368ab3b824c36276839c53b2adf1a3f4c09fefc33dfc400f6da96", size = 10082922, upload-time = "2026-09-24T20:37:13.045Z" },
{ url = "https://files.pythonhosted.org/packages/c6/d3/ac1798ba64f670698867fcfc591d50e7e421bef137db564858f619a30fcf/ruff-0.15.20-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9ebd1fd9b9c95fc0bd7b2761aebec1f030013d2e193a2901b224af68fe47251b", size = 11208649, upload-time = "2026-06-25T17:19:48.787Z" }, { url = "https://files.pythonhosted.org/packages/a5/27/7bf51f5a7aa375e9f339280a303aab44ca75dc1525f1cdc5991761685b0f/ruff-0.16.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a5f27be168556594a86d2f415db0cf43f5291917849318f873c7e2791f7a8c67", size = 10236360, upload-time = "2026-09-24T20:37:16.049Z" },
{ url = "https://files.pythonhosted.org/packages/47/47/d3ac899991202095dfcf3d5176be4272642be3cf981a2f1a30f72a2afb95/ruff-0.15.20-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c5b16cdd67ca108185cd36dce98c576350c03b1660a751de725fb049193a0632", size = 10622638, upload-time = "2026-06-25T17:19:51.354Z" }, { url = "https://files.pythonhosted.org/packages/b6/63/09659283f92f02dff45809da194a70da2f688d87c55d8875c4fae3536072/ruff-0.16.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1632eb1d6197f33bd00b1acbc5b71009e89a8895c158e2d2b03a834fac964ab6", size = 9892940, upload-time = "2026-09-24T20:37:17.957Z" },
{ url = "https://files.pythonhosted.org/packages/33/13/4e043fe30aa94d4ff5213a9881fc296d12960f5971b234a5263fdc225312/ruff-0.15.20-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3413bb3c3d2ca6a8208f1f4809cd2dca3c6de6d0b491c0e70847672bde6e6efd", size = 10984227, upload-time = "2026-06-25T17:19:54.044Z" }, { url = "https://files.pythonhosted.org/packages/24/58/98de1b72ec172f5f8f1731236fe21585b3998bf7dfe9fcc44ae9ba626012/ruff-0.16.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b3f951b14d865d5952c89d40a5ca07e87abe24fa5453299878411e127748fb1c", size = 10032114, upload-time = "2026-09-24T20:37:19.942Z" },
{ url = "https://files.pythonhosted.org/packages/76/e6/92e7bf40388bc5800073b96564f56264f7e48bfd1a498f5ced6ae6d5a769/ruff-0.15.20-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd7ec42b3bb3da066488db093308a69c4ac5ee6d2af333a86ba6e2eb2e7dd44b", size = 10622882, upload-time = "2026-06-25T17:19:57.037Z" }, { url = "https://files.pythonhosted.org/packages/c8/7d/f1e17c54ab59d4bad1dce8ee3e22a7a1d0ef4745240decacdcf3832b5bb2/ruff-0.16.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:447fc07e1573afff7cb02803462b12b6c8ece7cf10e2cd78565fa6d7a1c0bf8d", size = 9910227, upload-time = "2026-09-24T20:37:21.872Z" },
{ url = "https://files.pythonhosted.org/packages/13/7a/43460be3f24495a3aa46d4b16873e2c4941b3b5f0b00cf88c03b7b94b339/ruff-0.15.20-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1a36ad0eb77fba9aabfb69ede54de6f376d04ac18ebea022847046d340a8267", size = 11474808, upload-time = "2026-06-25T17:20:00.357Z" }, { url = "https://files.pythonhosted.org/packages/34/19/436f647a65075bbd3bab2668b3bdaa5120559b294694018cdcefabbbf30b/ruff-0.16.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a3e039a6a40ed976c491722b60e0ae4a4aa1a86057f540ee7a37a5d19ae9120", size = 10547484, upload-time = "2026-09-24T20:37:24.229Z" },
{ url = "https://files.pythonhosted.org/packages/27/a0/f37077884873221c6b33b4ab49eb18f9f88e54a16a25a5bca59bef46dd66/ruff-0.15.20-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b6df3b1e4610432f0386dba04d853b5f08cbbc903410c6fcc02f620f05aff53c", size = 12293094, upload-time = "2026-06-25T17:20:03.446Z" }, { url = "https://files.pythonhosted.org/packages/03/59/38430a6bc2f6d8095447ac39625cf8b6e9344a47e6c26225c8ba1bff3ffb/ruff-0.16.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4684dded7db60aa57cb118fa158630f5feade4af5782903b6053484bdf9bd129", size = 11412367, upload-time = "2026-09-24T20:37:26.307Z" },
{ url = "https://files.pythonhosted.org/packages/a6/74/165545b60256a9704c21ac0ec4a0d07933b320812f9584836c9f4aca4292/ruff-0.15.20-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e89f198a1ea6ef0d727c1cf16088bc91a6cb0ab947dedc966715691647186eae", size = 11526176, upload-time = "2026-06-25T17:20:06.301Z" }, { url = "https://files.pythonhosted.org/packages/c8/bd/bbb6d7fc7f208c8b8c50dd5dc8206e4cfdb1e7a8fb852606a3adf370c880/ruff-0.16.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d29c934357e45642fda2f34c0b1f4025b4a6c01e15e4bf0016879d60078a142c", size = 10869787, upload-time = "2026-09-24T20:37:28.35Z" },
{ url = "https://files.pythonhosted.org/packages/86/b1/a976a136d40ade83ce743578399865f57001003a409acadc0ecbb3051082/ruff-0.15.20-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309809086c2acb67624950a3c8133e80f32d0d3e27106c0cd60ff26657c9f24b", size = 11520767, upload-time = "2026-06-25T17:20:09.191Z" }, { url = "https://files.pythonhosted.org/packages/bc/b8/9c543074918061abbefc3bd139bee22de35abedb00dde0f2d27288838962/ruff-0.16.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a21713e629d3e5bdb2f5c2def1cc7f04f47fa8e1a7eb0571b4a28e1da64bc728", size = 10406494, upload-time = "2026-09-24T20:37:30.624Z" },
{ url = "https://files.pythonhosted.org/packages/19/0f/f032696cb01c9b54c0263fa393474d7758f1cdc021a01b04e3cbc2500999/ruff-0.15.20-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2d2374caa2f2c2f9e2b7da0a50802cfb8b79f55a9b5e49379f564544fbf56487", size = 11500132, upload-time = "2026-06-25T17:20:13.602Z" }, { url = "https://files.pythonhosted.org/packages/35/7a/5a8851bd146e7ccf8fd4b003f6c75c11f8fbdb0e60673b45097986b6bf41/ruff-0.16.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:7baa24ef5fc8e77aa93879e1d3f43754a01ae488e869f1ae30cf431afd4d2452", size = 10590083, upload-time = "2026-09-24T20:37:32.439Z" },
{ url = "https://files.pythonhosted.org/packages/4b/f4/51b1a14bc69e8c224b15dab9cce8e99b425e0455d462caa2b3c9be2b6a8e/ruff-0.15.20-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a1ed17b65293e0c2f22fc387bc13198a5de94bf4429589b0ff6946b0feaf21a3", size = 10943828, upload-time = "2026-06-25T17:20:16.635Z" }, { url = "https://files.pythonhosted.org/packages/87/f0/4c3467188f23f806960b46fa76575a7cd0514c9ba90562650b71efc96980/ruff-0.16.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a41aac6230aadfaa133bdfa1614488531ffa3e0837567ae04c0da2058a9c0f9e", size = 10119151, upload-time = "2026-09-24T20:37:34.581Z" },
{ url = "https://files.pythonhosted.org/packages/71/4b/fe267640783cd02bf6c5cc290b1df1051be2ec294c678b5c15fe19e52343/ruff-0.15.20-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f701305e66b38ea6c91882490eb73459796808e4c6362a1b765255e0cdcd4053", size = 10645418, upload-time = "2026-06-25T17:20:19.4Z" }, { url = "https://files.pythonhosted.org/packages/15/34/5a4def5adea572ce6aea0bb64f21f928ee317b80e0db747d7979b01d7261/ruff-0.16.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:c2529fb5896d49115b0e9aa8f887490b34bbe76baf879ec2264ac59406869ce7", size = 9911544, upload-time = "2026-09-24T20:37:36.796Z" },
{ url = "https://files.pythonhosted.org/packages/b0/c0/a65aa4ec2f5e87a1df32dc3ec1fede434fe3dfd5cbcf3b503cafc676ab54/ruff-0.15.20-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5b9c0c367ad8e5d0d5b5b8537864c469a0a0e55417aadfbeca41fa61333be9f4", size = 11211770, upload-time = "2026-06-25T17:20:22.033Z" }, { url = "https://files.pythonhosted.org/packages/62/5d/d15ebea7499eef9373318c0ee6ca127832927c6529731f6d48e18dce7ca9/ruff-0.16.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:41e3870277694177429b56406d65dfbdb2c2802c52b715edaf6a0b829c69d4ee", size = 10269884, upload-time = "2026-09-24T20:37:38.857Z" },
{ url = "https://files.pythonhosted.org/packages/5a/a4/0caa331d954ae2723d729d351c989cb4ca8b6077d5c6c2cb6de75e98c041/ruff-0.15.20-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:01cc00dd58f0df339d0e902219dd53990ea99996a0344e5d9cc8d45d5307e460", size = 11618698, upload-time = "2026-06-25T17:20:25.259Z" }, { url = "https://files.pythonhosted.org/packages/d1/56/c5d3cd119ded7a3c7aba0e961b69cb3df701c91662c98ad694467d060ce1/ruff-0.16.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8adbe4e58af167f767d7b2ba5e83c42e878350796cf78c2f5e14ab9903a92588", size = 10749366, upload-time = "2026-09-24T20:37:41.042Z" },
{ url = "https://files.pythonhosted.org/packages/10/9b/5f14927848d2fd4aa891fd88d883788c5a7baba561c7874732364045708c/ruff-0.15.20-py3-none-win32.whl", hash = "sha256:ed65ef510e43a137207e0f01cfcf998aeddb1aeeda5c9d35023e910284d7cf21", size = 10857322, upload-time = "2026-06-25T17:20:28.612Z" }, { url = "https://files.pythonhosted.org/packages/ac/fe/734ec7527029ac757ecf821f143c9f3fcf69149c21f53a044a899b430f5a/ruff-0.16.9-py3-none-win32.whl", hash = "sha256:0e1dbc2073624dee6618d41d0098690a7244654af746704b64759e12b6b6b385", size = 10152355, upload-time = "2026-09-24T20:37:43.025Z" },
{ url = "https://files.pythonhosted.org/packages/fa/f0/fe47c501f9dea92a26d788ff98bb5d92ed4cb4c88792c5c88af6b697dc8e/ruff-0.15.20-py3-none-win_amd64.whl", hash = "sha256:a525c81c70fb0380344dd1d8745d8cc1c890b7fc94a58d5a07bd8eb9557b8415", size = 11993274, upload-time = "2026-06-25T17:20:31.871Z" }, { url = "https://files.pythonhosted.org/packages/14/21/26e4643629b3ebb44f0a06f9c9a53058d63d989415f63a9a3c28e2ee7f22/ruff-0.16.9-py3-none-win_amd64.whl", hash = "sha256:6bd40fec8cd4c8a3d4dd589bd8ad4e6320c13c29234159bfd959a40d529d597b", size = 10592965, upload-time = "2026-09-24T20:37:44.944Z" },
{ url = "https://files.pythonhosted.org/packages/d7/2b/9555445e1201d92b3195f45cdb153a0b68f24e0a4273f6e3d5ab46e212bb/ruff-0.15.20-py3-none-win_arm64.whl", hash = "sha256:2f5b2a6d614e8700388806a14996c40fab2c47b819ef57d790a34878858ed9ca", size = 11343498, upload-time = "2026-06-25T17:20:35.03Z" }, { url = "https://files.pythonhosted.org/packages/51/60/5fb1a39dbb5ae314d5f59bc7348a63c1d5c20f3cd83914c4b5cb0be31d2d/ruff-0.16.9-py3-none-win_arm64.whl", hash = "sha256:ed1a252039200f57a59eebc063b54beabea67bfbaaca0eeaa7f54b5fbcda2284", size = 10458649, upload-time = "2026-09-24T20:37:46.882Z" },
] ]
[[package]] [[package]]
name = "sentry-sdk" name = "sentry-sdk"
version = "2.66.1" version = "2.70.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "certifi" }, { name = "certifi" },
{ name = "urllib3" }, { name = "urllib3" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/7f/6f/d59cad0889d15fde85254cf58e701484de3f3f0406003b3197746910b19b/sentry_sdk-2.66.1.tar.gz", hash = "sha256:f882fb08710c5f8bfc603aafa3e901b384009a19cc3f76a572b863392ee81cdc", size = 940543, upload-time = "2026-07-22T12:26:54.553Z" } sdist = { url = "https://files.pythonhosted.org/packages/52/0a/e37553f3106d0f7f0d4b8ad2c7b92410d131929786ffd116833d3b8164df/sentry_sdk-2.70.0.tar.gz", hash = "sha256:27e4b512f47be93136645dbe8f02d430473ee73a389ce430b2b7104baa66cf75", size = 1047234, upload-time = "2026-09-22T09:40:11.721Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/89/d3/726bd88f0eece09ddf431bea4c9191c18e7a8d070b854eb0014d447712ee/sentry_sdk-2.66.1-py3-none-any.whl", hash = "sha256:86002793161d9a95ef04bdd8d442e9bfece5d989b755f05d6360215094a7aff6", size = 505555, upload-time = "2026-07-22T12:26:52.71Z" }, { url = "https://files.pythonhosted.org/packages/eb/ff/b1e56f009bfb9b8023910b533d5ebf4ec5650da49891f2b8c7f3d4815401/sentry_sdk-2.70.0-py3-none-any.whl", hash = "sha256:89600e8bb5fc5239cad5c4434217f453204f2207ad29399ea58947460b37a62e", size = 531752, upload-time = "2026-09-22T09:40:10.063Z" },
] ]
[package.optional-dependencies] [package.optional-dependencies]
@@ -1251,16 +1355,16 @@ wheels = [
[[package]] [[package]]
name = "sqlmodel" name = "sqlmodel"
version = "0.0.39" version = "0.0.47"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "pydantic" }, { name = "pydantic" },
{ name = "sqlalchemy" }, { name = "sqlalchemy" },
{ name = "typing-extensions" }, { name = "typing-extensions" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/68/ee/22a0559283c3cf6048678e787ed5d4959dcd00dedd8ba4567eeae684eeb1/sqlmodel-0.0.39.tar.gz", hash = "sha256:23d8e50a8d8ee936032ed79c55023a5d618dd6bc3c510bbf4909d1a7a605a570", size = 91057, upload-time = "2026-06-25T13:01:38.475Z" } sdist = { url = "https://files.pythonhosted.org/packages/4f/60/aa4cb68f03fa6bd1ca9e9a9ca89c3bc6c67f50981d0abcd9d6364609516d/sqlmodel-0.0.47.tar.gz", hash = "sha256:3606430ea6db7f00c8ae2157458b9766644ea3ad365c601e26a86cf23f5910d5", size = 98136, upload-time = "2026-09-23T17:31:07.921Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/cf/7d/b9813a582d4eb310be35e1fc7dfaae71207d7b62e9e53be314ebd251b53b/sqlmodel-0.0.39-py3-none-any.whl", hash = "sha256:90ebe92ce5cc11d7fff8dc7cb594790a102333c8fe7c14865254f6fc5c939795", size = 29680, upload-time = "2026-06-25T13:01:37.494Z" }, { url = "https://files.pythonhosted.org/packages/f7/b3/988928029b0e68de7ac587c2e1ccd379aa18e7c0aa7cc426628145a032fe/sqlmodel-0.0.47-py3-none-any.whl", hash = "sha256:165294386c3e4b25fa02f151ca1abb0cbf5fdb76f6093a38aca33fdc30dfd485", size = 31314, upload-time = "2026-09-23T17:31:06.8Z" },
] ]
[[package]] [[package]]
@@ -1275,43 +1379,34 @@ 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" }, { 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 = "tenacity"
version = "9.1.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/47/c6/ee486fd809e357697ee8a44d3d69222b344920433d3b6666ccd9b374630c/tenacity-9.1.4.tar.gz", hash = "sha256:adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a", size = 49413, upload-time = "2026-02-07T10:45:33.841Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d7/c1/eb8f9debc45d3b7918a32ab756658a0904732f75e555402972246b0b8e71/tenacity-9.1.4-py3-none-any.whl", hash = "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55", size = 28926, upload-time = "2026-02-07T10:45:32.24Z" },
]
[[package]] [[package]]
name = "ty" name = "ty"
version = "0.0.54" version = "0.0.84"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ae/c6/2ea90406d82cf82b0a68725130da2cc9de161bfa883c7dee4f0d94dbf3ce/ty-0.0.54.tar.gz", hash = "sha256:b6b3cfe174f27744413c898b2488ca52ea76070637095de131698e506b455055", size = 6009000, upload-time = "2026-06-25T17:53:17.098Z" } sdist = { url = "https://files.pythonhosted.org/packages/e1/f6/34f8869c45ea87fe6a931ffa824b1fe4428167173543d92988d631add355/ty-0.0.84.tar.gz", hash = "sha256:0cefdb0cd5d399418dbe83c349ba605047b7dff815ae6f460c80e8522b40be67", size = 7409537, upload-time = "2026-09-24T13:16:24.367Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/b8/26/a83e688e108a29a8630d7075bae47b72614cfa275c88b3166575ca0a8af0/ty-0.0.54-py3-none-linux_armv6l.whl", hash = "sha256:0365ea133d6b028952c22e6412a00da9739bc3b538df3c0a61972bf64d8558f1", size = 11616164, upload-time = "2026-06-25T17:52:29.174Z" }, { url = "https://files.pythonhosted.org/packages/aa/0c/600648778e1c79133ce6b6a5c4c2531ae16b90c9b30f5041b38b574f61b8/ty-0.0.84-py3-none-linux_armv6l.whl", hash = "sha256:868438e2b9abfc835dd5b5b07ed693db95d8348ac40e3228252511d63ae87695", size = 13935333, upload-time = "2026-09-24T13:15:31.167Z" },
{ url = "https://files.pythonhosted.org/packages/85/89/9f3374f9eef0267aed5efbd5544e47cfa6affc94236d929e878a780cf7d7/ty-0.0.54-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0de0cf48918609a3996cc2a4e18e8028fc4d2446bb82df822e9737f53bb9afee", size = 11351250, upload-time = "2026-06-25T17:52:32.181Z" }, { url = "https://files.pythonhosted.org/packages/c5/3b/27f06f49945c36170765fad82076898039ae3873338e9865f876dd62f12c/ty-0.0.84-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b00f39b521f0b559cde0f6abf8dd47213b513b04f33d58efeb9b98486a16bb17", size = 13511328, upload-time = "2026-09-24T13:15:34.446Z" },
{ url = "https://files.pythonhosted.org/packages/28/11/e0e3f542a6de0e0dd5f503215527f8d9fc141426bd52dbe98fce927ff831/ty-0.0.54-py3-none-macosx_11_0_arm64.whl", hash = "sha256:57c7c6c1fcd2aa29cd40117142c0d45a2b0e6c42817ef58ea8dcb5f59d6ea802", size = 10872162, upload-time = "2026-06-25T17:52:35.206Z" }, { url = "https://files.pythonhosted.org/packages/fc/b6/b12dd130e7f30141f0645614f454c925991cd389b1cc6c8284fd6bf32326/ty-0.0.84-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8f78c2915567f2ee62ce0c24d86091bc76f81577677755e242d09951db588b9a", size = 13445445, upload-time = "2026-09-24T13:15:37.476Z" },
{ url = "https://files.pythonhosted.org/packages/3a/b0/fa369840f1ccd391971f693a8f86c445eb1a0fad61a6e0e773f0871a7a6f/ty-0.0.54-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc179be5a070c5dc7785a5e108eece2ab216978a91f91e11661308749a6c28ee", size = 11419826, upload-time = "2026-06-25T17:52:37.942Z" }, { url = "https://files.pythonhosted.org/packages/44/96/f012de4b3c1d9a4773326621380d768b4d48c47e620dc012775c5e01c7d7/ty-0.0.84-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61155cad0921e890d86d1d911a40b3a0e924ef4bdc3b4f3865a6c89c7e3012dc", size = 13471235, upload-time = "2026-09-24T13:15:40.64Z" },
{ url = "https://files.pythonhosted.org/packages/62/5f/8fbf9b7147cf08cc96cc9f84b9699ae7c7a0a1eb0a4c3d912261351b1cbb/ty-0.0.54-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5a0a764e2261f9292adf14bbc91b9d7ef7ee7c00b8f3d9f2563f19ae3605109b", size = 11411732, upload-time = "2026-06-25T17:52:41.124Z" }, { url = "https://files.pythonhosted.org/packages/05/32/dca630f5f5353fa7bf67eebccf1549df99b16656d21071d0df6677c826a5/ty-0.0.84-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:417c3bf14947ed16b0b92976ab333c27915590354e96d4f581709068d3811355", size = 13680704, upload-time = "2026-09-24T13:15:43.817Z" },
{ url = "https://files.pythonhosted.org/packages/8a/ce/53aa78828f98ce396d5cc0f0878d990c0d70f6bf7704105232399315952f/ty-0.0.54-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2259e1a1f744a5f20f079dcc4061c1464001c75f255aaca7316e422f8f5e5e09", size = 12039318, upload-time = "2026-06-25T17:52:43.981Z" }, { url = "https://files.pythonhosted.org/packages/44/8d/33702c8f62f05ad45b33644fb914d483de6a0a7bc8e51884e9b8773e6d69/ty-0.0.84-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e06ef117759f654f5379efe9ae79bda8a186016c016f1023d05c34f8e15c1546", size = 14518923, upload-time = "2026-09-24T13:15:46.802Z" },
{ url = "https://files.pythonhosted.org/packages/9e/05/5654d4d20376a73cd2e9df8178515c466ff9b2bda15748ee5810c955a265/ty-0.0.54-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8674617617399d4fdd568b3e3a8f87a913df525c80691d732b0744b571b341c8", size = 12625562, upload-time = "2026-06-25T17:52:46.578Z" }, { url = "https://files.pythonhosted.org/packages/c8/a1/da3246e6ef2ae0e842bca4058b25c54bee55ab058ffff4ee54e72517ec01/ty-0.0.84-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a05d1d53db8b64410ab38c6e32c5898510584869d1d5dd9c0605db81c9aef7df", size = 15027731, upload-time = "2026-09-24T13:15:49.755Z" },
{ url = "https://files.pythonhosted.org/packages/37/f7/982be4abde816c821c6aeaba133dc34b5346eebbb29dadbf77b8ee46dad9/ty-0.0.54-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e949f7bbfa80a9fa00968924028aa673a505faa234b0b9d1241b7d34373eae14", size = 12175175, upload-time = "2026-06-25T17:52:49.176Z" }, { url = "https://files.pythonhosted.org/packages/20/17/b0099098a560aeb4f9fde6c912f9a81622c3e44e6555e1fbe322b368ec31/ty-0.0.84-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b92400494ab855cfb1204ac1101e3f53b2d09a8e21d171758b2cf9acdab1809e", size = 14748260, upload-time = "2026-09-24T13:15:52.713Z" },
{ url = "https://files.pythonhosted.org/packages/6a/8b/371fa3d121a1d9c8806a997ec5aa7b6a91912044d936594e6cf04d197471/ty-0.0.54-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a0ce5af9eefe636888377d8ab8ea817239ed96de64fede62c4e5331ae16db5c", size = 11945921, upload-time = "2026-06-25T17:52:51.983Z" }, { url = "https://files.pythonhosted.org/packages/60/d5/32cd67aedb271f006e716ad093806c96c5d7e2af86be8255469dbd3d76c8/ty-0.0.84-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af17eb391ae3027fed9be1780bea555c2b8a25ba68e184c627b4981b2c9eaab7", size = 14205858, upload-time = "2026-09-24T13:15:55.677Z" },
{ url = "https://files.pythonhosted.org/packages/dd/76/cab1ff8f6029b657b3fed59add1a2f188df6af26b6d649a6c789139dd88d/ty-0.0.54-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:ad329d1942e23ee428948e673b2a50249f27e164220075b0a2ef8169029c8423", size = 12269965, upload-time = "2026-06-25T17:52:54.697Z" }, { url = "https://files.pythonhosted.org/packages/43/c4/638ac62ca2e8eca1b92e3e16273085744fada728b993500819b5c258ca15/ty-0.0.84-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:44cf06af0e7ec78ea6d8ff33a3450fc19627756a4fba681f7410d4e3ef63047a", size = 14573196, upload-time = "2026-09-24T13:15:59.165Z" },
{ url = "https://files.pythonhosted.org/packages/31/2f/c14b36cacacf7cc5ae40c862c34c07fadeba85cb93fd8f328cdac2029270/ty-0.0.54-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6e43124e6ac4cb9702d99facb28c6b2f2574c8a4b8f59eae2632eef4962e3439", size = 11370742, upload-time = "2026-06-25T17:52:57.233Z" }, { url = "https://files.pythonhosted.org/packages/a6/f6/9ee81c5d8921e5fd5f10889ae563c95cd3272b31df2ada528919ca1cef6b/ty-0.0.84-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c6e56b443ff9ba462e34bfbc1e582004c2719ed35f8f93a433744b0ba0afb2d9", size = 13480760, upload-time = "2026-09-24T13:16:02.313Z" },
{ url = "https://files.pythonhosted.org/packages/db/1b/9e55bb273d3ea8efe165a73e34ae92220593e4dbfe665b8283be9d3eadea/ty-0.0.54-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:994ede70fc1b6f0efc29d0f1fc0d819630bed1d2610eac2e9dabb2840f3f3bf9", size = 11432346, upload-time = "2026-06-25T17:52:59.898Z" }, { url = "https://files.pythonhosted.org/packages/f6/83/fbe5d1177667ae16d84c3242846b143809c9cc09568b333e5a9b4d5908f6/ty-0.0.84-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8b6562fcc48820c1030e2a4509ef2c14ef93043ca4860280074314ac3a825b95", size = 13696346, upload-time = "2026-09-24T13:16:05.391Z" },
{ url = "https://files.pythonhosted.org/packages/db/16/402e8d2b1ab1020a25774f22ec0ba4378617758197278c1ddbc093b64854/ty-0.0.54-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b1d048df26abb433acc5b3bcef214167c7dc9e3341f8f200a3c59702516a0ed0", size = 11707341, upload-time = "2026-06-25T17:53:02.678Z" }, { url = "https://files.pythonhosted.org/packages/86/9a/1b4ac9ddc34cc0ab4624bc219825d66af52f1ad87f4778ad08ad03fa06d5/ty-0.0.84-py3-none-musllinux_1_2_i686.whl", hash = "sha256:dee7421451efcbd70f03e95a8f2c8d9e5ec3d35edcd27b4095ee00ee062abfc4", size = 13959410, upload-time = "2026-09-24T13:16:08.661Z" },
{ url = "https://files.pythonhosted.org/packages/3b/ca/220599403e1dc864109402c88e22e8793108ec3be41576cd044772023d94/ty-0.0.54-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:651e90094d41c4add3d616e0f2c1c881c6700a2ae3ac191c023c665050ff2cdc", size = 12053427, upload-time = "2026-06-25T17:53:05.983Z" }, { url = "https://files.pythonhosted.org/packages/c9/04/70e289437a67dfd686056ab4fd91880261a8233033a6a95d29393741fb97/ty-0.0.84-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:31e96307cf621babe969307dd657783d672e2d8bdbd06493710da635bf63fa07", size = 14350294, upload-time = "2026-09-24T13:16:11.865Z" },
{ url = "https://files.pythonhosted.org/packages/fc/a9/f08df843e3d4422184a847829eb103c49a6118eed7024969a1a706de1c5e/ty-0.0.54-py3-none-win32.whl", hash = "sha256:7e876c9b5130afc6b6e46035a2eecca9b563b78249030296e1089922428d5415", size = 11044018, upload-time = "2026-06-25T17:53:09.582Z" }, { url = "https://files.pythonhosted.org/packages/9c/f4/ae7dbd118425c850981d23efb850f0d9d7f6b8d2b852bde2767434f668b4/ty-0.0.84-py3-none-win32.whl", hash = "sha256:4bdc85f91cbaae35825f2b17bb4ac5de0563367875cb0f42b2bf07a1bf7ff2ba", size = 13176439, upload-time = "2026-09-24T13:16:15.049Z" },
{ url = "https://files.pythonhosted.org/packages/4c/26/230719333d79fae599e3c2da5ac35e36786cdd6c4b4f2a5d5450ce171dc7/ty-0.0.54-py3-none-win_amd64.whl", hash = "sha256:f478476f3222807b4d92f15c5298a8c242f2ed17c724da75f4a8c6b522b4f29a", size = 12107765, upload-time = "2026-06-25T17:53:12.2Z" }, { url = "https://files.pythonhosted.org/packages/29/ec/994d87252f065dda5a500bc8afc726d18f6582d81d6f4ff1feb8a267c800/ty-0.0.84-py3-none-win_amd64.whl", hash = "sha256:71cc9aa7a7c89d4e12c6a814e9c0c3310ad5bee44efedbd6f5a42d91e9ac0f0d", size = 13962879, upload-time = "2026-09-24T13:16:18.191Z" },
{ url = "https://files.pythonhosted.org/packages/28/68/b5fcb35ceebffab3e8898bd25e51d0bb77a0c43c50b7a8e3fae56125e4f5/ty-0.0.54-py3-none-win_arm64.whl", hash = "sha256:01ab9eb8c0802d35ae73fa08e4e037963250ee5ee6aa7ed8c8b994e5495db5ef", size = 11498510, upload-time = "2026-06-25T17:53:14.693Z" }, { url = "https://files.pythonhosted.org/packages/6d/1c/e6e91852b72566b1ab9aca504712857dfb89324c12092a5bd61d42d34427/ty-0.0.84-py3-none-win_arm64.whl", hash = "sha256:6c9d3718c4a0d318bc9c92af9b2aa85c3294e7dff54cf1d69ad79568f56516da", size = 13656453, upload-time = "2026-09-24T13:16:21.51Z" },
] ]
[[package]] [[package]]
name = "typer" name = "typer"
version = "0.26.8" version = "0.27.2"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "annotated-doc" }, { name = "annotated-doc" },
@@ -1319,9 +1414,9 @@ dependencies = [
{ name = "rich" }, { name = "rich" },
{ name = "shellingham" }, { name = "shellingham" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/7c/f7/68adc395201b20b872d68e975386832e8005ffeacedd43a1d837a32815be/typer-0.26.8.tar.gz", hash = "sha256:c244a6bd558886fe3f8780efb6bdd28bb9aff005a94eedebaa5cb32926fe2f7e", size = 202097, upload-time = "2026-06-26T09:22:45.705Z" } sdist = { url = "https://files.pythonhosted.org/packages/16/f7/57713ba479fd405eb76de31404b2c744c289e336b2d999511ebf51e496f7/typer-0.27.2.tar.gz", hash = "sha256:269b7eb9d3c202ca84b4bc9618cb04ebb43d3d4d1e567e4c768607232c05f945", size = 204045, upload-time = "2026-08-28T10:26:55.046Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/80/87/b9fd69c92c6102a066e1b86a35243f53e70bd4c709f2a26d9f4fee4f4dc0/typer-0.26.8-py3-none-any.whl", hash = "sha256:3512ca79ac5c11113414b36e80281b872884477722440691c89d1112e321a49c", size = 122564, upload-time = "2026-06-26T09:22:44.72Z" }, { url = "https://files.pythonhosted.org/packages/dc/bf/205d0004930ede8f542fb58f601526fccf4ae7626075ca1e6c4de5d3d652/typer-0.27.2-py3-none-any.whl", hash = "sha256:b3a5fc4342d5fc8fda8fc3010b1cf117e9249aab7fae800c2eff62fd3842d97d", size = 123130, upload-time = "2026-08-28T10:26:53.752Z" },
] ]
[[package]] [[package]]
@@ -1356,11 +1451,11 @@ wheels = [
[[package]] [[package]]
name = "urllib3" name = "urllib3"
version = "2.7.0" version = "2.8.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } sdist = { url = "https://files.pythonhosted.org/packages/e3/05/b17359e1cefb4f909b5e40b1b90a496d987258916dbbf88e842c729f510e/urllib3-2.8.0.tar.gz", hash = "sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63", size = 458972, upload-time = "2026-09-15T19:29:36.253Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, { url = "https://files.pythonhosted.org/packages/92/9d/c4e665119135114480843e7ab388fa94d8480650450e6f8e26b70d323a4c/urllib3-2.8.0-py3-none-any.whl", hash = "sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3", size = 135717, upload-time = "2026-09-15T19:29:34.577Z" },
] ]
[[package]] [[package]]
@@ -1483,18 +1578,18 @@ wheels = [
[[package]] [[package]]
name = "zizmor" name = "zizmor"
version = "1.28.0" version = "1.30.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/64/92/6b58872ac1184a441037bdc05c658a3900f49401b25f6e834a268ef6ff52/zizmor-1.28.0.tar.gz", hash = "sha256:6d5a300b80bf4c12e9cbe78ffd3874ec3f94b65bea78c994ec18157e2846ece0", size = 550169, upload-time = "2026-07-21T22:16:27.361Z" } sdist = { url = "https://files.pythonhosted.org/packages/a7/c5/3d3dc61c0b64c082e1f704f5cf44f6a38153adbccf8787dd06a1a0ed14e0/zizmor-1.30.1.tar.gz", hash = "sha256:cbb7e5cb2fec471066d5c326bcaa69ed3eefdcf6a76d7ad38335b924acf86c07", size = 575745, upload-time = "2026-09-09T05:27:21.325Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/e8/48/a725a1c34e5565eeeb4e4a93c59662f206e5156f2029627c66b2373edf55/zizmor-1.28.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:149dba59a8bd2897960ee54c40ae8b5801a71293bad71c8d2913c74ab66a294c", size = 8909502, upload-time = "2026-07-21T22:16:07.47Z" }, { url = "https://files.pythonhosted.org/packages/22/a1/a877361a6c6ae13fb0ff4ed3c9d681ccddb9c838c5c3ca8f163a0fcfded9/zizmor-1.30.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:17fda74e15fe41a6aa354ea4cf71fc7ff3cd025cda6584bb6d7466b3f1d00fe6", size = 9148491, upload-time = "2026-09-09T05:26:58.842Z" },
{ url = "https://files.pythonhosted.org/packages/20/92/ba6d0eb2b336b4c4826bb0c9ed23c3239d46d71d8c1ddd2d395efeff7f52/zizmor-1.28.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:0949f57a6d20deeb9c509705afce8de233c166475e25be305985a1fe553c6e0d", size = 8520554, upload-time = "2026-07-21T22:16:09.587Z" }, { url = "https://files.pythonhosted.org/packages/eb/0e/ea44a8f466b9a080dc0634497f6c9ee63dd586a94f5e2cd743e06c4df140/zizmor-1.30.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f9eb092f089e35fa9fb3b70aeec0a19eca7dacf2ab161f1d50b18351895f085c", size = 8743712, upload-time = "2026-09-09T05:27:01.342Z" },
{ url = "https://files.pythonhosted.org/packages/4f/07/69aba8157056fc0949cbdc787d8437813961e204a117c629d34afc827d6d/zizmor-1.28.0-py3-none-manylinux_2_24_aarch64.whl", hash = "sha256:9819f91f0ef486e4af98a6aacfed23c6b4067fabcecb88d49231228a3d43f821", size = 8759226, upload-time = "2026-07-21T22:16:11.627Z" }, { url = "https://files.pythonhosted.org/packages/37/9c/43764a6e9eb5ad5a332202afc3e63622e3380669ecf7079b6b0de892e474/zizmor-1.30.1-py3-none-manylinux_2_24_aarch64.whl", hash = "sha256:92906f448365672ebb657fb50d36f997290a3aaba8cac723214741ea9054845d", size = 9009843, upload-time = "2026-09-09T05:27:03.522Z" },
{ url = "https://files.pythonhosted.org/packages/69/33/b65717a3ca4573611f47612e67be5458ff700e857f49f759741fa67b0519/zizmor-1.28.0-py3-none-manylinux_2_28_armv7l.whl", hash = "sha256:354e6cb98a15a88593a6f7ac6236b092b83a2aad5c4768ee750f9b3262f668d9", size = 8382005, upload-time = "2026-07-21T22:16:13.896Z" }, { url = "https://files.pythonhosted.org/packages/cf/2d/96c9c7b37e46e2b092e5daac3c6e9d4966ed7b58845f3f04950043d89c34/zizmor-1.30.1-py3-none-manylinux_2_28_armv7l.whl", hash = "sha256:5e8a3d54c3d9d51307371f4d1d49f8f9e56de00205c6cc660c6846a95ad4e135", size = 8620204, upload-time = "2026-09-09T05:27:06.001Z" },
{ url = "https://files.pythonhosted.org/packages/5b/b4/f823bd2a1ba6dc432fdcbd249d4c442ce79bd137d34d986fce5c181f2800/zizmor-1.28.0-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:ae2cab67ce713e760e0d1b61ad749d374693ea2b310337aab11cd446748267f3", size = 9175601, upload-time = "2026-07-21T22:16:16.177Z" }, { url = "https://files.pythonhosted.org/packages/63/55/1900b53d34dcebc207cf3ceb7957b3748e8bb40958f062e3853ab72f2395/zizmor-1.30.1-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:eee12266b793cb87ad4a7e3af2e72404f8a63e3de5eb099b80bf7b1cfd232a8e", size = 9467893, upload-time = "2026-09-09T05:27:08.07Z" },
{ url = "https://files.pythonhosted.org/packages/f3/75/adad17b0320eb3bc9ed797019fccf69a809affdf5ba9402bdc8b910957e1/zizmor-1.28.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7b00018cf2cc948c3b3e010c1a1f30fdc001f0d062640469f17e0ef006e74eb7", size = 8792861, upload-time = "2026-07-21T22:16:18.035Z" }, { url = "https://files.pythonhosted.org/packages/9b/aa/d998bbdebf1f40e0d3dd027f94bceb961065640b29bc82bf5ffa9266fdb9/zizmor-1.30.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:15679642e8c4f825ba3f22537c698b1bbbad19ead23640f62ec45ebbe7abc803", size = 9038587, upload-time = "2026-09-09T05:27:10.226Z" },
{ url = "https://files.pythonhosted.org/packages/1a/3d/4d5583f24730f404c104eacf8aca4ca5a3d4c480e9d3cfe3eadd93351fd6/zizmor-1.28.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:6897f02b0d02fd709f5ebc13cd37d97ad464219e0ba86a03f6d86f7777b7b102", size = 8343070, upload-time = "2026-07-21T22:16:20.092Z" }, { url = "https://files.pythonhosted.org/packages/e0/16/0512afae720fd4b7e22bbb6f5bdd24fad49d6b1b92cd34ac71e0d8076109/zizmor-1.30.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:bf63099a27ef3bf1329dbab44bb9d1027124855c71bd0eca864d5c8e1c5c0db1", size = 8583701, upload-time = "2026-09-09T05:27:12.73Z" },
{ url = "https://files.pythonhosted.org/packages/97/e2/4521e6dd56bc0d44eefb177794e261925c5cd00ab647b17e5513bddcb985/zizmor-1.28.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ef69d198dcf6835c9b6eea8673cbc5c36f10b3f1b98b51d28266b7e01c3704d4", size = 9262222, upload-time = "2026-07-21T22:16:21.89Z" }, { url = "https://files.pythonhosted.org/packages/ca/ff/72efacfb79177835609fe712e9d543a0e5a05d41e3774e907f0b610c8619/zizmor-1.30.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:4a5ef2fa4fbd2984f794cfc2f95790bd03fa0bae801487ed550e044675874e60", size = 9558806, upload-time = "2026-09-09T05:27:15.124Z" },
{ url = "https://files.pythonhosted.org/packages/a2/96/5d841d197c821921cb8243352c610ca2505a86871c10f81efe4da190d24c/zizmor-1.28.0-py3-none-win32.whl", hash = "sha256:833c360ba5a9c74ca45007b8c79939826fca0c5ed65144fa08f63a7009cef096", size = 7541161, upload-time = "2026-07-21T22:16:23.965Z" }, { url = "https://files.pythonhosted.org/packages/bb/64/1746121c819319ad7e9d0dfaa96de38dc52d4ac5f6485472b11362f3c6ba/zizmor-1.30.1-py3-none-win32.whl", hash = "sha256:42512bb4a1bce4c787d221d1572ff6b691fa52a3d129b2910be8afaf860c5871", size = 7773805, upload-time = "2026-09-09T05:27:17.218Z" },
{ url = "https://files.pythonhosted.org/packages/b1/f5/3f4591746e5a9c7e8efc2e9e02aa43e1aa0957fe33cae64ce386c74b882e/zizmor-1.28.0-py3-none-win_amd64.whl", hash = "sha256:c93b30d211b0b0c38905a8803cc2653fff37de03eb77105302a02f709773bd4a", size = 8619340, upload-time = "2026-07-21T22:16:25.6Z" }, { url = "https://files.pythonhosted.org/packages/db/fe/2889fcbcf7728dd5e013bae7cf5c4e634d981a9e26d55e1da6ab9006404f/zizmor-1.30.1-py3-none-win_amd64.whl", hash = "sha256:06e039fae8e185e2a1a8b002f2d95a6f5c3b548048bdd79e004dcb501fcd0868", size = 8925696, upload-time = "2026-09-09T05:27:19.159Z" },
] ]