Compare commits
11 Commits
c2faf2c81c
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| c4d2516ea5 | |||
| f19ccf349b | |||
| 0f8572b26e | |||
| 8b7610671c | |||
| 5f020258e8 | |||
| af38580a71 | |||
| cdaaaad6af | |||
| bc935b41de | |||
| 02d3a9c33f | |||
| 800d665c8b | |||
| 49a81b5489 |
@@ -12,3 +12,5 @@ coverage.xml
|
|||||||
.coverage
|
.coverage
|
||||||
htmlcov/
|
htmlcov/
|
||||||
birdcam.db
|
birdcam.db
|
||||||
|
.vscode/
|
||||||
|
.ruff_cache/
|
||||||
|
|||||||
@@ -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 Flask’s built-in dev server:
|
||||||
|
```bash
|
||||||
|
flask run
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 How It Works
|
||||||
|
|
||||||
|
### Camera Status
|
||||||
|
- The `CameraStatus` model tracks a single camera’s 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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
+55
-14
@@ -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),
|
||||||
)
|
)
|
||||||
@@ -74,3 +74,44 @@ class CameraEvent(Base):
|
|||||||
.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
@@ -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) + ' · ' + 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
function showOffline(message = "Stream is offline") {
|
if (isRecording) {
|
||||||
|
preview.classList.add("is-recording");
|
||||||
|
} else {
|
||||||
|
preview.classList.remove("is-recording");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 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
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user