Compare commits

13 Commits

Author SHA1 Message Date
letteka c4d2516ea5 chore: dependency updates 2026-05-25 (#31)
CI / black (push) Failing after 9m17s
CI / ruff (push) Successful in 11m43s
CI / mypy (push) Successful in 10m48s
CI / pytest (push) Failing after 12m7s
Dependency update / dependency-update (push) Successful in 9m30s
## Dependency update summary

Generated: 2026-05-25 09:01 UTC

### Vulnerability scan

No vulnerabilities found.

### Outdated packages

The following packages were updated:

Was: version = "26.5.0"
Updated: version = "26.5.1"
Was: version = "2026.4.22"
Updated: version = "2026.5.20"
Was: version = "8.4.0"
Updated: version = "8.4.1"
Was: version = "3.5.0"
Updated: version = "3.5.1"
Was: version = "3.15"
Updated: version = "3.16"
Was: python-versions = ">=3.8"
Updated: python-versions = ">=3.9"
Was: version = "2.4.5"
Updated: version = "2.4.6"
Was: version = "3.4.11"
Updated: version = "3.4.12"
Was: version = "2.0.49"
Updated: version = "2.0.50"
---------

Co-authored-by: Gitea CI <ci@gitea.com>
Reviewed-on: #31
2026-05-25 15:09:15 -07:00
letteka f19ccf349b Adding recording (#29)
CI / mypy (push) Successful in 1m43s
CI / black (push) Successful in 1m34s
CI / ruff (push) Successful in 1m32s
CI / pytest (push) Failing after 1m44s
Dependency update / dependency-update (push) Successful in 1m58s
Reviewed-on: #29
Co-authored-by: Andrew Kettel <andrew.kettel@gmail.com>
Co-committed-by: Andrew Kettel <andrew.kettel@gmail.com>
2026-05-24 14:54:38 -07:00
letteka 0f8572b26e chore: dependency updates 2026-05-17 (#28)
CI / black (push) Successful in 1m30s
CI / ruff (push) Successful in 1m26s
CI / mypy (push) Successful in 1m43s
CI / pytest (push) Successful in 1m35s
Dependency update / dependency-update (push) Successful in 1m47s
## Dependency update summary

Generated: 2026-05-17 00:56 UTC

### Vulnerability scan

No vulnerabilities found.

### Outdated packages

The following packages were updated:

Was: version = "8.3.3"
Updated: version = "8.4.0"
---------

Co-authored-by: Gitea CI <ci@gitea.com>
Reviewed-on: #28
2026-05-17 13:18:00 -07:00
letteka 8b7610671c chore: dependency updates 2026-05-17 (#27)
CI / black (push) Successful in 1m32s
CI / ruff (push) Successful in 1m29s
CI / mypy (push) Successful in 1m42s
CI / pytest (push) Successful in 1m37s
Dependency update / dependency-update (push) Successful in 1m52s
## Dependency update summary

Generated: 2026-05-17 00:21 UTC

### Vulnerability scan

No vulnerabilities found.

### Outdated packages

The following packages were updated:

Was: markers = {pi = "python_version == \"3.12\""}
Updated: markers = {pi = "python_version < \"3.13\""}
---------

Co-authored-by: Gitea CI <ci@gitea.com>
Reviewed-on: #27
2026-05-16 17:49:02 -07:00
letteka 5f020258e8 Merge pull request 'update black' (#26) from update_black into main
CI / black (push) Successful in 1m30s
CI / ruff (push) Successful in 1m28s
CI / mypy (push) Successful in 1m42s
CI / pytest (push) Successful in 1m36s
Dependency update / dependency-update (push) Successful in 1m49s
Reviewed-on: #26
2026-05-16 17:11:20 -07:00
letteka af38580a71 update black
CI / black (pull_request) Successful in 1m29s
CI / ruff (pull_request) Successful in 1m31s
CI / mypy (pull_request) Successful in 1m42s
CI / pytest (pull_request) Successful in 1m38s
2026-05-16 17:06:49 -07:00
letteka cdaaaad6af new readme (#25)
CI / black (push) Successful in 1m30s
CI / ruff (push) Successful in 1m31s
CI / mypy (push) Successful in 1m43s
CI / pytest (push) Successful in 1m37s
Dependency update / dependency-update (push) Failing after 1m50s
Reviewed-on: #25
Co-authored-by: Andrew Kettel <andrew.kettel@gmail.com>
Co-committed-by: Andrew Kettel <andrew.kettel@gmail.com>
2026-05-16 16:45:18 -07:00
letteka bc935b41de chore: dependency updates 2026-05-16 (#24)
CI / black (push) Successful in 1m33s
CI / ruff (push) Successful in 1m30s
CI / mypy (push) Successful in 1m44s
CI / pytest (push) Successful in 1m37s
Dependency update / dependency-update (push) Failing after 1m49s
## Dependency update summary

Generated: 2026-05-16 16:41 UTC

### Vulnerability scan

Vulnerabilities found:

Name | Version | ID | Fix Versions
--- | --- | --- | ---
pytest | 8.4.2 | CVE-2025-71176 | 9.0.3

Name | Skip Reason
--- | ---
birdcam2 | Dependency not found on PyPI and could not be audited: birdcam2 (0.1.0)

### Outdated packages

The following packages were updated:

Was: markers = {pi = "python_version == \"3.12\""}
Updated: markers = {pi = "python_version < \"3.13\""}
---------

Co-authored-by: Gitea CI <ci@gitea.com>
Reviewed-on: #24
Co-authored-by: Andrew Kettel <andrew.kettel@gmail.com>
Co-committed-by: Andrew Kettel <andrew.kettel@gmail.com>
2026-05-16 16:21:26 -07:00
letteka 02d3a9c33f Fixing ruff and mypy issues (#23)
CI / black (push) Successful in 1m31s
CI / ruff (push) Successful in 1m28s
CI / mypy (push) Successful in 1m40s
CI / pytest (push) Successful in 1m35s
Dependency update / dependency-update (push) Successful in 1m49s
Reviewed-on: #23
Co-authored-by: Andrew Kettel <andrew.kettel@gmail.com>
Co-committed-by: Andrew Kettel <andrew.kettel@gmail.com>
2026-05-16 09:33:40 -07:00
letteka 800d665c8b Merge pull request 'chore: dependency updates 2026-05-12' (#19) from dependency-updates-20260512 into main
CI / black (push) Successful in 1m28s
CI / ruff (push) Successful in 1m28s
CI / mypy (push) Successful in 1m42s
CI / pytest (push) Failing after 1m37s
Dependency update / dependency-update (push) Successful in 1m50s
Reviewed-on: #19
2026-05-15 16:10:24 -07:00
letteka c2faf2c81c Fixing ruff and mypy issues (#20)
Dependency update / dependency-update (push) Failing after 1m52s
CI / black (push) Successful in 1m31s
CI / ruff (push) Successful in 1m32s
CI / mypy (push) Successful in 1m41s
CI / pytest (push) Failing after 1m37s
Reviewed-on: #20
Co-authored-by: Andrew Kettel <andrew.kettel@gmail.com>
Co-committed-by: Andrew Kettel <andrew.kettel@gmail.com>
2026-05-12 16:42:02 -07:00
Gitea CI 49a81b5489 chore: update dependencies 2026-05-12
CI / black (pull_request) Failing after 1m31s
CI / ruff (pull_request) Successful in 1m30s
CI / mypy (pull_request) Successful in 1m44s
CI / pytest (pull_request) Failing after 1m36s
2026-05-12 22:45:09 +00:00
letteka fb1d7fec37 Fixing ruff and mypy issues (#18)
CI / black (push) Failing after 1m34s
CI / ruff (push) Successful in 1m31s
CI / mypy (push) Successful in 1m44s
CI / pytest (push) Failing after 1m42s
Dependency update / dependency-update (push) Successful in 1m51s
Reviewed-on: #18
Co-authored-by: Andrew Kettel <andrew.kettel@gmail.com>
Co-committed-by: Andrew Kettel <andrew.kettel@gmail.com>
2026-05-12 15:34:17 -07:00
9 changed files with 1099 additions and 477 deletions
+2
View File
@@ -12,3 +12,5 @@ coverage.xml
.coverage .coverage
htmlcov/ htmlcov/
birdcam.db birdcam.db
.vscode/
.ruff_cache/
+96 -83
View File
@@ -1,93 +1,106 @@
# birdcam2 # BirdCam: A Flask-Based Camera Monitoring System
A lightweight, Python-based system for monitoring and managing Raspberry Pi cameras using Flask, SQLAlchemy, and modern Python best practices.
## 🚀 Overview
## Getting started BirdCam is a simple yet robust application designed to monitor camera status and log events in real time. Built with Flask and SQLAlchemy, it provides:
- Real-time camera status tracking (`running` state).
- Event logging for camera actions (`start`, `stop`) with IP addresses and timestamps.
- A clean, maintainable architecture using modern Python 3.12 features and type hints.
To make it easy for you to get started with GitLab, here's a list of recommended next steps. It's ideal for hobbyists, developers, or IoT projects where you need to track camera activity from a central dashboard or script.
Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)! ## 🔧 Key Features
## Add your files - ✅ Real-time camera status (e.g., whether a camera is running or not).
- ✅ Event logging with full metadata (action, IP address, timestamp).
- ✅ Automatic creation of status records if missing.
- ✅ Type-safe, well-documented code with full type hints.
- ✅ Modern Python practices:
- Uses Python 3.12 with `__future__.annotations`.
- Leverages `dataclasses`-like patterns with `Mapped` and `mapped_column`.
- Includes comprehensive linting via **Black**, **Ruff**, and **Mypy**.
- ✅ Automated test coverage with `pytest` and `pytest-cov`.
* [Create](https://docs.gitlab.com/user/project/repository/web_editor/#create-a-file) or [upload](https://docs.gitlab.com/user/project/repository/web_editor/#upload-a-file) files ## 📦 Dependencies
* [Add files using the command line](https://docs.gitlab.com/topics/git/add_files/#add-files-to-a-git-repository) or push an existing Git repository with the following command:
``` | Package | Version | Purpose |
cd existing_repo |----------------------|---------------|------------------------------------------|
git remote add origin https://gitlab.com/akettel/birdcam2.git | `Flask` | `^3.0` | Web framework for serving the app |
git branch -M main | `Flask-SQLAlchemy` | `^3.1.1` | ORM for database interactions |
git push -uf origin main | `Pillow` | `^12.1.1` | Image processing (for camera previews) |
| `Gunicorn` | `^25.1.0` | Production WSGI server |
| `Pytest` | `^9.0.3` | Testing framework |
| `Pytest-Cov` | `^7.1.0` | Test coverage reporting |
| `Black` / `Ruff` | `^26.0` / `^0.4` | Code formatting and linting |
| `Mypy` | `^1.10` | Static type checking |
> ✅ All dependencies are pinned for stability and compatibility.
## 🛠️ Setup & Installation
1. Clone the repository:
```bash
git clone https://gitea.letteka.com/letteka/birdcam.git
cd birdcam
```
2. Install dependencies:
```bash
pip install -e .
```
3. Run the app:
```bash
gunicorn -w 1 -b 0.0.0.0:5000 app:app
```
Or use Flasks built-in dev server:
```bash
flask run
```
## 📝 How It Works
### Camera Status
- The `CameraStatus` model tracks a single cameras running state.
- On startup, it ensures a status record exists (ID = 1).
- The `get()` and `set_running()` methods allow safe access and updates.
### Camera Events
- Every time a camera action occurs (`start` or `stop`), an event is logged.
- Events include:
- Action type (`start` / `stop`)
- Client IP address
- Timestamp with timezone support
- The `recent()` method allows retrieval of the latest events (e.g., for logging or dashboard display).
## 🧪 Testing
The project includes full test coverage using `pytest` with coverage reporting:
```bash
pytest --cov=src --cov-report=term-missing
``` ```
## Integrate with your tools Tests are located in the tests/ directory and cover:
- Status retrieval and updates.
* [Set up project integrations](https://gitlab.com/akettel/birdcam2/-/settings/integrations) - Event logging and retrieval.
- Edge cases (e.g., missing status records).
## Collaborate with your team ## 📝 Development Workflow
✅ Code is formatted with Black and linted with Ruff.
* [Invite team members and collaborators](https://docs.gitlab.com/user/project/members/) ✅ Static type checking is enforced using Mypy.
* [Create a new merge request](https://docs.gitlab.com/user/project/merge_requests/creating_merge_requests/) ✅ All code uses consistent naming and formatting.
* [Automatically close issues from merge requests](https://docs.gitlab.com/user/project/issues/managing_issues/#closing-issues-automatically) ✅ All public APIs are documented with docstrings.
* [Enable merge request approvals](https://docs.gitlab.com/user/project/merge_requests/approvals/) ## ⚙️ Future Improvements
* [Set auto-merge](https://docs.gitlab.com/user/project/merge_requests/auto_merge/) - Add support for multiple cameras.
- Integrate with a web dashboard (e.g., using Vue.js or Streamlit).
## Test and Deploy - Add camera preview functionality via HTTP endpoints.
- Support for MQTT or other event-driven systems.
Use the built-in continuous integration in GitLab. - Add user authentication or role-based access.
## 📚 License
* [Get started with GitLab CI/CD](https://docs.gitlab.com/ci/quick_start/) This project is open-source and available under the MIT License.
* [Analyze your code for known vulnerabilities with Static Application Security Testing (SAST)](https://docs.gitlab.com/user/application_security/sast/) ## 🙌 Contributing
* [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/topics/autodevops/requirements/) Contributions are welcome! Please open an issue or submit a pull request with clear descriptions and tests.
* [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/user/clusters/agent/) 📬 Contact: Andrew Kettel andrew.kettel@gmail.com
* [Set up protected environments](https://docs.gitlab.com/ci/environments/protected_environments/)
***
# Editing this README
When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thanks to [makeareadme.com](https://www.makeareadme.com/) for this template.
## Suggestions for a good README
Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information.
## Name
Choose a self-explaining name for your project.
## Description
Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors.
## Badges
On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge.
## Visuals
Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method.
## Installation
Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection.
## Usage
Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README.
## Support
Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc.
## Roadmap
If you have ideas for releases in the future, it is a good idea to list them in the README.
## Contributing
State if you are open to contributions and what your requirements are for accepting them.
For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self.
You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser.
## Authors and acknowledgment
Show your appreciation to those who have contributed to the project.
## License
For open source projects, say how it is licensed.
## Project status
If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.
Generated
+321 -305
View File
@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand. # This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand.
[[package]] [[package]]
name = "attrs" name = "attrs"
@@ -48,39 +48,39 @@ files = [
[[package]] [[package]]
name = "black" name = "black"
version = "26.3.1" version = "26.5.1"
description = "The uncompromising code formatter." description = "The uncompromising code formatter."
optional = false optional = false
python-versions = ">=3.10" python-versions = ">=3.10"
groups = ["dev"] groups = ["dev"]
files = [ files = [
{file = "black-26.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:86a8b5035fce64f5dcd1b794cf8ec4d31fe458cf6ce3986a30deb434df82a1d2"}, {file = "black-26.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9942db8888e06943c5dde66ca0037dcff82a2a4ec1ad0ada9e0d2ee9d9823893"},
{file = "black-26.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5602bdb96d52d2d0672f24f6ffe5218795736dd34807fd0fd55ccd6bf206168b"}, {file = "black-26.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:89c93167a74d3a75dfaa38a5c7cca015537d5820dd7f17d63267d674a61cae90"},
{file = "black-26.3.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c54a4a82e291a1fee5137371ab488866b7c86a3305af4026bdd4dc78642e1ac"}, {file = "black-26.5.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22f2cd76d069cc54c71f10360744ba8983fbb616903b4304a85b734915c8e1b4"},
{file = "black-26.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:6e131579c243c98f35bce64a7e08e87fb2d610544754675d4a0e73a070a5aa3a"}, {file = "black-26.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:87ed5c6f450580a2f6790bc7cbfb016dfc73bc750249762268a3695361315eef"},
{file = "black-26.3.1-cp310-cp310-win_arm64.whl", hash = "sha256:5ed0ca58586c8d9a487352a96b15272b7fa55d139fc8496b519e78023a8dab0a"}, {file = "black-26.5.1-cp310-cp310-win_arm64.whl", hash = "sha256:58b4bd92cf88aacf83d88479c8f9caee044b1ec55f2451a337354a7ea2590a22"},
{file = "black-26.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:28ef38aee69e4b12fda8dba75e21f9b4f979b490c8ac0baa7cb505369ac9e1ff"}, {file = "black-26.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:96ae2c733b2aabdd9986e2c5df628ff3473676cd1c5faded1ff496cf6d74083c"},
{file = "black-26.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf9bf162ed91a26f1adba8efda0b573bc6924ec1408a52cc6f82cb73ec2b142c"}, {file = "black-26.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0e48b87e03bf109288e55cfceadcfa15ff5470aca2851a851950ed2926f450d7"},
{file = "black-26.3.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:474c27574d6d7037c1bc875a81d9be0a9a4f9ee95e62800dab3cfaadbf75acd5"}, {file = "black-26.5.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5119fa92ae61f786e8c3662fd60aece1d0a2dd5cca5d0c79417a95e7a4272a59"},
{file = "black-26.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:5e9d0d86df21f2e1677cc4bd090cd0e446278bcbbe49bf3659c308c3e402843e"}, {file = "black-26.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:30d3c14661f2792e9142cce3eeeb1cbc175b3eb5f733be0c8eeb99651e52b0c3"},
{file = "black-26.3.1-cp311-cp311-win_arm64.whl", hash = "sha256:9a5e9f45e5d5e1c5b5c29b3bd4265dcc90e8b92cf4534520896ed77f791f4da5"}, {file = "black-26.5.1-cp311-cp311-win_arm64.whl", hash = "sha256:1ef92b76f7733f282fd096ea406200b5a286c42947412b0eaff3a74e3616cefe"},
{file = "black-26.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e6f89631eb88a7302d416594a32faeee9fb8fb848290da9d0a5f2903519fc1"}, {file = "black-26.5.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4ad6fa01f941920f54f2bbb35f3df7673428a0ef98a0b0840c2eaef3b110efa8"},
{file = "black-26.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41cd2012d35b47d589cb8a16faf8a32ef7a336f56356babd9fcf70939ad1897f"}, {file = "black-26.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3915f256e75a2d7cf88d8953d37f780455dc586cc72dee059c528fe77f581217"},
{file = "black-26.3.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f76ff19ec5297dd8e66eb64deda23631e642c9393ab592826fd4bdc97a4bce7"}, {file = "black-26.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d98d4137277c75dfb898ec8d846c4fd68ba1e9cf77f95e2865c203dc18f4c3d"},
{file = "black-26.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:ddb113db38838eb9f043623ba274cfaf7d51d5b0c22ecb30afe58b1bb8322983"}, {file = "black-26.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:a1dca32d9f1784af512a13410ec204c6f7f0aa9797a111c42e1c03449821c264"},
{file = "black-26.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:dfdd51fc3e64ea4f35873d1b3fb25326773d55d2329ff8449139ebaad7357efb"}, {file = "black-26.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:1037d5ac7b7b310b2632ad867ec8d0e4c4819dcdb0b820f63135da746a24e418"},
{file = "black-26.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:855822d90f884905362f602880ed8b5df1b7e3ee7d0db2502d4388a954cc8c54"}, {file = "black-26.5.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2b36cf2ddf5566e205f6535f782a62194a184d33e175b64ae8c40b1737522be3"},
{file = "black-26.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8a33d657f3276328ce00e4d37fe70361e1ec7614da5d7b6e78de5426cb56332f"}, {file = "black-26.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f7ea64ebfa01b50f693508fc39f875e264446d3b097088f84f203b9d09618a0"},
{file = "black-26.3.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f1cd08e99d2f9317292a311dfe578fd2a24b15dbce97792f9c4d752275c1fa56"}, {file = "black-26.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecb3e624844c798144e9bd986954e0adc81d8911a1f30f375e1252fe26e8c294"},
{file = "black-26.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:c7e72339f841b5a237ff14f7d3880ddd0fc7f98a1199e8c4327f9a4f478c1839"}, {file = "black-26.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:e1a26503279b6b310669fb0b219c39e4820b77e8189fe80f522bb511f247db0a"},
{file = "black-26.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:afc622538b430aa4c8c853f7f63bc582b3b8030fd8c80b70fb5fa5b834e575c2"}, {file = "black-26.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c34b25da232ead53a6f335b76dbea124f4d152ad568b9080d6f944bc2b34b52"},
{file = "black-26.3.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2d6bfaf7fd0993b420bed691f20f9492d53ce9a2bcccea4b797d34e947318a78"}, {file = "black-26.5.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e88976690a64b0af98312ca958415849cb42423423c5f2ee74af4b49a97a2168"},
{file = "black-26.3.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f89f2ab047c76a9c03f78d0d66ca519e389519902fa27e7a91117ef7611c0568"}, {file = "black-26.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32d5ea7f6c8bdfa6e648326ebca1f02b0764e2a029edc6f8dce2627e19d468c3"},
{file = "black-26.3.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b07fc0dab849d24a80a29cfab8d8a19187d1c4685d8a5e6385a5ce323c1f015f"}, {file = "black-26.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ea8d16dc41655aa113cd64665e7219446cd7e4ff2248d7178eaa905190c86b18"},
{file = "black-26.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:0126ae5b7c09957da2bdbd91a9ba1207453feada9e9fe51992848658c6c8e01c"}, {file = "black-26.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:577f21094ea469ef92ec1adaf2c9441a226d2144d01a5be2fa823cecf6543e50"},
{file = "black-26.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:92c0ec1f2cc149551a2b7b47efc32c866406b6891b0ee4625e95967c8f4acfb1"}, {file = "black-26.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:ed1a20af114c301a0269bf01163d51dbef72737fd65f850001e7cbe7f3c7abae"},
{file = "black-26.3.1-py3-none-any.whl", hash = "sha256:2bd5aa94fc267d38bb21a70d7410a89f1a1d318841855f698746f8e7f51acd1b"}, {file = "black-26.5.1-py3-none-any.whl", hash = "sha256:4ed7f7da04046d2e488437170797d3b4a4ad83906683bcb7dfc68b673bbce5e2"},
{file = "black-26.3.1.tar.gz", hash = "sha256:2c50f5063a9641c7eed7795014ba37b0f5fa227f3d408b968936e24bc0566b07"}, {file = "black-26.5.1.tar.gz", hash = "sha256:dd321f668053961824bcc1be1cc1df748b2d7e4fa28086b08331e577b0100a73"},
] ]
[package.dependencies] [package.dependencies]
@@ -151,14 +151,14 @@ redis = ["redis (>=2.10.5)"]
[[package]] [[package]]
name = "certifi" name = "certifi"
version = "2026.4.22" version = "2026.5.20"
description = "Python package for providing Mozilla's CA Bundle." description = "Python package for providing Mozilla's CA Bundle."
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
groups = ["dev"] groups = ["dev"]
files = [ files = [
{file = "certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a"}, {file = "certifi-2026.5.20-py3-none-any.whl", hash = "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897"},
{file = "certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580"}, {file = "certifi-2026.5.20.tar.gz", hash = "sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d"},
] ]
[[package]] [[package]]
@@ -302,14 +302,14 @@ files = [
[[package]] [[package]]
name = "click" name = "click"
version = "8.3.3" version = "8.4.1"
description = "Composable command line interface toolkit" description = "Composable command line interface toolkit"
optional = false optional = false
python-versions = ">=3.10" python-versions = ">=3.10"
groups = ["main", "dev"] groups = ["main", "dev"]
files = [ files = [
{file = "click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613"}, {file = "click-8.4.1-py3-none-any.whl", hash = "sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2"},
{file = "click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2"}, {file = "click-8.4.1.tar.gz", hash = "sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96"},
] ]
[package.dependencies] [package.dependencies]
@@ -537,72 +537,92 @@ sqlalchemy = ">=2.0.16"
[[package]] [[package]]
name = "greenlet" name = "greenlet"
version = "3.5.0" version = "3.5.1"
description = "Lightweight in-process concurrent programming" description = "Lightweight in-process concurrent programming"
optional = false optional = false
python-versions = ">=3.10" python-versions = ">=3.10"
groups = ["main"] groups = ["main"]
markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\"" markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\""
files = [ files = [
{file = "greenlet-3.5.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:29ea813b2e1f45fa9649a17853b2b5465c4072fbcb072e5af6cd3a288216574a"}, {file = "greenlet-3.5.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:7eacb17a9d41538a2bc4912eba5ef13823c83cb69e4d141d0813debe7163187f"},
{file = "greenlet-3.5.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:804a70b328e706b785c6ef16187051c394a63dd1a906d89be24b6ad77759f13f"}, {file = "greenlet-3.5.1-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e5cc9606aa5f4e0bde0d3bd502b44f743864c3ffa5cfa1011b1e30f5aa02366f"},
{file = "greenlet-3.5.0-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:884f649de075b84739713d41dd4dfd41e2b910bfb769c4a3ea02ec1da52cd9bb"}, {file = "greenlet-3.5.1-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c3d35f87c7253b715d13d679e0783d845910144f282cb939fe1ba4ac8616269c"},
{file = "greenlet-3.5.0-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4d0eadc7e4d9ffb2af4247b606cae307be8e448911e5a0d0b16d72fc3d224cfd"}, {file = "greenlet-3.5.1-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:00929c98ec525fd9bf075875d8c5f6a983a90906cdf78a66e6de2d8e466c2a19"},
{file = "greenlet-3.5.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b28037cb07768933c54d81bfe47a85f9f402f57d7d69743b991a713b63954eb"}, {file = "greenlet-3.5.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:540dae7b956209af4d70a3be35927b4055f617763771e5e84a5255bea934d2f5"},
{file = "greenlet-3.5.0-cp310-cp310-manylinux_2_39_riscv64.whl", hash = "sha256:f8c30c2225f40dd76c50790f0eb3b5c7c18431efb299e2782083e1981feed243"}, {file = "greenlet-3.5.1-cp310-cp310-manylinux_2_39_riscv64.whl", hash = "sha256:001775efe7b8e758861294c7a27c28af87f3f3f1c20468a2bc618c45b346c061"},
{file = "greenlet-3.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cda05425526240807408156b6960a17a79a0c760b813573b67027823be760977"}, {file = "greenlet-3.5.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ed8cdb691169715a9a492844a83246f090182247d1a5031dc78a403f68ba1e97"},
{file = "greenlet-3.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9c615f869163e14bb1ced20322d8038fb680b08236521ac3f30cd4c1288785a0"}, {file = "greenlet-3.5.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9d59e840387076a51016777a9328b3f2c427c6f9208a6e958bad251be50a648d"},
{file = "greenlet-3.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:ba8f0bdc2fae6ce915dfd0c16d2d00bca7e4247c1eae4416e06430e522137858"}, {file = "greenlet-3.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:b9152fca4a6466e114aaec745ae61cba739903a109754a9d4e1262f01e9259b1"},
{file = "greenlet-3.5.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:8f1cc966c126639cd152fdaa52624d2655f492faa79e013fea161de3e6dda082"}, {file = "greenlet-3.5.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:73f78f9b9f0a5c06e5c946ba1e8e36f5114923b6be109ee618c54f079c3ea14f"},
{file = "greenlet-3.5.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:362624e6a8e5bca3b8233e45eef33903a100e9539a2b995c364d595dbc4018b3"}, {file = "greenlet-3.5.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0cbed8bb44e23c5b199f888f4e4ce096b45ad9f25ff74a7ad0213875e936bb2"},
{file = "greenlet-3.5.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5ecd83806b0f4c2f53b1018e0005cd82269ea01d42befc0368730028d850ed1c"}, {file = "greenlet-3.5.1-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a203a8bd0acb0701653d3bbb26e404854a68674139ed5cbb778830f42b09bb33"},
{file = "greenlet-3.5.0-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa94cb2288681e3a11645958f1871d48ee9211bd2f66628fdace505927d6e564"}, {file = "greenlet-3.5.1-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6ebeb75c81211f5c702576cf81f315e77e23cfdb2c7c6fcb9dd143e6de35c360"},
{file = "greenlet-3.5.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ff251e9a0279522e62f6176412869395a64ddf2b5c5f782ff609a8216a4e662"}, {file = "greenlet-3.5.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a271fcd66c74615cda6a964fda3f304267a12e50a084472218a39bb0376f563"},
{file = "greenlet-3.5.0-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:64d6ac45f7271f48e45f67c95b54ef73534c52ec041fcda8edf520c6d811f4bc"}, {file = "greenlet-3.5.1-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:017a544f0385d441e88714160d089d6900ef46c9eff9d99b6715a5ef2d127747"},
{file = "greenlet-3.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d874e79afd41a96e11ff4c5d0bc90a80973e476fda1c2c64985667397df432b"}, {file = "greenlet-3.5.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ded7b068c7c31c1a8657d4fd42d886b3e051ae29f88b80c5ff9d502257b0f071"},
{file = "greenlet-3.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0ed006e4b86c59de7467eb2601cd1b77b5a7d657d1ee55e30fe30d76451edba4"}, {file = "greenlet-3.5.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d0932b81d72f552ded9d810d00021b64d89f2195a91ce115b893f943b7a4ab3c"},
{file = "greenlet-3.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:703cb211b820dbffbbc55a16bfc6e4583a6e6e990f33a119d2cc8b83211119c8"}, {file = "greenlet-3.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:88e300d136eac057b2397aa1cfd7328b4c87c7eb66a09c7bc6a1292234db474e"},
{file = "greenlet-3.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:6c18dfb59c70f5a94acd271c72e90128c3c776e41e5f07767908c8c1b74ad339"}, {file = "greenlet-3.5.1-cp311-cp311-win_arm64.whl", hash = "sha256:cc6ab7e555c8a112ad3a76e368e86e12a2754bcae1652a5602e133ec7b635523"},
{file = "greenlet-3.5.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:db2910d3c809444e0a20147361f343fe2798e106af8d9d8506f5305302655a9f"}, {file = "greenlet-3.5.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:fa4f98af3a528f0c3fd592a26df7f376f93329c8f4d987f6bb979057af8bf5e2"},
{file = "greenlet-3.5.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ec9ea74e7268ace7f9aab1b1a4e730193fc661b39a993cd91c606c32d4a3628"}, {file = "greenlet-3.5.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffea73584b216150eab159b6d12348fb253e68757974de1e2c40d8a318ac89ed"},
{file = "greenlet-3.5.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54d243512da35485fc7a6bf3c178fdda6327a9d6506fcdd62b1abd1e41b2927b"}, {file = "greenlet-3.5.1-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1072b4f9edcc1e192d9283a66a3e68d6b84c561de33a83d7858beb9ba1effe10"},
{file = "greenlet-3.5.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:41353ec2ecedf7aa8f682753a41919f8718031a6edac46b8d3dc7ed9e1ceb136"}, {file = "greenlet-3.5.1-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:89101bfd5011e069be974903cb3a4e4523845e4ece2d62dcd8d358933c0ef249"},
{file = "greenlet-3.5.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d280a7f5c331622c69f97eb167f33577ff2d1df282c41cd15907fc0a3ca198c"}, {file = "greenlet-3.5.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:add5217d68b31130f0beca584d7fef4878327d2e31642b66618a14eef312b63b"},
{file = "greenlet-3.5.0-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:58c1c374fe2b3d852f9b6b11a7dff4c85404e51b9a596fd9e89cf904eb09866d"}, {file = "greenlet-3.5.1-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:e6cd99ea59dd5d89f0c956606571d79bfe6f68c9eb7f4a4083a41a7f1587edee"},
{file = "greenlet-3.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1eb67d5adefb5bd2e182d42678a328979a209e4e82eb93575708185d31d1f588"}, {file = "greenlet-3.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a5ea42a752d47a145eae922b605cd1634665ac3d5ec1e72402d5048e8d60d207"},
{file = "greenlet-3.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2628d6c86f6cb0cb45e0c3c54058bbec559f57eaae699447748cb3928150577e"}, {file = "greenlet-3.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c5551170cf4f5ff5623e9af81323751979fee2c731e2287b61f73cd27257b823"},
{file = "greenlet-3.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:d4d9f0624c775f2dfc56ba54d515a8c771044346852a918b405914f6b19d7fd8"}, {file = "greenlet-3.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:3c8bb982ad117d29478ef8f5533e97df21f1e2befd17a299257b0c96d1371c0b"},
{file = "greenlet-3.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:83ed9f27f1680b50e89f40f6df348a290ea234b249a4003d366663a12eab94f2"}, {file = "greenlet-3.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:80eb4b04dadc4e67df3fae179a32c4706a3f495bc7f22fc8a81115d5f5512188"},
{file = "greenlet-3.5.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:5a5ed18de6a0f6cc7087f1563f6bd93fc7df1c19165ca01e9bde5a5dc281d106"}, {file = "greenlet-3.5.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:51518ff74664078fc51bffcc6fc529b0df5ae58da192691cee765d45ce944a2b"},
{file = "greenlet-3.5.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a717fbc46d8a354fa675f7c1e813485b6ba3885f9bef0cd56e5ba27d758ff5b"}, {file = "greenlet-3.5.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ffdb3c0bb002c99cd8f298957e046c3dbf6006b5b7cdf11a4e19194624a0a0a"},
{file = "greenlet-3.5.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ddc090c5c1792b10246a78e8c2163ebbe04cf877f9d785c230a7b27b39ad038e"}, {file = "greenlet-3.5.1-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7715a5a2c3378ba602c3a440558261e13a820bb53a82693aacd7b7f6d964e283"},
{file = "greenlet-3.5.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4964101b8585c144cbda5532b1aa644255126c08a265dae90c16e7a0e63aaa9d"}, {file = "greenlet-3.5.1-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d40a890035c0058cadbdc4af7569800fd28a0e527a0fdbb7b5f9418f176846ce"},
{file = "greenlet-3.5.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2094acd54b272cb6eae8c03dd87b3fa1820a4cef18d6889c378d503500a1dc13"}, {file = "greenlet-3.5.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc71ff466927a201b08305acac451ebe1aedfcea002f62f1f2f2ac2ac1e6a135"},
{file = "greenlet-3.5.0-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:7022615368890680e67b9965d33f5773aade330d5343bbe25560135aaa849eae"}, {file = "greenlet-3.5.1-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:67821bb03e4e98664490edb787ff6af501194c29bbee0f5c1dfdcf1dc3d9d436"},
{file = "greenlet-3.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5e05ba267789ea87b5a155cf0e810b1ab88bf18e9e8740813945ceb8ee4350ba"}, {file = "greenlet-3.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cd443683db272ebaaca03af98c0b063ab30db70ea8a31a1559f35e3f7b744ccd"},
{file = "greenlet-3.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0ecec963079cd58cbd14723582384f11f166fd58883c15dcbfb342e0bc9b5846"}, {file = "greenlet-3.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:089fff7a6ce8d9316d1f65ebc00273a56be258c1725b32b94de90a3a979557e1"},
{file = "greenlet-3.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:728d9667d8f2f586644b748dbd9bb67e50d6a9381767d1357714ea6825bb3bf5"}, {file = "greenlet-3.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:110a1ca7b49b014b097f6078272c3f4ed31af45b254de5228b79adba879f6af9"},
{file = "greenlet-3.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:47422135b1d308c14b2c6e758beedb1acd33bb91679f5670edf77bf46244722b"}, {file = "greenlet-3.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:f16ba1efc0715b680a18b8123d90dad887c6112ae3555b4b5c32c149540c6b4e"},
{file = "greenlet-3.5.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:f35807464c4c58c55f0d31dfa83c541a5615d825c2fe3d2b95360cf7c4e3c0a8"}, {file = "greenlet-3.5.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d8ab31c9de8651a2facdd5c5bb0011f2380dd1a7af78ce2adf4b56095294fc07"},
{file = "greenlet-3.5.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55fa7ea52771be44af0de27d8b80c02cd18c2c3cddde6c847ecebdf72418b6a1"}, {file = "greenlet-3.5.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e300185139abc337ade480c327183adf42a875ac7181bfe66d7d4efea31fbea"},
{file = "greenlet-3.5.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a97e4821aa710603f94de0da25f25096454d78ffdace5dc77f3a006bc01abba3"}, {file = "greenlet-3.5.1-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7ffdb990dcaa0234cf9845aead5df2e3c3a8b6507d409274dd87e0d5ab05ffc2"},
{file = "greenlet-3.5.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bf2d8a80bec89ab46221ae45c5373d5ba0bd36c19aa8508e85c6cd7e5106cd37"}, {file = "greenlet-3.5.1-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c09df69dc1712d131332054a858a3e5cca400967fa3a672e2324fbb0971448c"},
{file = "greenlet-3.5.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f52a464e4ed91780bdfbbdd2b97197f3accaa629b98c200f4dffada759f3ae7"}, {file = "greenlet-3.5.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2f82b3597e9d83b63408affed0b48fd0f54935edac4302237b9a837be0dae33c"},
{file = "greenlet-3.5.0-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:1bae92a1dd94c5f9d9493c3a212dd874c202442047cf96446412c862feca83a2"}, {file = "greenlet-3.5.1-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:a4764e0bfc6a4d114c865b32520805c16a990ef5f286a514413b05d5ecd6a23d"},
{file = "greenlet-3.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:762612baf1161ccb8437c0161c668a688223cba28e1bf038f4eb47b13e39ccdf"}, {file = "greenlet-3.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c0141e37414c10164e702b8fb1473304221ad98f71600850c6ef7ff4880feba0"},
{file = "greenlet-3.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:57a43c6079a89713522bc4bcb9f75070ecf5d3dbad7792bfe42239362cbf2a16"}, {file = "greenlet-3.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:50ae25a67bea74ea41fb14b960bc532df73eb713417b2d61892dced82fe8d3bc"},
{file = "greenlet-3.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:3bc59be3945ae9750b9e7d45067d01ae3fe90ea5f9ade99239dabdd6e28a5033"}, {file = "greenlet-3.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:8a17c42330e261299766b75ac1ea32caa437a9453c8f65d16a13140db378ecd3"},
{file = "greenlet-3.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:a96fcee45e03fe30a62669fd16ab5c9d3c172660d3085605cb1e2d1280d3c988"}, {file = "greenlet-3.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:7b5f5fae05b8ac6d176a61b60c394a8cbdc2b5b91b81793066e68745cf165e54"},
{file = "greenlet-3.5.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:a10a732421ab4fec934783ce3e54763470d0181db6e3468f9103a275c3ed1853"}, {file = "greenlet-3.5.1-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:ea8da1e900d758d078810d4255d8c6aa572181896a31ec79d779eb79c3adc9ad"},
{file = "greenlet-3.5.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fc391b1566f2907d17aaebe78f8855dc45675159a775fcf9e61f8ee0078e87f"}, {file = "greenlet-3.5.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a19570c52a21420dcbc94e661994bc325c0b5b11304540fed514586da5dc8f2e"},
{file = "greenlet-3.5.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:680bd0e7ad5e8daa8a4aa89f68fd6adc834b8a8036dc256533f7e08f4a4b01f7"}, {file = "greenlet-3.5.1-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3d955c89b75eeca4723d7cc14135f393cd47c32e2a6cb4a8e4c6e760a26b0986"},
{file = "greenlet-3.5.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1aa4ce8debcd4ea7fb2e150f3036588c41493d1d52c43538924ae1819003f4ce"}, {file = "greenlet-3.5.1-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ea37d5a157eb9493820d3792ac4ece28619a394391d2b9f2f78057d396ff0f0f"},
{file = "greenlet-3.5.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddb36c7d6c9c0a65f18c7258634e0c416c6ab59caac8c987b96f80c2ebda0112"}, {file = "greenlet-3.5.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2daaaebd1a5aa88c49045b6baf9310b3263796bd88db713edf37cf53e7bb4e"},
{file = "greenlet-3.5.0-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:728a73687e39ae9ca34e4694cbf2f049d3fbc7174639468d0f67200a97d8f9e2"}, {file = "greenlet-3.5.1-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:8d8a23250ea3ec7b36de8fa4b541e9e2db3ee82915cc060ab0631609ad8b28de"},
{file = "greenlet-3.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e5ddf316ced87539144621453c3aef229575825fe60c604e62bedc4003f372b2"}, {file = "greenlet-3.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3bfbd69cc349e43bf3a8ae1c85548ff0718efc887615c2db16c3833d7b0b072d"},
{file = "greenlet-3.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4a448128607be0de65342dc9b31be7f948ef4cc0bc8832069350abefd310a8f2"}, {file = "greenlet-3.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4378720dd888136c27215a0214d32a4d37c3852765d45bc37aad0623423cfd78"},
{file = "greenlet-3.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d60097128cb0a1cab9ea541186ea13cd7b847b8449a7787c2e2350da0cb82d86"}, {file = "greenlet-3.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:45718441607f9325d948db98cbc691276059316d0358c188c246da4e1d4d23d2"},
{file = "greenlet-3.5.0.tar.gz", hash = "sha256:d419647372241bc68e957bf38d5c1f98852155e4146bd1e4121adea81f4f01e4"}, {file = "greenlet-3.5.1-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:2baee5ca02031757ffe8cc3d69f0cc0aec7065ce362622da74f32d3bcab1c541"},
{file = "greenlet-3.5.1-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b1ec3274918a81d3ea778b9e75b56b72b33f300edb6cf7f3a7fe1dae56683de"},
{file = "greenlet-3.5.1-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:111e2390ffffc47d5840b01711dd7fac07d4c09283d0283e7f3264b14e284c64"},
{file = "greenlet-3.5.1-cp315-cp315-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:10a9a1c0bfbc93d41156ffcb90c75fbc05544054faf15dcc1fdf9765f8b607f0"},
{file = "greenlet-3.5.1-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e630136e905fe5ff43e86945ae41220b6d1470956a39220e708110ac48d01ea5"},
{file = "greenlet-3.5.1-cp315-cp315-manylinux_2_39_riscv64.whl", hash = "sha256:ef08c1567c78074b22d1a200183d52d04a14df447bf70bcbb6a3507a48e776fc"},
{file = "greenlet-3.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:975eac34b44a7077ca4d421348455b94f0f518246a7f14bc6d2fdcfe5b584368"},
{file = "greenlet-3.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:9ab3c3a0b2ae6198e67c898dad5215a49f9ae0d0081b3c3ec59f333e39eeca26"},
{file = "greenlet-3.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:cbfc69be86e10dcfef5b1e6269d1d6926552aa89ee39e1de3353360c1b6989ab"},
{file = "greenlet-3.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:92fd6d44ac5e5a887c8a5dc4a8ba0ba908527c31c12f78c6bc7dcfe8aab279f6"},
{file = "greenlet-3.5.1-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:a6fdf2433a5441ef9a95464f7c3e674775da1c8c1177fff311cee1acad4626ed"},
{file = "greenlet-3.5.1-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7546556f0d649f99f6a361098a55f761181bb2ea12ff150bb16d26092ad88244"},
{file = "greenlet-3.5.1-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d5ee3ea898009fa898f85f9982255d35278c477bebe185beca249cab42d4526c"},
{file = "greenlet-3.5.1-cp315-cp315t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a57b0d05a0448eed231d59c0ceb287dde984551e54cbc51ac2d4865712838e9c"},
{file = "greenlet-3.5.1-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5c81f74d204d3edd136ebfd50dce53acbb776995d721a0fe801626cfc93b8cd"},
{file = "greenlet-3.5.1-cp315-cp315t-manylinux_2_39_riscv64.whl", hash = "sha256:b0703c2cef53e01baec47f7a3868009913ad71ec678bbecb42a6f40895e4ce62"},
{file = "greenlet-3.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:2c18ef16bf6d4dd410e4dd52996888ea1497be26892fe5bbc73580aba4287b8e"},
{file = "greenlet-3.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:17d86354f0ae6b61bf9be5148d0dd34e06c3cb7c602c671f79f29ac3b150e659"},
{file = "greenlet-3.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:e7516cf6ae6b8a582c2770a0caed47b8a48373ed732c33d69a72913ae6ac923e"},
{file = "greenlet-3.5.1-cp315-cp315t-win_arm64.whl", hash = "sha256:5028648bf2253ec4745add746129d3904121fa7fe871a76bed23c5720573ce0a"},
{file = "greenlet-3.5.1.tar.gz", hash = "sha256:5a56aeb7d5d9cc4b3a735efb5095bd4b4f6f0e4f93e5ca876d0e2315137b7829"},
] ]
[package.extras] [package.extras]
@@ -635,14 +655,14 @@ tornado = ["tornado (>=6.5.0)"]
[[package]] [[package]]
name = "idna" name = "idna"
version = "3.14" version = "3.16"
description = "Internationalized Domain Names in Applications (IDNA)" description = "Internationalized Domain Names in Applications (IDNA)"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.9"
groups = ["dev"] groups = ["dev"]
files = [ files = [
{file = "idna-3.14-py3-none-any.whl", hash = "sha256:e677eaf072e290f7b725f9acf0b3a2bd55f9fd6f7c70abe5f0e34823d0accf69"}, {file = "idna-3.16-py3-none-any.whl", hash = "sha256:cc246e3a3f89580c3a951b5ad298ca4638078b2cdd4f115654332b5c26daded5"},
{file = "idna-3.14.tar.gz", hash = "sha256:466d810d7a2cc1022bea9b037c39728d51ae7dad40d480fc9b7d7ecf98ba8ee3"}, {file = "idna-3.16.tar.gz", hash = "sha256:d7a6da03db833450fca25d2358ac9ff06cd624577a4aea3a596d5c0f77b8e03d"},
] ]
[package.extras] [package.extras]
@@ -704,7 +724,7 @@ files = [
[package.dependencies] [package.dependencies]
attrs = ">=22.2.0" attrs = ">=22.2.0"
jsonschema-specifications = ">=2023.3.6" jsonschema-specifications = ">=2023.03.6"
referencing = ">=0.28.4" referencing = ">=0.28.4"
rpds-py = ">=0.25.0" rpds-py = ">=0.25.0"
@@ -1150,143 +1170,143 @@ files = [
[[package]] [[package]]
name = "numpy" name = "numpy"
version = "2.4.4" version = "2.4.6"
description = "Fundamental package for array computing in Python" description = "Fundamental package for array computing in Python"
optional = false optional = false
python-versions = ">=3.11" python-versions = ">=3.11"
groups = ["pi"] groups = ["pi"]
files = [ files = [
{file = "numpy-2.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f983334aea213c99992053ede6168500e5f086ce74fbc4acc3f2b00f5762e9db"}, {file = "numpy-2.4.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0280e0356c0829a18d9de1cb7eee50ec22ca639878d7240307ca0943d73cd2c4"},
{file = "numpy-2.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:72944b19f2324114e9dc86a159787333b77874143efcf89a5167ef83cfee8af0"}, {file = "numpy-2.4.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:110f8b71aacb688ec69062bb7f6938a0f8acb01b7c1c4beb453c65b6d234584d"},
{file = "numpy-2.4.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:86b6f55f5a352b48d7fbfd2dbc3d5b780b2d79f4d3c121f33eb6efb22e9a2015"}, {file = "numpy-2.4.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:4cfe66903cc32a9921a6733d96b19bb6abf310397581bbad89c228f5abaf0ee8"},
{file = "numpy-2.4.4-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:ba1f4fc670ed79f876f70082eff4f9583c15fb9a4b89d6188412de4d18ae2f40"}, {file = "numpy-2.4.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8155154c7c691289fe18f510b5d4657c68c67989f293f0535a91360392ff6538"},
{file = "numpy-2.4.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a87ec22c87be071b6bdbd27920b129b94f2fc964358ce38f3822635a3e2e03d"}, {file = "numpy-2.4.6-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ab0a9c4ffb1a6d95ef519fe4247dba8eb6b18ad93999f76b7f657039acabd47"},
{file = "numpy-2.4.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df3775294accfdd75f32c74ae39fcba920c9a378a2fc18a12b6820aa8c1fb502"}, {file = "numpy-2.4.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cd468399cfd2504718f0ba50e410dca55a170b61a02ad92bb18c8a65186e93"},
{file = "numpy-2.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0d4e437e295f18ec29bc79daf55e8a47a9113df44d66f702f02a293d93a2d6dd"}, {file = "numpy-2.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2d37ab77531417474168eb79d6d80b14f821a966818505d03013d0833edb7a8"},
{file = "numpy-2.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6aa3236c78803afbcb255045fbef97a9e25a1f6c9888357d205ddc42f4d6eba5"}, {file = "numpy-2.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f407cb6b8e9d6d8c626bc73c945db1706035af8fd632295547bf1c9e46d092d6"},
{file = "numpy-2.4.4-cp311-cp311-win32.whl", hash = "sha256:30caa73029a225b2d40d9fae193e008e24b2026b7ee1a867b7ee8d96ca1a448e"}, {file = "numpy-2.4.6-cp311-cp311-win32.whl", hash = "sha256:ddea102b48f9e339f3948bf22040944184627a30fdf7f858667673b9c5f033c8"},
{file = "numpy-2.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:6bbe4eb67390b0a0265a2c25458f6b90a409d5d069f1041e6aff1e27e3d9a79e"}, {file = "numpy-2.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:1e254a00cdf42b1e4d5b3d68d33af63268d41340d8885df2ab6470f2e1500147"},
{file = "numpy-2.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:fcfe2045fd2e8f3cb0ce9d4ba6dba6333b8fa05bb8a4939c908cd43322d14c7e"}, {file = "numpy-2.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:ed9749eef4cbd126da3dc1d6bcb3a57f5eb7ac6a6484146bdbf743f552dfc577"},
{file = "numpy-2.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15716cfef24d3a9762e3acdf87e27f58dc823d1348f765bbea6bef8c639bfa1b"}, {file = "numpy-2.4.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1"},
{file = "numpy-2.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23cbfd4c17357c81021f21540da84ee282b9c8fba38a03b7b9d09ba6b951421e"}, {file = "numpy-2.4.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb"},
{file = "numpy-2.4.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8b3b60bb7cba2c8c81837661c488637eee696f59a877788a396d33150c35d842"}, {file = "numpy-2.4.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41"},
{file = "numpy-2.4.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e4a010c27ff6f210ff4c6ef34394cd61470d01014439b192ec22552ee867f2a8"}, {file = "numpy-2.4.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698"},
{file = "numpy-2.4.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9e75681b59ddaa5e659898085ae0eaea229d054f2ac0c7e563a62205a700121"}, {file = "numpy-2.4.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f"},
{file = "numpy-2.4.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:81f4a14bee47aec54f883e0cad2d73986640c1590eb9bfaaba7ad17394481e6e"}, {file = "numpy-2.4.6-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853"},
{file = "numpy-2.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:62d6b0f03b694173f9fcb1fb317f7222fd0b0b103e784c6549f5e53a27718c44"}, {file = "numpy-2.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a"},
{file = "numpy-2.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fbc356aae7adf9e6336d336b9c8111d390a05df88f1805573ebb0807bd06fd1d"}, {file = "numpy-2.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2"},
{file = "numpy-2.4.4-cp312-cp312-win32.whl", hash = "sha256:0d35aea54ad1d420c812bfa0385c71cd7cc5bcf7c65fed95fc2cd02fe8c79827"}, {file = "numpy-2.4.6-cp312-cp312-win32.whl", hash = "sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45"},
{file = "numpy-2.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:b5f0362dc928a6ecd9db58868fca5e48485205e3855957bdedea308f8672ea4a"}, {file = "numpy-2.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751"},
{file = "numpy-2.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:846300f379b5b12cc769334464656bc882e0735d27d9726568bc932fdc49d5ec"}, {file = "numpy-2.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8"},
{file = "numpy-2.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:08f2e31ed5e6f04b118e49821397f12767934cfdd12a1ce86a058f91e004ee50"}, {file = "numpy-2.4.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0"},
{file = "numpy-2.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e823b8b6edc81e747526f70f71a9c0a07ac4e7ad13020aa736bb7c9d67196115"}, {file = "numpy-2.4.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb"},
{file = "numpy-2.4.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4a19d9dba1a76618dd86b164d608566f393f8ec6ac7c44f0cc879011c45e65af"}, {file = "numpy-2.4.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f"},
{file = "numpy-2.4.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d2a8490669bfe99a233298348acc2d824d496dee0e66e31b66a6022c2ad74a5c"}, {file = "numpy-2.4.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3"},
{file = "numpy-2.4.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45dbed2ab436a9e826e302fcdcbe9133f9b0006e5af7168afb8963a6520da103"}, {file = "numpy-2.4.6-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b"},
{file = "numpy-2.4.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c901b15172510173f5cb310eae652908340f8dede90fff9e3bf6c0d8dfd92f83"}, {file = "numpy-2.4.6-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089"},
{file = "numpy-2.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:99d838547ace2c4aace6c4f76e879ddfe02bb58a80c1549928477862b7a6d6ed"}, {file = "numpy-2.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a"},
{file = "numpy-2.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0aec54fd785890ecca25a6003fd9a5aed47ad607bbac5cd64f836ad8666f4959"}, {file = "numpy-2.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605"},
{file = "numpy-2.4.4-cp313-cp313-win32.whl", hash = "sha256:07077278157d02f65c43b1b26a3886bce886f95d20aabd11f87932750dfb14ed"}, {file = "numpy-2.4.6-cp313-cp313-win32.whl", hash = "sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91"},
{file = "numpy-2.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:5c70f1cc1c4efbe316a572e2d8b9b9cc44e89b95f79ca3331553fbb63716e2bf"}, {file = "numpy-2.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359"},
{file = "numpy-2.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:ef4059d6e5152fa1a39f888e344c73fdc926e1b2dd58c771d67b0acfbf2aa67d"}, {file = "numpy-2.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778"},
{file = "numpy-2.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4bbc7f303d125971f60ec0aaad5e12c62d0d2c925f0ab1273debd0e4ba37aba5"}, {file = "numpy-2.4.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1"},
{file = "numpy-2.4.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:4d6d57903571f86180eb98f8f0c839fa9ebbfb031356d87f1361be91e433f5b7"}, {file = "numpy-2.4.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe"},
{file = "numpy-2.4.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:4636de7fd195197b7535f231b5de9e4b36d2c440b6e566d2e4e4746e6af0ca93"}, {file = "numpy-2.4.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997"},
{file = "numpy-2.4.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad2e2ef14e0b04e544ea2fa0a36463f847f113d314aa02e5b402fdf910ef309e"}, {file = "numpy-2.4.6-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20"},
{file = "numpy-2.4.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a285b3b96f951841799528cd1f4f01cd70e7e0204b4abebac9463eecfcf2a40"}, {file = "numpy-2.4.6-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d"},
{file = "numpy-2.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f8474c4241bc18b750be2abea9d7a9ec84f46ef861dbacf86a4f6e043401f79e"}, {file = "numpy-2.4.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67"},
{file = "numpy-2.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4e874c976154687c1f71715b034739b45c7711bec81db01914770373d125e392"}, {file = "numpy-2.4.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd"},
{file = "numpy-2.4.4-cp313-cp313t-win32.whl", hash = "sha256:9c585a1790d5436a5374bac930dad6ed244c046ed91b2b2a3634eb2971d21008"}, {file = "numpy-2.4.6-cp313-cp313t-win32.whl", hash = "sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab"},
{file = "numpy-2.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:93e15038125dc1e5345d9b5b68aa7f996ec33b98118d18c6ca0d0b7d6198b7e8"}, {file = "numpy-2.4.6-cp313-cp313t-win_amd64.whl", hash = "sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75"},
{file = "numpy-2.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:0dfd3f9d3adbe2920b68b5cd3d51444e13a10792ec7154cd0a2f6e74d4ab3233"}, {file = "numpy-2.4.6-cp313-cp313t-win_arm64.whl", hash = "sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd"},
{file = "numpy-2.4.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f169b9a863d34f5d11b8698ead99febeaa17a13ca044961aa8e2662a6c7766a0"}, {file = "numpy-2.4.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079"},
{file = "numpy-2.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2483e4584a1cb3092da4470b38866634bafb223cbcd551ee047633fd2584599a"}, {file = "numpy-2.4.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7"},
{file = "numpy-2.4.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:2d19e6e2095506d1736b7d80595e0f252d76b89f5e715c35e06e937679ea7d7a"}, {file = "numpy-2.4.6-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5"},
{file = "numpy-2.4.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:6a246d5914aa1c820c9443ddcee9c02bec3e203b0c080349533fae17727dfd1b"}, {file = "numpy-2.4.6-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096"},
{file = "numpy-2.4.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:989824e9faf85f96ec9c7761cd8d29c531ad857bfa1daa930cba85baaecf1a9a"}, {file = "numpy-2.4.6-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b"},
{file = "numpy-2.4.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27a8d92cd10f1382a67d7cf4db7ce18341b66438bdd9f691d7b0e48d104c2a9d"}, {file = "numpy-2.4.6-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8"},
{file = "numpy-2.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e44319a2953c738205bf3354537979eaa3998ed673395b964c1176083dd46252"}, {file = "numpy-2.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402"},
{file = "numpy-2.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e892aff75639bbef0d2a2cfd55535510df26ff92f63c92cd84ef8d4ba5a5557f"}, {file = "numpy-2.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb"},
{file = "numpy-2.4.4-cp314-cp314-win32.whl", hash = "sha256:1378871da56ca8943c2ba674530924bb8ca40cd228358a3b5f302ad60cf875fc"}, {file = "numpy-2.4.6-cp314-cp314-win32.whl", hash = "sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1"},
{file = "numpy-2.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:715d1c092715954784bc79e1174fc2a90093dc4dc84ea15eb14dad8abdcdeb74"}, {file = "numpy-2.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261"},
{file = "numpy-2.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:2c194dd721e54ecad9ad387c1d35e63dce5c4450c6dc7dd5611283dda239aabb"}, {file = "numpy-2.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6"},
{file = "numpy-2.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2aa0613a5177c264ff5921051a5719d20095ea586ca88cc802c5c218d1c67d3e"}, {file = "numpy-2.4.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a"},
{file = "numpy-2.4.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:42c16925aa5a02362f986765f9ebabf20de75cdefdca827d14315c568dcab113"}, {file = "numpy-2.4.6-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e"},
{file = "numpy-2.4.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:874f200b2a981c647340f841730fc3a2b54c9d940566a3c4149099591e2c4c3d"}, {file = "numpy-2.4.6-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e"},
{file = "numpy-2.4.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b39d38a9bd2ae1becd7eac1303d031c5c110ad31f2b319c6e7d98b135c934d"}, {file = "numpy-2.4.6-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43"},
{file = "numpy-2.4.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b268594bccac7d7cf5844c7732e3f20c50921d94e36d7ec9b79e9857694b1b2f"}, {file = "numpy-2.4.6-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e"},
{file = "numpy-2.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac6b31e35612a26483e20750126d30d0941f949426974cace8e6b5c58a3657b0"}, {file = "numpy-2.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895"},
{file = "numpy-2.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8e3ed142f2728df44263aaf5fb1f5b0b99f4070c553a0d7f033be65338329150"}, {file = "numpy-2.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4"},
{file = "numpy-2.4.4-cp314-cp314t-win32.whl", hash = "sha256:dddbbd259598d7240b18c9d87c56a9d2fb3b02fe266f49a7c101532e78c1d871"}, {file = "numpy-2.4.6-cp314-cp314t-win32.whl", hash = "sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063"},
{file = "numpy-2.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a7164afb23be6e37ad90b2f10426149fd75aee07ca55653d2aa41e66c4ef697e"}, {file = "numpy-2.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627"},
{file = "numpy-2.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:ba203255017337d39f89bdd58417f03c4426f12beed0440cfd933cb15f8669c7"}, {file = "numpy-2.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66"},
{file = "numpy-2.4.4-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:58c8b5929fcb8287cbd6f0a3fae19c6e03a5c48402ae792962ac465224a629a4"}, {file = "numpy-2.4.6-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:55cced7c52e981362f708ad635198e97a752dfba412cc03c23bbf3bd8d5cd662"},
{file = "numpy-2.4.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:eea7ac5d2dce4189771cedb559c738a71512768210dc4e4753b107a2048b3d0e"}, {file = "numpy-2.4.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d6da64deb6b8ed903e7560180a92f2d804ee1ba5eeb849ac2748b8c1aba1f6d7"},
{file = "numpy-2.4.4-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:51fc224f7ca4d92656d5a5eb315f12eb5fe2c97a66249aa7b5f562528a3be38c"}, {file = "numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:68a5124b13fa6cc2086764a20005d30bc0548146f7f5322f02fce212ca14317f"},
{file = "numpy-2.4.4-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:28a650663f7314afc3e6ec620f44f333c386aad9f6fc472030865dc0ebb26ee3"}, {file = "numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:948424b06129ce883307e8cff868c31396d8dc7630a59c61d70d98dbe70f222c"},
{file = "numpy-2.4.4-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19710a9ca9992d7174e9c52f643d4272dcd1558c5f7af7f6f8190f633bd651a7"}, {file = "numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbbdb29840ca3d91ee0fece42fc29278886d908280bfec0a5846c6f901a3eb0"},
{file = "numpy-2.4.4-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b2aec6af35c113b05695ebb5749a787acd63cafc83086a05771d1e1cd1e555f"}, {file = "numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ad03c0965fb3c692200e74d458ca28c1dbb4ce96f9a479a8aa041ad5fabca02"},
{file = "numpy-2.4.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f2cf083b324a467e1ab358c105f6cad5ea950f50524668a80c486ff1db24e119"}, {file = "numpy-2.4.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2803abfebfc990042cd494d8ce2d5f82e9d847af6d35ec486923aa19dbad5e73"},
{file = "numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0"}, {file = "numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda"},
] ]
[[package]] [[package]]
name = "openexr" name = "openexr"
version = "3.4.11" version = "3.4.12"
description = "Python bindings for the OpenEXR image file format" description = "Python bindings for the OpenEXR image file format"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
groups = ["pi"] groups = ["pi"]
files = [ files = [
{file = "openexr-3.4.11-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:677a6988dc2dca7756b660f9c9dfbfa4497552f9b90285df0f9cd5b780bda901"}, {file = "openexr-3.4.12-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:85108cc4285996449ac43c952264b318c7062c18ad22da6125a1d77978057448"},
{file = "openexr-3.4.11-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:e241db758acc7db68e07034d274545b1a05f5130b6d4702eda68a0b4f7e0452f"}, {file = "openexr-3.4.12-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:68bc8c0e2ef6a451b6321b6005d4698fa4260f762495dd404b3691dbd01b394c"},
{file = "openexr-3.4.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7945e9e954405574a40e6e3930c35295287c849c7a78679b39614d4d138383b3"}, {file = "openexr-3.4.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:06c1efcc9d1c6af354bc3c7c14114096814e8074830d78f64b7ec44033374e9b"},
{file = "openexr-3.4.11-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f183257d1c7f80525195226e1627b7132e8ed7628edd0d11c81fec130a7f06be"}, {file = "openexr-3.4.12-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d0a9158bff791442a710b5742e1cef431f68fadcdc6fc37dde9db24175b3cfa2"},
{file = "openexr-3.4.11-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1502e90b2e2cc4134846e208a9a3a9b3cfca886c681a759d68b0c2c0fbfcb526"}, {file = "openexr-3.4.12-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8a1760bfb3ca2f71a9c99f4cf13392457c54d60d61eb7b3b64a7b876be2df7d6"},
{file = "openexr-3.4.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b3effb270004b8af068426f223edb2009909a51d2970da3b0576a2db09339377"}, {file = "openexr-3.4.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3c082f5c16eadd2b9bbdc3bb812f257a33ef14f2752bbaec2eaeeac34887080e"},
{file = "openexr-3.4.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4e5341501f889e8b0d768c677645eedf949847ae82dac027b414157a866812b6"}, {file = "openexr-3.4.12-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:52eaf6805a092d90a49a89b909cc758c6da3bda888eee46bb8ce0c7c2444873c"},
{file = "openexr-3.4.11-cp310-cp310-win_amd64.whl", hash = "sha256:47ef1dc8e75ba33af6cb4b2880e006ec6e27911d0f9c4a477c4501e7f4994c61"}, {file = "openexr-3.4.12-cp310-cp310-win_amd64.whl", hash = "sha256:6ce8e8b430576ba3d274beef50753c5a3ebcd646ea3db2b855ed4a0a2a3bcf11"},
{file = "openexr-3.4.11-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:502f8d2e32928a475316730569fa9fcfd22d4258d3f40afe253db4fcd258f02e"}, {file = "openexr-3.4.12-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:eb0aa7036a5c1b022073ad1955780337a381f4fd3d7fc85b31263090240e7102"},
{file = "openexr-3.4.11-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:e64b2ad85f14897580020eb2f5519e7b1ba96892c468934e65edb3bebc66cb68"}, {file = "openexr-3.4.12-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:5c3c689c7973cc55621cf835314b7437517fef54f20046b8a594adce1b14e1e4"},
{file = "openexr-3.4.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c0566a70859a6f237b25034b83b832a6c67a63ea2224094f54b208d1462ca749"}, {file = "openexr-3.4.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:983ab3a11ca74bae35d298c09cfa979f16646cd950fc4defa5620cf6ef9c58c9"},
{file = "openexr-3.4.11-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bc04f405f428b511fda8403ae22443ae1f6bcca98256cff49b5bca817e8c89dd"}, {file = "openexr-3.4.12-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2417aad023c4946d0c29bbb98fe605504b83842081bdfe0a7f1fb190202190a1"},
{file = "openexr-3.4.11-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cffbb450c121cdf026e9405c590e9b70d5ca1b519da51cd955f1c7541dc5ac2f"}, {file = "openexr-3.4.12-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4e7b52c1a999ec7c1a32ad01197ee4a41c0b81924fdad105b684ff06bfef4e67"},
{file = "openexr-3.4.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fe8493a34d08b9169445aef13dcfeded0285a038b8c3f125bec9fe606f1b3b25"}, {file = "openexr-3.4.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0c312a67896a874a07c87f7098da139d47406e5ceca6915282eec9e252ce3449"},
{file = "openexr-3.4.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5dd631d7df0838073a65f2e52a6333e448cc60dadcca57df33186cc6140080df"}, {file = "openexr-3.4.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b736108b27aa3acd6f52d523d596627bc1e7366053d292442769d4ee4251ed2"},
{file = "openexr-3.4.11-cp311-cp311-win_amd64.whl", hash = "sha256:6c5a791e9835e3bc1c58e054ea5402d4fda371a1c1aacd75f9580e52d0f39455"}, {file = "openexr-3.4.12-cp311-cp311-win_amd64.whl", hash = "sha256:ed544f1eced73568d15a00140b94d502e4e5b7f7f2ec142269aeb45338596f09"},
{file = "openexr-3.4.11-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:c11187b2b7818abcd4caae98939a192409de7032900305540f124ab02a122f2d"}, {file = "openexr-3.4.12-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:efab1439a2d160837b68aa26053d6145e85cc326f086f5f62ecd05946cf769a5"},
{file = "openexr-3.4.11-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:d0faef97e93a539a7de59b247c22b19037f3adbf17e61790f61cfd1ca631e68a"}, {file = "openexr-3.4.12-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:f306aca0d2dd5fa157d2cb0f5e1fdd8ee9e126fc85bd31a34ff39533d1943b05"},
{file = "openexr-3.4.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9a16d54d1fb65cefd73189c7d7f2437385563a297129fefcae3601b31704b8ac"}, {file = "openexr-3.4.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b975e643a790e0e4f9f3de3dd9210a51c1c65b555ed4f7653f059fc32675fa0a"},
{file = "openexr-3.4.11-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a5baebe08bcf294feb50ceae07456302e3d87886b27cafcabe2e81dce3dc1214"}, {file = "openexr-3.4.12-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80f75a668b2fe66d961a234157245d98f4172837a339d0ce10d97663738a5d00"},
{file = "openexr-3.4.11-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6b5f90d8ca262119e1ca626ee9ccc92bd1859cff38c19e3ba14c0353ec1352f9"}, {file = "openexr-3.4.12-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:222aca5d3526910b6a75b522d9dce5946d64159aa016472353b912c33cb1cd59"},
{file = "openexr-3.4.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c90d565b37c53e9b644ef454a02642cea6e3b3f22de7f638f4346cefd782b9ca"}, {file = "openexr-3.4.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3a01ecb034caddde76191944b7965b48179d635ac4cc3bdbf33034b45153a4b4"},
{file = "openexr-3.4.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:26a9fe29d65aa7e7086e9d89f3c416cc66c0fb2b211b3b98d1df95b25c276b5a"}, {file = "openexr-3.4.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fa3f711c2f8683de7c8150fe6038b5bd4729430bc308826e406b64b718fc86f3"},
{file = "openexr-3.4.11-cp312-cp312-win_amd64.whl", hash = "sha256:65a3a5d456ce33f79fdaba5c9b5e28837164b6e672f3866bbd8c940c69abc1fb"}, {file = "openexr-3.4.12-cp312-cp312-win_amd64.whl", hash = "sha256:8a2eaed1d869fd6c2cecacb4807e8482a5ee30ac39a6d33373ab4718c5e171b3"},
{file = "openexr-3.4.11-cp313-cp313-macosx_10_15_universal2.whl", hash = "sha256:fb1ef1a5ba9293efbfc5ceda38ed45847b484bfbf21391d5401f61a613e55b16"}, {file = "openexr-3.4.12-cp313-cp313-macosx_10_15_universal2.whl", hash = "sha256:3031ed47d3a579d3fa6998d0a6dad22012ce9fd6a33485fa5339ca4a079d0665"},
{file = "openexr-3.4.11-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:911d73a84178ade1b7757c262da62a8a45fa241502a83f3af6c84c89c518c4f7"}, {file = "openexr-3.4.12-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:059f666c43a5d255abc0f9d6f87f2c5ca6ba462fc0442316a672e0ecbcf6a4a9"},
{file = "openexr-3.4.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ea1e90291a48515f3079789be13b23a0976ba3b8522dd71a812f419d19d4246a"}, {file = "openexr-3.4.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:553f4267221490e846b7f63edcca807fac1757cd242a49063f048fd0e757029c"},
{file = "openexr-3.4.11-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c28ccb6c69374c7834600bbb43546949f2e670268eca400703535d89e0da3e6"}, {file = "openexr-3.4.12-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4dc03aa88a57d47c49780bee40c4f0febabf8ed150e2bd60e55f3ee6bea62493"},
{file = "openexr-3.4.11-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:41deb09f525fb3ad7cb9f114953f7343cf28ae65ac3b7fdd9a3f3429c590ad19"}, {file = "openexr-3.4.12-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8301110402427ea48073ab11556873c0ba3dc8d1d6fdc79fd77a7738a50eeb11"},
{file = "openexr-3.4.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:168a213f9c5ecda7e9fe44f4f05efb5516a19151ac55274e3ceda930535c3ca7"}, {file = "openexr-3.4.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b8be14c4794f4ad19112fbbed7767c5bf6216435f27cb42a499cdab0f3d26562"},
{file = "openexr-3.4.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5a40912694f33e60a4991f4d26a046350eede26197b216ed33983a400326195c"}, {file = "openexr-3.4.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8feae6511257a2a33aa067c247a06b82e31bbbadfafe82a14ab5a7288a93b16b"},
{file = "openexr-3.4.11-cp313-cp313-win_amd64.whl", hash = "sha256:758844e0f77eab48d3e1accb912ec5cb9c726af6fb0b8812c141e1eedcf91be0"}, {file = "openexr-3.4.12-cp313-cp313-win_amd64.whl", hash = "sha256:19f6ac86915cd93ee42b615702477435d9fbbdd9da217c3f70d8a04586240759"},
{file = "openexr-3.4.11-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:8c0c53a901eff9bbcd97a9a2b20ea882b23f17dd0579a8d93490c5b13bc48536"}, {file = "openexr-3.4.12-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:5a7c007fdb33d4c1aa2fa02e709df23635a82d656cde2d78574659b6a47d0a8c"},
{file = "openexr-3.4.11-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:3ed4a9951f6e77bf7d28f3ec1b7de1e60ae7607af7a2201d0d4d97bb4a3cbc32"}, {file = "openexr-3.4.12-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:3db04ec227efbda710c38b60f53a8a06897ba2f24664911f54fd79367ee1a540"},
{file = "openexr-3.4.11-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fc41450bce5a25030564d865f73815de4db3ee25e64414b728b6ce6c15bd963a"}, {file = "openexr-3.4.12-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:dcf1eb54fb91887664ac03fee187ccbe542a95ac29ddba4c141d84496b891af8"},
{file = "openexr-3.4.11-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c0d8495cba2e3cac262d8e5a3f478fdf98cb79b3a294c48709e7734fe7110b60"}, {file = "openexr-3.4.12-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b6f6284d7c41611a92845a9fb47c930a80c4f726cef198aa6b3e501ac47ef3d4"},
{file = "openexr-3.4.11-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d7cf1fd0ccef5baf25a07fda0a340e0536bb87f584172125343da2678a4252da"}, {file = "openexr-3.4.12-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6f988539c6704acd9a4167afe10bda77c3c6d87067f159e592ffda2d934a0264"},
{file = "openexr-3.4.11-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:8abd9216e65b59629f946a89e97cb3eadbf31c11b306bc32438c7abce2aa6438"}, {file = "openexr-3.4.12-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:3a4c9cf3280107b0a264f55c19e3577aa8ae33dcc3c34d0741a0f74bd6fd4ff4"},
{file = "openexr-3.4.11-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:65d50df2bb85fd51c9dae8aac6e1c4bbef1c5d0a28934996186a24327ad0e349"}, {file = "openexr-3.4.12-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:af9971ee6a28f6f4ad39c8fa349dd36bf8d2d8aa9678b040b6e42fe34fcd175f"},
{file = "openexr-3.4.11-cp38-cp38-win_amd64.whl", hash = "sha256:7e57e3f2e9a573f23f3a2d9036dc58bb8eef27c04bbe4af1585a3c462700af5b"}, {file = "openexr-3.4.12-cp38-cp38-win_amd64.whl", hash = "sha256:399e2fb0747f52045cdb1bb5572ea337e2c059830cc3fe6487009df5cbb29d99"},
{file = "openexr-3.4.11-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:cb6a7f7eb30b511d0135d2f03c233a78248680b11e63a11c42432d2615c577f0"}, {file = "openexr-3.4.12-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:41b431606600cce78c18d9874dcf2c1806835e49dfec959d0f7674fa786ee5a2"},
{file = "openexr-3.4.11-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:7717c323030ebcf7976c50889cc83fe44b6e00b1d7c53e68a4e859ce426494d1"}, {file = "openexr-3.4.12-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:098bc33f47f2ccf54f462789cfebf2954b2a9c0544f1821276a0c1c7ade7b9f8"},
{file = "openexr-3.4.11-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:be6fa46e0b93a2d419e01c0862a73f3c4051a660381200a13566fa37fd098d50"}, {file = "openexr-3.4.12-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:106f8f36207543dcc9bb60e77e072b8fb3a5eb5a7dc82028dba2f9f90fdd50a5"},
{file = "openexr-3.4.11-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:757461bd5fede940c08d8c44b54529e573f87e521923bc860fc5bcbac8365d08"}, {file = "openexr-3.4.12-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4e4c6c39d693a7bd30bb37b7408aa720ceebf7352ee0b91be9f79e3664ad97c1"},
{file = "openexr-3.4.11-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5e9a181377af663161323bdf4907e50bc4dcf918dc793989da9c3d7ccc39ca1a"}, {file = "openexr-3.4.12-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:97170ba4d09bf5b2718f13bc362372006d26b547c5090ad6507715f2ef43ef4d"},
{file = "openexr-3.4.11-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:682f515547588aa9834a77f9beebb617378f55013f47f0dd3388cda4028ab57e"}, {file = "openexr-3.4.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:71dc88981fa83778c6a51e7fe4580da2cd2320bd85153f2c761577db215f3494"},
{file = "openexr-3.4.11-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3a35e8c545309d7d7b527b14a1c37561f2c6aab33b26f4402b5c29f7f82b04c3"}, {file = "openexr-3.4.12-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7a8e2c2b43f1b1c730e72e66674b7d645b64808e18bf6daf50fcbb7b4491b8fc"},
{file = "openexr-3.4.11-cp39-cp39-win_amd64.whl", hash = "sha256:4dd1e3821848d0a5d0a5f6cb72f38b50a1ecb2c85c5ebc62d62c0a1df99ad4f2"}, {file = "openexr-3.4.12-cp39-cp39-win_amd64.whl", hash = "sha256:d9767fd77a3401da3224999b0952053b9784416d0d91f223ea0c3dc8e441c664"},
{file = "openexr-3.4.11.tar.gz", hash = "sha256:c32233f4ffea51ca3fe05a3f0193b24bf21d7cab04a46c292badc7c9dfa60851"}, {file = "openexr-3.4.12.tar.gz", hash = "sha256:877da800b30146e5e29851da2a80147883244966f5b2e932e04f1f1a06ff4fc7"},
] ]
[package.dependencies] [package.dependencies]
@@ -1659,20 +1679,20 @@ diagrams = ["jinja2", "railroad-diagrams"]
[[package]] [[package]]
name = "pytest" name = "pytest"
version = "8.4.2" version = "9.0.3"
description = "pytest: simple powerful testing with Python" description = "pytest: simple powerful testing with Python"
optional = false optional = false
python-versions = ">=3.9" python-versions = ">=3.10"
groups = ["dev"] groups = ["dev"]
files = [ files = [
{file = "pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79"}, {file = "pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9"},
{file = "pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01"}, {file = "pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c"},
] ]
[package.dependencies] [package.dependencies]
colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""}
iniconfig = ">=1" iniconfig = ">=1.0.1"
packaging = ">=20" packaging = ">=22"
pluggy = ">=1.5,<2" pluggy = ">=1.5,<2"
pygments = ">=2.7.2" pygments = ">=2.7.2"
@@ -1681,22 +1701,23 @@ dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests
[[package]] [[package]]
name = "pytest-cov" name = "pytest-cov"
version = "5.0.0" version = "7.1.0"
description = "Pytest plugin for measuring coverage." description = "Pytest plugin for measuring coverage."
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.9"
groups = ["dev"] groups = ["dev"]
files = [ files = [
{file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, {file = "pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678"},
{file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, {file = "pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2"},
] ]
[package.dependencies] [package.dependencies]
coverage = {version = ">=5.2.1", extras = ["toml"]} coverage = {version = ">=7.10.6", extras = ["toml"]}
pytest = ">=4.6" pluggy = ">=1.2"
pytest = ">=7"
[package.extras] [package.extras]
testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] testing = ["process-tests", "pytest-xdist", "virtualenv"]
[[package]] [[package]]
name = "pytest-flask" name = "pytest-flask"
@@ -1803,14 +1824,14 @@ typing-extensions = {version = ">=4.4.0", markers = "python_version < \"3.13\""}
[[package]] [[package]]
name = "requests" name = "requests"
version = "2.34.0" version = "2.34.2"
description = "Python HTTP for Humans." description = "Python HTTP for Humans."
optional = false optional = false
python-versions = ">=3.10" python-versions = ">=3.10"
groups = ["dev"] groups = ["dev"]
files = [ files = [
{file = "requests-2.34.0-py3-none-any.whl", hash = "sha256:917520a21b767485ce7c588f4ebb917c436b24a31231b44228715eaeb5a52c60"}, {file = "requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0"},
{file = "requests-2.34.0.tar.gz", hash = "sha256:7d62fe92f50eb82c529b0916bb445afa1531a566fc8f35ffdc64446e771b856a"}, {file = "requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed"},
] ]
[package.dependencies] [package.dependencies]
@@ -2056,75 +2077,70 @@ files = [
[[package]] [[package]]
name = "sqlalchemy" name = "sqlalchemy"
version = "2.0.49" version = "2.0.50"
description = "Database Abstraction Library" description = "Database Abstraction Library"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
groups = ["main"] groups = ["main"]
files = [ files = [
{file = "sqlalchemy-2.0.49-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:42e8804962f9e6f4be2cbaedc0c3718f08f60a16910fa3d86da5a1e3b1bfe60f"}, {file = "sqlalchemy-2.0.50-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7af6eeb84985bf840ba779018ff9424d61ff69b52e66b8789d3c8da7bf5341b2"},
{file = "sqlalchemy-2.0.49-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc992c6ed024c8c3c592c5fc9846a03dd68a425674900c70122c77ea16c5fb0b"}, {file = "sqlalchemy-2.0.50-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0fe7822866f3a9fc5f3db21a290ce8961a53050115f05edf9402b6a5feb92a9f"},
{file = "sqlalchemy-2.0.49-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6eb188b84269f357669b62cb576b5b918de10fb7c728a005fa0ebb0b758adce1"}, {file = "sqlalchemy-2.0.50-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8e1b0f6a4dcd9b4839e2320afb5df37a6981cbc20ff9c423ae11c5537bdbd21"},
{file = "sqlalchemy-2.0.49-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:62557958002b69699bdb7f5137c6714ca1133f045f97b3903964f47db97ea339"}, {file = "sqlalchemy-2.0.50-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e195687f1af431c9515416288373b323b6eb599f774409814e89e9d603a56e39"},
{file = "sqlalchemy-2.0.49-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:da9b91bca419dc9b9267ffadde24eae9b1a6bffcd09d0a207e5e3af99a03ce0d"}, {file = "sqlalchemy-2.0.50-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ea1a8a2db4b2217d456c8d7a873bfc605f06fe3584d315264ea18c2a17585d0b"},
{file = "sqlalchemy-2.0.49-cp310-cp310-win32.whl", hash = "sha256:5e61abbec255be7b122aa461021daa7c3f310f3e743411a67079f9b3cc91ece3"}, {file = "sqlalchemy-2.0.50-cp310-cp310-win32.whl", hash = "sha256:68b154b08088b4ec32bb4d2958bfbb50e57549f91a4cd3e7f928e3553ed69031"},
{file = "sqlalchemy-2.0.49-cp310-cp310-win_amd64.whl", hash = "sha256:0c98c59075b890df8abfcc6ad632879540f5791c68baebacb4f833713b510e75"}, {file = "sqlalchemy-2.0.50-cp310-cp310-win_amd64.whl", hash = "sha256:66e374271ecb7101273f57af1a62446a953d327eec4f8089147de57c591bbacc"},
{file = "sqlalchemy-2.0.49-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c5070135e1b7409c4161133aa525419b0062088ed77c92b1da95366ec5cbebbe"}, {file = "sqlalchemy-2.0.50-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1aa6e403663a9c43c8fef7ce4bdb4cf48bcd8d352e91deda2a99f963270bd508"},
{file = "sqlalchemy-2.0.49-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ac7a3e245fd0310fd31495eb61af772e637bdf7d88ee81e7f10a3f271bff014"}, {file = "sqlalchemy-2.0.50-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51b637a84f9fa35ae1f9017e786cb142974a25305085e1b378b3647a67f65ad3"},
{file = "sqlalchemy-2.0.49-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d4e5a0ceba319942fa6b585cf82539288a61e314ef006c1209f734551ab9536"}, {file = "sqlalchemy-2.0.50-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2dab927761d9108550f0cf8e66ff21af56f907a0ce0a689793db615e2b55f62c"},
{file = "sqlalchemy-2.0.49-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3ddcb27fb39171de36e207600116ac9dfd4ae46f86c82a9bf3934043e80ebb88"}, {file = "sqlalchemy-2.0.50-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:545eae198d37bcf837a10ede3684e2af32458d6f35c597c35c2de7502dc38fc4"},
{file = "sqlalchemy-2.0.49-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:32fe6a41ad97302db2931f05bb91abbcc65b5ce4c675cd44b972428dd2947700"}, {file = "sqlalchemy-2.0.50-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0fec460e18cdbb4c7773531122ce9a27e96c6ca17af3933941d94da475ad2c86"},
{file = "sqlalchemy-2.0.49-cp311-cp311-win32.whl", hash = "sha256:46d51518d53edfbe0563662c96954dc8fcace9832332b914375f45a99b77cc9a"}, {file = "sqlalchemy-2.0.50-cp311-cp311-win32.whl", hash = "sha256:e6e814658818fd165e749e3d8490ef16cc7f379a118c37ada8b0589ffbaaac22"},
{file = "sqlalchemy-2.0.49-cp311-cp311-win_amd64.whl", hash = "sha256:951d4a210744813be63019f3df343bf233b7432aadf0db54c75802247330d3af"}, {file = "sqlalchemy-2.0.50-cp311-cp311-win_amd64.whl", hash = "sha256:1c5f858fe79c9f5d8fda065c06186356acb7f8df3cd52dbd5ee3f200e4b144f5"},
{file = "sqlalchemy-2.0.49-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4bbccb45260e4ff1b7db0be80a9025bb1e6698bdb808b83fff0000f7a90b2c0b"}, {file = "sqlalchemy-2.0.50-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23ae23d8b9d344d30d0a92f06d45825024a5790f1c1dd4cf452636a50d3e58cb"},
{file = "sqlalchemy-2.0.49-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb37f15714ec2652d574f021d479e78cd4eb9d04396dca36568fdfffb3487982"}, {file = "sqlalchemy-2.0.50-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:47b71b933e7b4ebad407c8fdfd70d2c4f08b78b3238bb30eebdd6eb32ca51b89"},
{file = "sqlalchemy-2.0.49-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3bb9ec6436a820a4c006aad1ac351f12de2f2dbdaad171692ee457a02429b672"}, {file = "sqlalchemy-2.0.50-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:110fdac56ace278949f00de805edacbd6141e382d992f9ba28238b3a0827a600"},
{file = "sqlalchemy-2.0.49-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8d6efc136f44a7e8bc8088507eaabbb8c2b55b3dbb63fe102c690da0ddebe55e"}, {file = "sqlalchemy-2.0.50-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0f5e4ac70e9e757f6b3e87c0491ff034442ecd8dfd36d041a50564c322dafc0e"},
{file = "sqlalchemy-2.0.49-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e06e617e3d4fd9e51d385dfe45b077a41e9d1b033a7702551e3278ac597dc750"}, {file = "sqlalchemy-2.0.50-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:724f3dcbe53dd0151e3cb5e7ec4ba4c620bede579caacd16275dc35ce06e8615"},
{file = "sqlalchemy-2.0.49-cp312-cp312-win32.whl", hash = "sha256:83101a6930332b87653886c01d1ee7e294b1fe46a07dd9a2d2b4f91bcc88eec0"}, {file = "sqlalchemy-2.0.50-cp312-cp312-win32.whl", hash = "sha256:1208050441471d003b7c8cb4054fb084f185cf35ac3f0ea270803865bca9939a"},
{file = "sqlalchemy-2.0.49-cp312-cp312-win_amd64.whl", hash = "sha256:618a308215b6cececb6240b9abde545e3acdabac7ae3e1d4e666896bf5ba44b4"}, {file = "sqlalchemy-2.0.50-cp312-cp312-win_amd64.whl", hash = "sha256:9d1af51558029a156a70986b7df88f042b3d158d7c8d8fb5072912d4b32d89c7"},
{file = "sqlalchemy-2.0.49-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df2d441bacf97022e81ad047e1597552eb3f83ca8a8f1a1fdd43cd7fe3898120"}, {file = "sqlalchemy-2.0.50-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:06a9210bdc5f4298cff0781087e2ff45683922252dacc452846373a58761f093"},
{file = "sqlalchemy-2.0.49-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8e20e511dc15265fb433571391ba313e10dd8ea7e509d51686a51313b4ac01a2"}, {file = "sqlalchemy-2.0.50-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b53784972ade4f8174b9aa661f31a06f8a936d2cfdd602913ff3c6dd40ae873"},
{file = "sqlalchemy-2.0.49-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47604cb2159f8bbd5a1ab48a714557156320f20871ee64d550d8bf2683d980d3"}, {file = "sqlalchemy-2.0.50-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:31648fa14460537e768a7303b078e4344d208e0d23e06867c1f376a227ed82db"},
{file = "sqlalchemy-2.0.49-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:22d8798819f86720bc646ab015baff5ea4c971d68121cb36e2ebc2ee43ead2b7"}, {file = "sqlalchemy-2.0.50-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:03f4323c980ad0e918cc9e5369b015f759f4e534db5bbaf4dc36832c10d05064"},
{file = "sqlalchemy-2.0.49-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9b1c058c171b739e7c330760044803099c7fff11511e3ab3573e5327116a9c33"}, {file = "sqlalchemy-2.0.50-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2b9dcc43afef8ac157cd92fce96985d6b8b0cfbd3df4d666f66b4d55a75d202f"},
{file = "sqlalchemy-2.0.49-cp313-cp313-win32.whl", hash = "sha256:a143af2ea6672f2af3f44ed8f9cd020e9cc34c56f0e8db12019d5d9ecf41cb3b"}, {file = "sqlalchemy-2.0.50-cp313-cp313-win32.whl", hash = "sha256:60922d6599065ddca2c6f376b9aa2f41a6b85a271725e0909490bbc50b1998a5"},
{file = "sqlalchemy-2.0.49-cp313-cp313-win_amd64.whl", hash = "sha256:12b04d1db2663b421fe072d638a138460a51d5a862403295671c4f3987fb9148"}, {file = "sqlalchemy-2.0.50-cp313-cp313-win_amd64.whl", hash = "sha256:287086e67275a212c4582d166a6fb03a65ccc5551d80866270ce0dd9f34eccd3"},
{file = "sqlalchemy-2.0.49-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24bd94bb301ec672d8f0623eba9226cc90d775d25a0c92b5f8e4965d7f3a1518"}, {file = "sqlalchemy-2.0.50-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c966932507a4d7d0a37314927dbfcd89720e3f37d2a1e3352e7ae7939fa8e8a0"},
{file = "sqlalchemy-2.0.49-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a51d3db74ba489266ef55c7a4534eb0b8db9a326553df481c11e5d7660c8364d"}, {file = "sqlalchemy-2.0.50-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:faffef4bcc20a1892e65e155293d99d60855bbbc79250ab712819cfd56a8e6bb"},
{file = "sqlalchemy-2.0.49-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:55250fe61d6ebfd6934a272ee16ef1244e0f16b7af6cd18ab5b1fc9f08631db0"}, {file = "sqlalchemy-2.0.50-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c206aec519a2e7bd08abbfb33436e325fd22c632d9c21a9047e376ce241646e"},
{file = "sqlalchemy-2.0.49-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:46796877b47034b559a593d7e4b549aba151dae73f9e78212a3478161c12ab08"}, {file = "sqlalchemy-2.0.50-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bef4ac756363227ef6402a75fee025a4bc690f92328e825868939b3b3a446a6d"},
{file = "sqlalchemy-2.0.49-cp313-cp313t-win32.whl", hash = "sha256:9c4969a86e41454f2858256c39bdfb966a20961e9b58bf8749b65abf447e9a8d"}, {file = "sqlalchemy-2.0.50-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:96fbee6b19c19cd1556c8bf9419447cf2ec149ffcab7ab64348c23e54ef8547f"},
{file = "sqlalchemy-2.0.49-cp313-cp313t-win_amd64.whl", hash = "sha256:b9870d15ef00e4d0559ae10ee5bc71b654d1f20076dbe8bc7ed19b4c0625ceba"}, {file = "sqlalchemy-2.0.50-cp314-cp314-win32.whl", hash = "sha256:8f00e3eb43ba30eb1b238ee03a8a62309486d1321eda3328bb611e0340033ad8"},
{file = "sqlalchemy-2.0.49-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:233088b4b99ebcbc5258c755a097aa52fbf90727a03a5a80781c4b9c54347a2e"}, {file = "sqlalchemy-2.0.50-cp314-cp314-win_amd64.whl", hash = "sha256:15708c613cd5005b7dffe1f66ee6a63ee8f5e46799f71c70ebad74178c676a39"},
{file = "sqlalchemy-2.0.49-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57ca426a48eb2c682dae8204cd89ea8ab7031e2675120a47924fabc7caacbc2a"}, {file = "sqlalchemy-2.0.50-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3699dac4be410e97049a1658e9480da9cde956594aa0f3aebc60b88f21c5ba70"},
{file = "sqlalchemy-2.0.49-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:685e93e9c8f399b0c96a624799820176312f5ceef958c0f88215af4013d29066"}, {file = "sqlalchemy-2.0.50-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f96233858e3df43932ac11589e22520da6e8aeb624b03fedfeebb0e8ea213086"},
{file = "sqlalchemy-2.0.49-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e0400fa22f79acc334d9a6b185dc00a44a8e6578aa7e12d0ddcd8434152b187"}, {file = "sqlalchemy-2.0.50-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c4e70c46fad30c3bcc6a4708bc0130a3173e11a5b25f0ea4a9d8911b450f1f52"},
{file = "sqlalchemy-2.0.49-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a05977bffe9bffd2229f477fa75eabe3192b1b05f408961d1bebff8d1cd4d401"}, {file = "sqlalchemy-2.0.50-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1918a3cf564d16d95bca7301005f41ab2ad50b07cd3b9da50d3ed986db148d6a"},
{file = "sqlalchemy-2.0.49-cp314-cp314-win32.whl", hash = "sha256:0f2fa354ba106eafff2c14b0cc51f22801d1e8b2e4149342023bd6f0955de5f5"}, {file = "sqlalchemy-2.0.50-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b00098cdbdbd38c7be3d568b0c9c3122b8c0ec62b911b57cd5e6e0254d60a76d"},
{file = "sqlalchemy-2.0.49-cp314-cp314-win_amd64.whl", hash = "sha256:77641d299179c37b89cf2343ca9972c88bb6eef0d5fc504a2f86afd15cd5adf5"}, {file = "sqlalchemy-2.0.50-cp314-cp314t-win32.whl", hash = "sha256:1fbd55a969d7ac44a98e3dec75016074f809fa08f871585ace58dde110d1bf3e"},
{file = "sqlalchemy-2.0.49-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c1dc3368794d522f43914e03312202523cc89692f5389c32bea0233924f8d977"}, {file = "sqlalchemy-2.0.50-cp314-cp314t-win_amd64.whl", hash = "sha256:c5c3cdb753a9004183e1ccb634b41611654c989e61bc68617ce878e46d6f1e51"},
{file = "sqlalchemy-2.0.49-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c821c47ecfe05cc32140dcf8dc6fd5d21971c86dbd56eabfe5ba07a64910c01"}, {file = "sqlalchemy-2.0.50-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4a8e8af330cbb3a1931d3d6c91b239fc2ef135f7dd471dfa34c575028e0b1fa8"},
{file = "sqlalchemy-2.0.49-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9c04bff9a5335eb95c6ecf1c117576a0aa560def274876fd156cfe5510fccc61"}, {file = "sqlalchemy-2.0.50-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eefd9a03cc0047b14153872d228499d048bd7deaf926109c9ec25b15157b8e23"},
{file = "sqlalchemy-2.0.49-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7f605a456948c35260e7b2a39f8952a26f077fd25653c37740ed186b90aaa68a"}, {file = "sqlalchemy-2.0.50-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13b85b20f9ab714a666df9d8e72e253ec33c16c7e1e375c877e5bf6367a3e917"},
{file = "sqlalchemy-2.0.49-cp314-cp314t-win32.whl", hash = "sha256:6270d717b11c5476b0cbb21eedc8d4dbb7d1a956fd6c15a23e96f197a6193158"}, {file = "sqlalchemy-2.0.50-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:27b7062af702c61994e8806ad87e42d0a2c879e0a8e5c61c7f69d81dabe24fdf"},
{file = "sqlalchemy-2.0.49-cp314-cp314t-win_amd64.whl", hash = "sha256:275424295f4256fd301744b8f335cff367825d270f155d522b30c7bf49903ee7"}, {file = "sqlalchemy-2.0.50-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:2c1920cde9d741ba3dda9b1aa5acd8c23ea17780ccfb2252d01878d5d0d628d3"},
{file = "sqlalchemy-2.0.49-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8a97ac839c2c6672c4865e48f3cbad7152cee85f4233fb4ca6291d775b9b954a"}, {file = "sqlalchemy-2.0.50-cp38-cp38-win32.whl", hash = "sha256:7b1ddb7b5fc60dfa9df6a487f06a143c77def47c0351849da2bcea59b244a56c"},
{file = "sqlalchemy-2.0.49-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c338ec6ec01c0bc8e735c58b9f5d51e75bacb6ff23296658826d7cfdfdb8678a"}, {file = "sqlalchemy-2.0.50-cp38-cp38-win_amd64.whl", hash = "sha256:0e104e196f457ec608eb8af736c5eb4c6bc58f481b546f485a7f9c628ee532be"},
{file = "sqlalchemy-2.0.49-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:566df36fd0e901625523a5a1835032f1ebdd7f7886c54584143fa6c668b4df3b"}, {file = "sqlalchemy-2.0.50-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:409a8121b917116b035bedc5e532ad470c74a2d279f6c302100985b6304e9f9e"},
{file = "sqlalchemy-2.0.49-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d99945830a6f3e9638d89a28ed130b1eb24c91255e4f24366fbe699b983f29e4"}, {file = "sqlalchemy-2.0.50-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9602c07b03e1449747ecb69f9998a7194a589124475788b370adce57c9e9a56e"},
{file = "sqlalchemy-2.0.49-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:01146546d84185f12721a1d2ce0c6673451a7894d1460b592d378ca4871a0c72"}, {file = "sqlalchemy-2.0.50-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3d10700bd519573f6ce5badbabbfe7f5baea84cdf370f2cbbfb4be28dfddbf1d"},
{file = "sqlalchemy-2.0.49-cp38-cp38-win32.whl", hash = "sha256:69469ce8ce7a8df4d37620e3163b71238719e1e2e5048d114a1b6ce0fbf8c662"}, {file = "sqlalchemy-2.0.50-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7e36efdcc5493f8024ec873a4ee3855bfd2de0c5b19eba16f920e9d2a0d28622"},
{file = "sqlalchemy-2.0.49-cp38-cp38-win_amd64.whl", hash = "sha256:b95b2f470c1b2683febd2e7eab1d3f0e078c91dbdd0b00e9c645d07a413bb99f"}, {file = "sqlalchemy-2.0.50-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:adc0fe7d38d8c8058f7421c25508fcbc74df38233a42aa8324409844122dce8f"},
{file = "sqlalchemy-2.0.49-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:43d044780732d9e0381ac8d5316f95d7f02ef04d6e4ef6dc82379f09795d993f"}, {file = "sqlalchemy-2.0.50-cp39-cp39-win32.whl", hash = "sha256:0a31c5963d58d3e3d11c5b97709e248305705de1fdf51ec3bf396674c5898b7e"},
{file = "sqlalchemy-2.0.49-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d6be30b2a75362325176c036d7fb8d19e8846c77e87683ffaa8177b35135613"}, {file = "sqlalchemy-2.0.50-cp39-cp39-win_amd64.whl", hash = "sha256:83a9fce296b7e052316d8c6943237b31b9c00f58ca9c253f2d165df52637a293"},
{file = "sqlalchemy-2.0.49-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d898cc2c76c135ef65517f4ddd7a3512fb41f23087b0650efb3418b8389a3cd1"}, {file = "sqlalchemy-2.0.50-py3-none-any.whl", hash = "sha256:92064363517a3ff8212b5a93b8c62876579d8dfd1ca5b561335f30152d884fa9"},
{file = "sqlalchemy-2.0.49-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:059d7151fff513c53a4638da8778be7fce81a0c4854c7348ebd0c4078ddf28fe"}, {file = "sqlalchemy-2.0.50.tar.gz", hash = "sha256:af5607d11ef90fd6a5c0549fe0045dce1663d427426bcfb506dcb5346a85a3b9"},
{file = "sqlalchemy-2.0.49-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:334edbcff10514ad1d66e3a70b339c0a29886394892490119dbb669627b17717"},
{file = "sqlalchemy-2.0.49-cp39-cp39-win32.whl", hash = "sha256:74ab4ee7794d7ed1b0c37e7333640e0f0a626fc7b398c07a7aef52f484fddde3"},
{file = "sqlalchemy-2.0.49-cp39-cp39-win_amd64.whl", hash = "sha256:88690f4e1f0fbf5339bedbb127e240fec1fd3070e9934c0b7bef83432f779d2f"},
{file = "sqlalchemy-2.0.49-py3-none-any.whl", hash = "sha256:ec44cfa7ef1a728e88ad41674de50f6db8cfdb3e2af84af86e0041aaf02d43d0"},
{file = "sqlalchemy-2.0.49.tar.gz", hash = "sha256:d15950a57a210e36dd4cec1aac22787e2a4d57ba9318233e2ef8b2daf9ff2d5f"},
] ]
[package.dependencies] [package.dependencies]
@@ -2258,7 +2274,7 @@ files = [
{file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"},
{file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"},
] ]
markers = {pi = "python_version == \"3.12\""} markers = {pi = "python_version < \"3.13\""}
[[package]] [[package]]
name = "urllib3" name = "urllib3"
@@ -2314,4 +2330,4 @@ watchdog = ["watchdog (>=2.3)"]
[metadata] [metadata]
lock-version = "2.1" lock-version = "2.1"
python-versions = "^3.12" python-versions = "^3.12"
content-hash = "6e8a5ea4bf88a2865861f4d89f87e7c54009ea6224d35d92098642b0591e1816" content-hash = "f1e7faffc35767e0d68491449140c11c7787b5b65073b9d49865daf22a4d59db"
+3 -3
View File
@@ -17,10 +17,10 @@ flask-sqlalchemy = "^3.1.1"
picamera2 = "^0.3" picamera2 = "^0.3"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
pytest = "^8.0" pytest = "^9.0.3"
pytest-cov = "^5.0" pytest-cov = "^7.1.0"
pytest-flask = "^1.3" pytest-flask = "^1.3"
black = "^26.0" black = "^26.5.0"
ruff = "^0.4" ruff = "^0.4"
mypy = "^1.10" mypy = "^1.10"
pip-audit = "^2.10.0" pip-audit = "^2.10.0"
+65 -4
View File
@@ -1,5 +1,8 @@
from __future__ import annotations
import logging import logging
from datetime import UTC, datetime from datetime import UTC, datetime
from pathlib import Path
from flask import ( from flask import (
Flask, Flask,
@@ -12,8 +15,8 @@ from flask import (
) )
from werkzeug.middleware.proxy_fix import ProxyFix from werkzeug.middleware.proxy_fix import ProxyFix
from src.camera import camera from src.camera import RECORDINGS_DIR, camera
from src.database import CameraEvent, CameraStatus, db from src.database import CameraEvent, CameraRecordingEvent, CameraStatus, db
logging.basicConfig(level=logging.WARNING) logging.basicConfig(level=logging.WARNING)
@@ -33,7 +36,6 @@ def create_app() -> Flask:
# sync camera state with db on startup # sync camera state with db on startup
status = CameraStatus.get() status = CameraStatus.get()
if status.running and not camera.running: if status.running and not camera.running:
# was running before restart — mark as stopped
CameraStatus.set_running(False) CameraStatus.set_running(False)
return flask_app return flask_app
@@ -50,6 +52,9 @@ def get_client_ip() -> str:
) )
# ── Health ───────────────────────────────────────────────────────────────────
@app.get("/heartbeat") @app.get("/heartbeat")
def heartbeat() -> tuple[Response, int]: def heartbeat() -> tuple[Response, int]:
return ( return (
@@ -63,11 +68,17 @@ def heartbeat() -> tuple[Response, int]:
) )
# ── Pages ────────────────────────────────────────────────────────────────────
@app.get("/") @app.get("/")
def index() -> str: def index() -> str:
return render_template("index.html") return render_template("index.html")
# ── Stream endpoints ──────────────────────────────────────────────────────────
@app.post("/camera/start") @app.post("/camera/start")
def camera_start() -> tuple[Response, int]: def camera_start() -> tuple[Response, int]:
status = CameraStatus.get() status = CameraStatus.get()
@@ -95,6 +106,7 @@ def camera_stop() -> tuple[Response, int]:
@app.get("/camera/status") @app.get("/camera/status")
def camera_status() -> tuple[Response, int]: def camera_status() -> tuple[Response, int]:
status = CameraStatus.get() status = CameraStatus.get()
rec = camera.recording_status()
return ( return (
jsonify( jsonify(
{ {
@@ -103,6 +115,8 @@ def camera_status() -> tuple[Response, int]:
camera.wait_until_ready(timeout=0) if status.running else False camera.wait_until_ready(timeout=0) if status.running else False
), ),
"updated_at": status.updated_at.isoformat(), "updated_at": status.updated_at.isoformat(),
"recording": rec["recording"],
"recording_started_at": rec["started_at"],
} }
), ),
200, 200,
@@ -133,7 +147,6 @@ def hls_segment(filename: str) -> Response:
if not hls_dir.exists(): if not hls_dir.exists():
abort(404) abort(404)
# set correct MIME types for HLS files
if filename.endswith(".m3u8"): if filename.endswith(".m3u8"):
mimetype = "application/vnd.apple.mpegurl" mimetype = "application/vnd.apple.mpegurl"
elif filename.endswith(".ts"): elif filename.endswith(".ts"):
@@ -144,5 +157,53 @@ def hls_segment(filename: str) -> Response:
return send_from_directory(str(hls_dir), filename, mimetype=mimetype) return send_from_directory(str(hls_dir), filename, mimetype=mimetype)
# ── Recording endpoints ───────────────────────────────────────────────────────
@app.post("/camera/record/start")
def record_start() -> tuple[Response, int]:
if camera.recording:
return jsonify({"status": "already_recording"}), 200
try:
path = camera.start_recording()
CameraRecordingEvent.log("record_start", get_client_ip(), str(path))
return jsonify({"status": "recording", "path": str(path)}), 200
except RuntimeError as exc:
return jsonify({"status": "error", "message": str(exc)}), 409
@app.post("/camera/record/stop")
def record_stop() -> tuple[Response, int]:
if not camera.recording:
return jsonify({"status": "not_recording"}), 200
path = camera.stop_recording()
filename = Path(path).name if path else None
CameraRecordingEvent.log("record_stop", get_client_ip(), str(path))
return jsonify({"status": "stopped", "filename": filename}), 200
@app.get("/camera/recordings")
def list_recordings() -> tuple[Response, int]:
recordings = camera.list_recordings()
return jsonify(recordings), 200
@app.get("/camera/recordings/<filename>")
def download_recording(filename: str) -> Response:
# safety: only allow the expected filename pattern
if not filename.startswith("recording_") or not filename.endswith(".mp4"):
abort(404)
if not RECORDINGS_DIR.exists():
abort(404)
return send_from_directory(
str(RECORDINGS_DIR),
filename,
mimetype="video/mp4",
as_attachment=True,
)
if __name__ == "__main__": if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000, debug=True, threaded=True) app.run(host="0.0.0.0", port=5000, debug=True, threaded=True)
+194 -15
View File
@@ -1,9 +1,12 @@
from __future__ import annotations
import io import io
import logging import logging
import shutil import shutil
import subprocess import subprocess
import threading import threading
import time import time
from datetime import UTC, datetime
from pathlib import Path from pathlib import Path
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -19,6 +22,7 @@ except ImportError:
PICAMERA_AVAILABLE = False PICAMERA_AVAILABLE = False
HLS_DIR = Path("/tmp/hls") HLS_DIR = Path("/tmp/hls")
RECORDINGS_DIR = Path("/var/lib/birdcam/recordings")
SEGMENT_DURATION = 2 # seconds per segment SEGMENT_DURATION = 2 # seconds per segment
SEGMENT_COUNT = 5 # segments to keep in playlist SEGMENT_COUNT = 5 # segments to keep in playlist
BITRATE = 2_000_000 # 2 Mbps — adjust for bandwidth needs BITRATE = 2_000_000 # 2 Mbps — adjust for bandwidth needs
@@ -47,13 +51,21 @@ class Camera:
def __init__(self) -> None: def __init__(self) -> None:
self._picam: Picamera2 | None = None self._picam: Picamera2 | None = None
self._encoder: H264Encoder | None = None self._stream_encoder: H264Encoder | None = None
self._record_encoder: H264Encoder | None = None
self._ffmpeg: subprocess.Popen[bytes] | None = None self._ffmpeg: subprocess.Popen[bytes] | None = None
self._record_ffmpeg: subprocess.Popen[bytes] | None = None
self._output: PipeOutput | None = None self._output: PipeOutput | None = None
self._ready_event = threading.Event() self._ready_event = threading.Event()
self._watch_thread: threading.Thread | None = None self._watch_thread: threading.Thread | None = None
self._stop_event = threading.Event() self._stop_event = threading.Event()
self.running = False self.running = False
self.recording = False
self._current_recording_path: Path | None = None
self._recording_started_at: datetime | None = None
self._lock = threading.Lock()
# ── Streaming ────────────────────────────────────────────────────────────
def start(self) -> None: def start(self) -> None:
if self.running: if self.running:
@@ -65,7 +77,7 @@ class Camera:
HLS_DIR.mkdir(parents=True) HLS_DIR.mkdir(parents=True)
if not PICAMERA_AVAILABLE: if not PICAMERA_AVAILABLE:
logger.info("Mock camera started") logger.info("Mock camera started (picamera2 not available)")
self.running = True self.running = True
return return
@@ -75,11 +87,11 @@ class Camera:
"-loglevel", "-loglevel",
"warning", "warning",
"-f", "-f",
"h264", # input is raw H.264 "h264",
"-i", "-i",
"pipe:0", # read from stdin "pipe:0",
"-c:v", "-c:v",
"copy", # no re-encoding — pass through directly "copy",
"-hls_time", "-hls_time",
str(SEGMENT_DURATION), str(SEGMENT_DURATION),
"-hls_list_size", "-hls_list_size",
@@ -99,7 +111,6 @@ class Camera:
) )
self._output = PipeOutput(self._ffmpeg) self._output = PipeOutput(self._ffmpeg)
# configure picamera2 with H.264 encoder
self._picam = Picamera2() self._picam = Picamera2()
config = self._picam.create_video_configuration( config = self._picam.create_video_configuration(
main={"size": (1280, 720)}, main={"size": (1280, 720)},
@@ -107,19 +118,18 @@ class Camera:
self._picam.configure(config) self._picam.configure(config)
self._picam.set_controls( self._picam.set_controls(
{ {
"Brightness": 0.1, # -1.0 to 1.0, default 0.0 "Brightness": 0.1,
"Contrast": 1.1, # 0.0 to 32.0, default 1.0 "Contrast": 1.1,
"Saturation": 1.1, # 0.0 to 32.0, default 1.0 "Saturation": 1.1,
"Sharpness": 1.0, # 0.0 to 16.0, default 1.0 "Sharpness": 1.0,
"AwbEnable": True, # auto white balance "AwbEnable": True,
"AeEnable": True, # auto exposure "AeEnable": True,
} }
) )
self._encoder = H264Encoder(bitrate=BITRATE) self._stream_encoder = H264Encoder(bitrate=BITRATE)
buffered = io.BufferedWriter(self._output) buffered = io.BufferedWriter(self._output)
self._picam.start_recording(self._encoder, FileOutput(buffered)) self._picam.start_recording(self._stream_encoder, FileOutput(buffered))
# watch for the playlist to appear — signals first segment is ready
self._stop_event.clear() self._stop_event.clear()
self._ready_event.clear() self._ready_event.clear()
self._watch_thread = threading.Thread(target=self._watch_playlist, daemon=True) self._watch_thread = threading.Thread(target=self._watch_playlist, daemon=True)
@@ -147,6 +157,11 @@ class Camera:
def stop(self) -> None: def stop(self) -> None:
if not self.running: if not self.running:
return return
# stop any active recording first
if self.recording:
self.stop_recording()
self._stop_event.set() self._stop_event.set()
self._ready_event.set() # unblock any waiters self._ready_event.set() # unblock any waiters
@@ -174,6 +189,170 @@ class Camera:
self.running = False self.running = False
logger.info("Camera stopped") logger.info("Camera stopped")
# ── Recording ────────────────────────────────────────────────────────────
def start_recording(self) -> Path:
"""
Start recording to an MP4 file. Can run independently of or
simultaneously with the HLS stream. Returns the output file path.
"""
with self._lock:
if self.recording:
raise RuntimeError("Already recording")
RECORDINGS_DIR.mkdir(parents=True, exist_ok=True)
timestamp = datetime.now(UTC).strftime("%Y%m%d_%H%M%S")
output_path = RECORDINGS_DIR / f"recording_{timestamp}.mp4"
if not PICAMERA_AVAILABLE:
# mock mode — create an empty placeholder file
output_path.touch()
self._current_recording_path = output_path
self._recording_started_at = datetime.now(UTC)
self.recording = True
logger.info("Mock recording started: %s", output_path)
return output_path
if self._picam is None:
# camera not yet streaming — start it temporarily in record-only mode
self._picam = Picamera2()
config = self._picam.create_video_configuration(
main={"size": (1280, 720)},
)
self._picam.configure(config)
self._picam.set_controls(
{
"Brightness": 0.1,
"Contrast": 1.1,
"Saturation": 1.1,
"Sharpness": 1.0,
"AwbEnable": True,
"AeEnable": True,
}
)
# pipe raw H.264 → ffmpeg → MP4 container
ffmpeg_cmd = [
"ffmpeg",
"-loglevel",
"warning",
"-f",
"h264",
"-i",
"pipe:0",
"-c:v",
"copy",
"-movflags",
"+faststart",
str(output_path),
]
self._record_ffmpeg = subprocess.Popen(
ffmpeg_cmd,
stdin=subprocess.PIPE,
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
)
record_pipe = PipeOutput(self._record_ffmpeg)
record_buffered = io.BufferedWriter(record_pipe)
self._record_encoder = H264Encoder(bitrate=BITRATE)
if self.running:
# camera already running for streaming — add a second encoder output
self._picam.start_encoder(
self._record_encoder,
FileOutput(record_buffered),
)
else:
# no stream active — start camera just for recording
self._picam.start_recording(
self._record_encoder,
FileOutput(record_buffered),
)
self._current_recording_path = output_path
self._recording_started_at = datetime.now(UTC)
self.recording = True
logger.info("Recording started: %s", output_path)
return output_path
def stop_recording(self) -> Path | None:
"""Stop the active recording and finalise the MP4 file."""
with self._lock:
if not self.recording:
return None
path = self._current_recording_path
if PICAMERA_AVAILABLE and self._record_encoder is not None:
if self.running:
# streaming still active — only stop the recording encoder
self._picam.stop_encoder(self._record_encoder) # type: ignore[union-attr]
else:
# recording-only mode — stop the whole camera
if self._picam:
self._picam.stop_recording()
self._picam.close()
self._picam = None
self._record_encoder = None
if self._record_ffmpeg:
if self._record_ffmpeg.stdin:
self._record_ffmpeg.stdin.close()
try:
self._record_ffmpeg.wait(timeout=10)
except subprocess.TimeoutExpired:
self._record_ffmpeg.kill()
self._record_ffmpeg = None
self.recording = False
self._current_recording_path = None
self._recording_started_at = None
logger.info("Recording stopped: %s", path)
return path
def recording_status(self) -> dict[str, object]:
"""Return current recording state for the API."""
return {
"recording": self.recording,
"path": (
str(self._current_recording_path)
if self._current_recording_path
else None
),
"started_at": (
self._recording_started_at.isoformat()
if self._recording_started_at
else None
),
}
def list_recordings(self) -> list[dict[str, object]]:
"""Return metadata for all saved recordings, newest first."""
if not RECORDINGS_DIR.exists():
return []
recordings = []
for f in sorted(RECORDINGS_DIR.glob("recording_*.mp4"), reverse=True):
stat = f.stat()
recordings.append(
{
"filename": f.name,
"size_bytes": stat.st_size,
"created_at": datetime.fromtimestamp(
stat.st_ctime, tz=UTC
).isoformat(),
"duration_seconds": None, # could be derived via ffprobe if needed
}
)
return recordings
# ── Properties ───────────────────────────────────────────────────────────
@property @property
def hls_dir(self) -> Path: def hls_dir(self) -> Path:
return HLS_DIR return HLS_DIR
+62 -23
View File
@@ -3,22 +3,24 @@ from __future__ import annotations
from datetime import UTC, datetime from datetime import UTC, datetime
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.orm import DeclarativeBase, Mapped from sqlalchemy import Boolean, DateTime, Integer, String
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
db: SQLAlchemy = SQLAlchemy()
class Base(DeclarativeBase): class Base(DeclarativeBase):
pass pass
db: SQLAlchemy = SQLAlchemy(model_class=Base)
class CameraStatus(Base): class CameraStatus(Base):
__tablename__ = "camera_status" __tablename__ = "camera_status"
id: Mapped[int] = db.mapped_column(db.Integer, primary_key=True) id: Mapped[int] = mapped_column(Integer, primary_key=True)
running: Mapped[bool] = db.mapped_column(db.Boolean, nullable=False, default=False) running: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
updated_at: Mapped[datetime] = db.mapped_column( updated_at: Mapped[datetime] = mapped_column(
db.DateTime(timezone=True), DateTime(timezone=True),
nullable=False, nullable=False,
default=lambda: datetime.now(UTC), default=lambda: datetime.now(UTC),
) )
@@ -45,13 +47,11 @@ class CameraStatus(Base):
class CameraEvent(Base): class CameraEvent(Base):
__tablename__ = "camera_events" __tablename__ = "camera_events"
id: Mapped[int] = db.mapped_column(db.Integer, primary_key=True) id: Mapped[int] = mapped_column(Integer, primary_key=True)
action: Mapped[str] = db.mapped_column( action: Mapped[str] = mapped_column(String(10), nullable=False) # 'start' | 'stop'
db.String(10), nullable=False ip_address: Mapped[str] = mapped_column(String(45), nullable=False)
) # 'start' | 'stop' timestamp: Mapped[datetime] = mapped_column(
ip_address: Mapped[str] = db.mapped_column(db.String(45), nullable=False) DateTime(timezone=True),
timestamp: Mapped[datetime] = db.mapped_column(
db.DateTime(timezone=True),
nullable=False, nullable=False,
default=lambda: datetime.now(UTC), default=lambda: datetime.now(UTC),
) )
@@ -66,13 +66,52 @@ class CameraEvent(Base):
@staticmethod @staticmethod
def recent(limit: int = 50) -> list[CameraEvent]: def recent(limit: int = 50) -> list[CameraEvent]:
return list( return list(
db.session.execute(
db.session.execute( db.select(CameraEvent)
db.select(CameraEvent) .order_by(CameraEvent.timestamp.desc())
.order_by(CameraEvent.timestamp.desc()) .limit(limit)
.limit(limit) )
) .scalars()
.scalars() .all()
.all() )
class CameraRecordingEvent(Base):
"""Audit log for recording start/stop actions."""
__tablename__ = "camera_recording_events"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
action: Mapped[str] = mapped_column(
String(20), nullable=False
) # 'record_start' | 'record_stop'
ip_address: Mapped[str] = mapped_column(String(45), nullable=False)
file_path: Mapped[str] = mapped_column(String(512), nullable=False, default="")
timestamp: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
nullable=False,
default=lambda: datetime.now(UTC),
)
@staticmethod
def log(action: str, ip_address: str, file_path: str = "") -> CameraRecordingEvent:
event = CameraRecordingEvent(
action=action,
ip_address=ip_address,
file_path=file_path,
)
db.session.add(event)
db.session.commit()
return event
@staticmethod
def recent(limit: int = 50) -> list[CameraRecordingEvent]:
return list(
db.session.execute(
db.select(CameraRecordingEvent)
.order_by(CameraRecordingEvent.timestamp.desc())
.limit(limit)
)
.scalars()
.all()
) )
+262 -41
View File
@@ -44,6 +44,24 @@
justify-content: center; justify-content: center;
} }
#preview.is-recording {
outline: 2px solid #ef4444;
outline-offset: 2px;
animation: rec-pulse 2s ease-in-out infinite;
}
@keyframes rec-pulse {
0%,
100% {
outline-color: #ef4444;
}
50% {
outline-color: #7f1d1d;
}
}
#stream-video { #stream-video {
width: 100%; width: 100%;
height: 100%; height: 100%;
@@ -54,6 +72,10 @@
#placeholder { #placeholder {
color: #555; color: #555;
font-size: 0.9rem; font-size: 0.9rem;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
} }
.controls { .controls {
@@ -70,7 +92,7 @@
border: none; border: none;
border-radius: 6px; border-radius: 6px;
cursor: pointer; cursor: pointer;
transition: opacity 0.2s; transition: opacity 0.2s, background 0.2s;
} }
button:disabled { button:disabled {
@@ -88,11 +110,128 @@
color: #fff; color: #fff;
} }
#btn-record {
background: #3f3f3f;
color: #f0f0f0;
display: flex;
align-items: center;
gap: 0.5rem;
}
#btn-record.is-recording {
background: #b91c1c;
color: #fff;
}
.rec-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: currentColor;
flex-shrink: 0;
}
#btn-record.is-recording .rec-dot {
animation: blink 1s step-start infinite;
}
@keyframes blink {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0;
}
}
.badge {
font-size: 0.8rem;
padding: 0.25rem 0.75rem;
border-radius: 999px;
font-weight: 600;
letter-spacing: 0.03em;
}
.badge.live {
background: #dc2626;
color: #fff;
}
#status { #status {
font-size: 0.85rem; font-size: 0.85rem;
color: #888; color: #888;
min-height: 1.2em; min-height: 1.2em;
} }
#recordings-section {
width: 100%;
max-width: 720px;
}
#recordings-section h2 {
font-size: 0.9rem;
letter-spacing: 0.06em;
text-transform: uppercase;
color: #666;
margin-bottom: 0.75rem;
padding-bottom: 0.4rem;
border-bottom: 1px solid #222;
}
#recordings-list {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.recording-item {
display: flex;
align-items: center;
justify-content: space-between;
background: #1a1a1a;
border: 1px solid #2a2a2a;
border-radius: 6px;
padding: 0.5rem 0.85rem;
font-size: 0.82rem;
gap: 1rem;
}
.recording-item .rec-name {
font-family: monospace;
color: #ccc;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
}
.recording-item .rec-meta {
color: #555;
white-space: nowrap;
font-size: 0.75rem;
}
.recording-item a {
color: #22c55e;
text-decoration: none;
font-size: 0.78rem;
white-space: nowrap;
transition: color 0.15s;
}
.recording-item a:hover {
color: #4ade80;
}
#recordings-empty {
color: #444;
font-size: 0.82rem;
text-align: center;
padding: 0.75rem 0;
}
</style> </style>
</head> </head>
@@ -101,71 +240,165 @@
<div id="preview"> <div id="preview">
<div id="placeholder"> <div id="placeholder">
<div class="dot" id="status-dot"></div>
<span id="placeholder-text">Checking stream...</span> <span id="placeholder-text">Checking stream...</span>
</div> </div>
<video id="stream-video" autoplay muted playsinline></video> <video id="stream-video" autoplay muted playsinline></video>
</div> </div>
<div class="controls" id="controls"></div> <div class="controls" id="controls"></div>
<p id="status"></p> <p id="status"></p>
<!-- hls.js from CDN --> <div id="recordings-section">
<h2>Recordings</h2>
<div id="recordings-list">
<p id="recordings-empty">No recordings yet.</p>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/hls.js@1/dist/hls.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/hls.js@1/dist/hls.min.js"></script>
<script> <script>
const video = document.getElementById("stream-video"); const video = document.getElementById("stream-video");
const placeholder = document.getElementById("placeholder"); const placeholder = document.getElementById("placeholder");
const placeholderText = document.getElementById("placeholder-text"); const placeholderText = document.getElementById("placeholder-text");
const statusDot = document.getElementById("status-dot");
const controls = document.getElementById("controls"); const controls = document.getElementById("controls");
const statusEl = document.getElementById("status"); const statusEl = document.getElementById("status");
const preview = document.getElementById("preview");
const recordingsList = document.getElementById("recordings-list");
let hls = null; let hls = null;
let pollInterval = null; let pollInterval = null;
let isRecording = false;
function setStatus(msg) { statusEl.textContent = msg; }
function fmtBytes(b) {
if (b < 1024) return b + " B";
if (b < 1048576) return (b / 1024).toFixed(1) + " KB";
return (b / 1048576).toFixed(1) + " MB";
}
function fmtDate(iso) { return new Date(iso).toLocaleString(); }
// ── Recordings list ─────────────────────────────────────────────────────
async function refreshRecordings() {
const res = await fetch("/camera/recordings");
const list = await res.json();
recordingsList.innerHTML = "";
if (list.length === 0) {
recordingsList.innerHTML = '<p id="recordings-empty">No recordings yet.</p>';
return;
}
list.forEach(rec => {
const item = document.createElement("div");
item.className = "recording-item";
item.innerHTML =
'<span class="rec-name">' + rec.filename + '</span>' +
'<span class="rec-meta">' + fmtBytes(rec.size_bytes) + ' &nbsp;&middot;&nbsp; ' + fmtDate(rec.created_at) + '</span>' +
'<a href="/camera/recordings/' + rec.filename + '" download>Download</a>';
recordingsList.appendChild(item);
});
}
// ── Record button ───────────────────────────────────────────────────────
function createRecordButton() {
const btn = document.createElement("button");
btn.id = "btn-record";
const dot = document.createElement("span");
dot.className = "rec-dot";
const label = document.createElement("span");
label.className = "rec-label";
if (isRecording) {
btn.classList.add("is-recording");
label.textContent = "Stop Recording";
} else {
label.textContent = "Record";
}
btn.appendChild(dot);
btn.appendChild(label);
btn.addEventListener("click", toggleRecording);
return btn;
}
async function toggleRecording() {
const btn = document.getElementById("btn-record");
if (btn) btn.disabled = true;
if (!isRecording) {
setStatus("Starting recording...");
const res = await fetch("/camera/record/start", { method: "POST" });
const data = await res.json();
if (data.status === "recording" || data.status === "already_recording") {
isRecording = true;
setStatus("Recording");
} else {
setStatus("Failed to start recording: " + (data.message || "unknown error"));
}
} else {
setStatus("Stopping recording...");
await fetch("/camera/record/stop", { method: "POST" });
isRecording = false;
setStatus("Recording saved");
await refreshRecordings();
}
const streaming = !!document.getElementById("btn-stop");
renderControls(streaming);
}
// ── Controls ────────────────────────────────────────────────────────────
function renderControls(running) { function renderControls(running) {
controls.innerHTML = ""; controls.innerHTML = "";
if (running) { if (running) {
const stop = document.createElement("button"); const stop = document.createElement("button");
stop.id = "btn-stop"; stop.id = "btn-stop";
stop.textContent = "Stop"; stop.textContent = "Stop Stream";
stop.addEventListener("click", stopStream); stop.addEventListener("click", stopStream);
controls.appendChild(stop); controls.appendChild(stop);
const badge = document.createElement("span");
badge.className = "badge live";
badge.textContent = "● Live";
controls.appendChild(badge);
} else { } else {
const start = document.createElement("button"); const start = document.createElement("button");
start.id = "btn-start"; start.id = "btn-start";
start.textContent = "Start"; start.textContent = "Start Stream";
start.addEventListener("click", startStream); start.addEventListener("click", startStream);
controls.appendChild(start); controls.appendChild(start);
} }
controls.appendChild(createRecordButton());
if (running) {
const badge = document.createElement("span");
badge.className = "badge live";
badge.textContent = "Live";
controls.appendChild(badge);
}
if (isRecording) {
preview.classList.add("is-recording");
} else {
preview.classList.remove("is-recording");
}
} }
function showOffline(message = "Stream is offline") { // ── Video display ────────────────────────────────────────────────────────
function showOffline(message) {
video.style.display = "none"; video.style.display = "none";
video.src = ""; video.src = "";
placeholder.style.display = "flex"; placeholder.style.display = "flex";
placeholderText.textContent = message; placeholderText.textContent = message || "Stream is offline";
statusDot.classList.remove("live");
} }
function showLive() { function showLive() {
placeholder.style.display = "none"; placeholder.style.display = "none";
video.style.display = "block"; video.style.display = "block";
statusDot.classList.add("live");
} }
// ── HLS ───────────────────────────────────────────────────────────────── // ── HLS ─────────────────────────────────────────────────────────────────
function startHls() { function startHls() {
const src = "/camera/hls/stream.m3u8"; const src = "/camera/hls/stream.m3u8";
if (hls) { hls.destroy(); hls = null; } if (hls) { hls.destroy(); hls = null; }
if (Hls.isSupported()) { if (Hls.isSupported()) {
@@ -179,7 +412,6 @@
}); });
hls.on(Hls.Events.ERROR, (event, data) => { hls.on(Hls.Events.ERROR, (event, data) => {
if (data.fatal) { if (data.fatal) {
console.error("HLS fatal error:", data);
showOffline("Stream error — reloading..."); showOffline("Stream error — reloading...");
setTimeout(attachToStream, 3000); setTimeout(attachToStream, 3000);
} }
@@ -201,18 +433,14 @@
video.style.display = "none"; video.style.display = "none";
} }
// ── Status polling ─────────────────────────────────────────────────────── // ── Status polling ───────────────────────────────────────────────────────
async function fetchStatus() { async function fetchStatus() {
const res = await fetch("/camera/status"); const res = await fetch("/camera/status");
return res.json(); return res.json();
} }
function setStatus(msg) { async function waitForReady(maxAttempts) {
statusEl.textContent = msg; maxAttempts = maxAttempts || 40;
}
async function waitForReady(maxAttempts = 40) {
for (let i = 0; i < maxAttempts; i++) { for (let i = 0; i < maxAttempts; i++) {
const data = await fetchStatus(); const data = await fetchStatus();
if (data.ready) return true; if (data.ready) return true;
@@ -221,9 +449,10 @@
return false; return false;
} }
// Called on page load and after stream errors — attach if already live
async function attachToStream() { async function attachToStream() {
const data = await fetchStatus(); const data = await fetchStatus();
isRecording = data.recording || false;
if (data.ready) { if (data.ready) {
startHls(); startHls();
renderControls(true); renderControls(true);
@@ -259,15 +488,10 @@
if (pollInterval) { clearInterval(pollInterval); pollInterval = null; } if (pollInterval) { clearInterval(pollInterval); pollInterval = null; }
} }
// ── Stream start / stop ────────────────────────────────────────────────── // ── Stream start / stop ──────────────────────────────────────────────────
async function startStream() { async function startStream() {
// guard: re-check status in case another client just started it
const current = await fetchStatus(); const current = await fetchStatus();
if (current.running || current.ready) { if (current.running || current.ready) { await attachToStream(); return; }
await attachToStream();
return;
}
const btn = document.getElementById("btn-start"); const btn = document.getElementById("btn-start");
if (btn) btn.disabled = true; if (btn) btn.disabled = true;
@@ -275,7 +499,6 @@
placeholderText.textContent = "Starting..."; placeholderText.textContent = "Starting...";
await fetch("/camera/start", { method: "POST" }); await fetch("/camera/start", { method: "POST" });
setStatus("Waiting for first segment..."); setStatus("Waiting for first segment...");
const ready = await waitForReady(); const ready = await waitForReady();
@@ -301,14 +524,12 @@
startOfflinePoll(); startOfflinePoll();
} }
// ── Init ──────────────────────────────────────────────────────────────── // ── Init ──────────────────────────────────────────────────────────────────
(async () => { (async () => {
await attachToStream(); await attachToStream();
await refreshRecordings();
const data = await fetchStatus(); const data = await fetchStatus();
if (!data.running && !data.ready) { if (!data.running && !data.ready) startOfflinePoll();
startOfflinePoll();
}
})(); })();
</script> </script>
</body> </body>
+93 -2
View File
@@ -17,6 +17,9 @@ def client() -> Generator[FlaskClient, Any, Any]:
db.drop_all() db.drop_all()
# ── Stream tests ─────────────────────────────────────────────────────────────
def test_heartbeat_status_code(client: FlaskClient) -> None: def test_heartbeat_status_code(client: FlaskClient) -> None:
response = client.get("/heartbeat") response = client.get("/heartbeat")
assert response.status_code == 200 assert response.status_code == 200
@@ -35,7 +38,7 @@ def test_camera_stop(client: FlaskClient) -> None:
assert response.get_json()["status"] in ("stopped", "already_stopped") assert response.get_json()["status"] in ("stopped", "already_stopped")
def test_double_start_is_idempotent(client): def test_double_start_is_idempotent(client: FlaskClient) -> None:
client.post("/camera/start") client.post("/camera/start")
res = client.post("/camera/start") res = client.post("/camera/start")
assert res.get_json()["status"] == "already_running" assert res.get_json()["status"] == "already_running"
@@ -48,9 +51,19 @@ def test_camera_status(client: FlaskClient) -> None:
assert "running" in data assert "running" in data
assert "ready" in data assert "ready" in data
assert "updated_at" in data assert "updated_at" in data
# recording fields must always be present
assert "recording" in data
assert "recording_started_at" in data
def test_camera_log(client): def test_camera_status_includes_recording_false_by_default(client: FlaskClient) -> None:
res = client.get("/camera/status")
data = res.get_json()
assert data["recording"] is False
assert data["recording_started_at"] is None
def test_camera_log(client: FlaskClient) -> None:
client.post("/camera/start") client.post("/camera/start")
client.post("/camera/stop") client.post("/camera/stop")
res = client.get("/camera/log") res = client.get("/camera/log")
@@ -72,3 +85,81 @@ def test_index_page(client: FlaskClient) -> None:
response = client.get("/") response = client.get("/")
assert response.status_code == 200 assert response.status_code == 200
assert b"Pi Camera" in response.data assert b"Pi Camera" in response.data
# ── Recording tests ───────────────────────────────────────────────────────────
def test_record_start_returns_200(client: FlaskClient) -> None:
res = client.post("/camera/record/start")
assert res.status_code == 200
data = res.get_json()
assert data["status"] in ("recording", "already_recording")
def test_record_stop_when_not_recording(client: FlaskClient) -> None:
res = client.post("/camera/record/stop")
assert res.status_code == 200
assert res.get_json()["status"] == "not_recording"
def test_record_start_then_stop(client: FlaskClient) -> None:
start = client.post("/camera/record/start")
assert start.get_json()["status"] == "recording"
stop = client.post("/camera/record/stop")
assert stop.status_code == 200
assert stop.get_json()["status"] == "stopped"
def test_double_record_start_is_idempotent(client: FlaskClient) -> None:
client.post("/camera/record/start")
res = client.post("/camera/record/start")
assert res.get_json()["status"] == "already_recording"
# clean up
client.post("/camera/record/stop")
def test_status_reflects_recording_state(client: FlaskClient) -> None:
client.post("/camera/record/start")
status = client.get("/camera/status").get_json()
assert status["recording"] is True
assert status["recording_started_at"] is not None
client.post("/camera/record/stop")
status = client.get("/camera/status").get_json()
assert status["recording"] is False
assert status["recording_started_at"] is None
def test_stream_and_record_simultaneously(client: FlaskClient) -> None:
"""Stream and record can be active at the same time without interference."""
client.post("/camera/start")
rec = client.post("/camera/record/start")
assert rec.get_json()["status"] in ("recording", "already_recording")
status = client.get("/camera/status").get_json()
assert status["running"] is True
assert status["recording"] is True
# stopping stream does not affect recording state report from camera object
client.post("/camera/stop")
# recording was stopped as part of camera.stop() — that is expected behaviour
# (the camera itself cleans up on stop)
def test_list_recordings_empty(client: FlaskClient) -> None:
res = client.get("/camera/recordings")
assert res.status_code == 200
assert res.get_json() == []
def test_download_recording_invalid_filename(client: FlaskClient) -> None:
# filename pattern not matching should 404
res = client.get("/camera/recordings/../../etc/passwd")
assert res.status_code in (404, 400)
def test_download_recording_wrong_prefix(client: FlaskClient) -> None:
res = client.get("/camera/recordings/evil.mp4")
assert res.status_code == 404