Compare commits
31 Commits
Author | SHA1 | Date | |
---|---|---|---|
f80b805a71 | |||
|
684e0e84bc | ||
|
4c02b57672 | ||
|
11d3079dbd | ||
|
87276d9432 | ||
93654d84e4 | |||
52fb1dc3de | |||
dd69741c0a | |||
10a1331e22 | |||
43db10c59e | |||
b81e295807 | |||
611d11292a | |||
ca78223d8e | |||
194ae55cbd | |||
830ce6690d | |||
1214158b4a | |||
65d09ee787 | |||
9e791e184d | |||
c9dd733989 | |||
71a4299f83 | |||
67725d44a1 | |||
e089fb224c | |||
27bb320d14 | |||
4ebedc05f5 | |||
13ad36a807 | |||
08aa96fce1 | |||
7106991a04 | |||
d177812270 | |||
a203ce904e | |||
|
7a4d497353 | ||
|
0a03429abf |
4
.dir-locals.el
Normal file
4
.dir-locals.el
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
((python-mode . ((lsp-pylsp-plugins-ruff-enabled . t)
|
||||||
|
(lsp-pylsp-plugins-pydocstyle-enabled . nil)
|
||||||
|
(lsp-pylsp-plugins-flake8-enabled . nil)
|
||||||
|
(lsp-pylsp-plugins-mypy-enabled . t))))
|
37
.dockerignore
Normal file
37
.dockerignore
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
# Ignore Python cache files
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
|
||||||
|
# Ignore virtual environments
|
||||||
|
venv/
|
||||||
|
.env/
|
||||||
|
.venv
|
||||||
|
|
||||||
|
# Ignore test files and directories
|
||||||
|
tests/
|
||||||
|
*.test.*
|
||||||
|
|
||||||
|
# Ignore Docker-specific files
|
||||||
|
.dockerignore
|
||||||
|
Dockerfile*
|
||||||
|
|
||||||
|
# Ignore version control files
|
||||||
|
.git/
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# Ignore development tools and configs
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
|
||||||
|
# Ignore logs and temp files
|
||||||
|
*.log
|
||||||
|
*.tmp
|
||||||
|
*.bak
|
||||||
|
|
||||||
|
# Images
|
||||||
|
*.png
|
||||||
|
|
||||||
|
# Ignore caches
|
||||||
|
.*cache
|
175
.gitignore
vendored
Normal file
175
.gitignore
vendored
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
.DS_Store
|
||||||
|
.idea
|
||||||
|
*.log
|
||||||
|
tmp/
|
||||||
|
|
||||||
|
*.py[cod]
|
||||||
|
*.egg
|
||||||
|
build
|
||||||
|
htmlcov
|
||||||
|
|
||||||
|
# Created by https://www.toptal.com/developers/gitignore/api/python
|
||||||
|
# Edit at https://www.toptal.com/developers/gitignore?templates=python
|
||||||
|
|
||||||
|
### Python ###
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
cover/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
# .python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# pdm
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||||
|
#pdm.lock
|
||||||
|
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||||
|
# in version control.
|
||||||
|
# https://pdm.fming.dev/#use-with-ide
|
||||||
|
.pdm.toml
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
cython_debug/
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
|
#.idea/
|
||||||
|
|
||||||
|
# ruff
|
||||||
|
.ruff_cache/
|
||||||
|
|
||||||
|
# LSP config files
|
||||||
|
pyrightconfig.json
|
||||||
|
|
||||||
|
# End of https://www.toptal.com/developers/gitignore/api/python
|
@ -1,20 +1,61 @@
|
|||||||
image: python:3-alpine
|
image: ghcr.io/astral-sh/uv:0.6-python3.13-bookworm
|
||||||
|
|
||||||
# Also run CI for Merge requests
|
stages:
|
||||||
workflow:
|
- lint
|
||||||
rules:
|
- test
|
||||||
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
|
- deploy
|
||||||
- if: $CI_COMMIT_TAG
|
|
||||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
|
||||||
|
|
||||||
before_script:
|
lint:
|
||||||
# - python --version ; pip --version # For debugging
|
stage: lint
|
||||||
- pip install flake8 -qq
|
script:
|
||||||
- pip install mypy -qq
|
- uv run ruff check
|
||||||
- pip install types-PyYAML types-psycopg2 -qq
|
- uv run ruff format --check
|
||||||
|
- uv run mypy .
|
||||||
|
|
||||||
test:
|
test:
|
||||||
|
stage: test
|
||||||
script:
|
script:
|
||||||
# ignore long lines
|
- uv run pytest -v --cov=. --cov-report=xml
|
||||||
- flake8 --ignore=E501 cleanmedia
|
after_script:
|
||||||
- mypy cleanmedia
|
- bash <(curl -s https://codecov.io/bash)
|
||||||
|
artifacts:
|
||||||
|
reports:
|
||||||
|
coverage_report:
|
||||||
|
coverage_format: cobertura
|
||||||
|
path: coverage.xml
|
||||||
|
|
||||||
|
deploy_to_dockerhub:
|
||||||
|
stage: deploy
|
||||||
|
needs:
|
||||||
|
- test
|
||||||
|
only:
|
||||||
|
refs:
|
||||||
|
- master
|
||||||
|
except:
|
||||||
|
- tags
|
||||||
|
image: docker:latest
|
||||||
|
services:
|
||||||
|
- docker:dind
|
||||||
|
before_script:
|
||||||
|
- docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_PASSWORD
|
||||||
|
- docker buildx create --use
|
||||||
|
script:
|
||||||
|
- docker buildx build --push --tag rogsme/cleanmedia:latest .
|
||||||
|
|
||||||
|
deploy_to_gitlab:
|
||||||
|
stage: deploy
|
||||||
|
needs:
|
||||||
|
- test
|
||||||
|
only:
|
||||||
|
refs:
|
||||||
|
- master
|
||||||
|
except:
|
||||||
|
- tags
|
||||||
|
image: docker:latest
|
||||||
|
services:
|
||||||
|
- docker:dind
|
||||||
|
before_script:
|
||||||
|
- docker login registry.gitlab.com -u $GITLAB_USERNAME -p $GITLAB_PASSWORD
|
||||||
|
- docker buildx create --use
|
||||||
|
script:
|
||||||
|
- docker buildx build --push --tag registry.gitlab.com/rogs/cleanmedia:latest .
|
||||||
|
18
.pre-commit-config.yaml
Normal file
18
.pre-commit-config.yaml
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
repos:
|
||||||
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
|
rev: v0.11.2
|
||||||
|
hooks:
|
||||||
|
- id: ruff
|
||||||
|
args: [--fix]
|
||||||
|
- id: ruff-format
|
||||||
|
- id: ruff
|
||||||
|
name: ruff-isort
|
||||||
|
args: [--select, I, --fix]
|
||||||
|
- repo: local
|
||||||
|
hooks:
|
||||||
|
- id: mypy
|
||||||
|
name: mypy
|
||||||
|
entry: uv run mypy
|
||||||
|
language: system
|
||||||
|
types: [python]
|
||||||
|
args: [--strict]
|
32
Dockerfile
Normal file
32
Dockerfile
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
ARG PYTHON_VERSION=3.13
|
||||||
|
FROM ghcr.io/astral-sh/uv:0.6-python${PYTHON_VERSION}-alpine AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN apk add --no-cache postgresql-dev
|
||||||
|
|
||||||
|
COPY uv.lock .
|
||||||
|
COPY pyproject.toml .
|
||||||
|
|
||||||
|
RUN uv sync --no-dev
|
||||||
|
|
||||||
|
FROM python:${PYTHON_VERSION}-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN apk add --no-cache postgresql-libs
|
||||||
|
|
||||||
|
ENV SUPERCRONIC_URL=https://github.com/aptible/supercronic/releases/download/v0.2.33/supercronic-linux-amd64 \
|
||||||
|
SUPERCRONIC_SHA1SUM=71b0d58cc53f6bd72cf2f293e09e294b79c666d8
|
||||||
|
|
||||||
|
RUN wget -qO /usr/local/bin/supercronic "$SUPERCRONIC_URL" \
|
||||||
|
&& echo "${SUPERCRONIC_SHA1SUM} /usr/local/bin/supercronic" | sha1sum -c - \
|
||||||
|
&& chmod +x /usr/local/bin/supercronic
|
||||||
|
|
||||||
|
COPY --from=builder /app/.venv /app/.venv
|
||||||
|
|
||||||
|
ENV PATH=/app/.venv/bin:$PATH
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
CMD echo "${CRON:-0 0 * * *} python /app/cleanmedia.py ${CLEANMEDIA_OPTS:--c /etc/dendrite/dendrite.yaml -t 30 -n -l}" > /app/crontab && \
|
||||||
|
/usr/local/bin/supercronic /app/crontab
|
201
README.md
201
README.md
@ -1,79 +1,180 @@
|
|||||||
# Cleanmedia
|
# Cleanmedia
|
||||||
|
[](https://codecov.io/gl/rogs/cleanmedia)
|
||||||
|
|
||||||
A poor man's data retention policy for dendrite servers.
|
<p align="center">
|
||||||
|
<img src="https://gitlab.com/uploads/-/system/project/avatar/64971838/logo.png" alt="cleanmedia"/>
|
||||||
|
</p>
|
||||||
|
|
||||||
## USAGE
|
A data retention policy tool for Dendrite servers.
|
||||||
|
|
||||||
Check the command line options with --help. You mainly pass it the dendrite
|
## Special thanks
|
||||||
configuration file as a means to find a) the media directory and b) the postgres
|
|
||||||
credentials for the dendrite data base.
|
|
||||||
|
|
||||||
You can also pass in the number of days you want to keep remote
|
The original author of this script is Sebastian Spaeth ([sspaeth](https://gitlab.com/sspaeth)). All props to him!
|
||||||
media. Optionally, you may also purge media from local users on the
|
|
||||||
homeserver.
|
|
||||||
|
|
||||||
### How it works
|
## Overview
|
||||||
|
|
||||||
#### Purge remote media (default)
|
Cleanmedia helps manage media storage on Dendrite servers by implementing configurable retention policies for both remote and local media files. It can remove old media files based on age while preserving essential content like user avatars.
|
||||||
|
|
||||||
cleanmedia scours the database for all entries in the media repository
|
## Installation
|
||||||
where user_id is an empty string (that is, the media was not uploaded
|
|
||||||
by a local user). It then deletes all entries, thumbnails and media
|
|
||||||
files that have been created `DAYS` time ago. (with DAYS being
|
|
||||||
configurable via command line and a default of 30 days)
|
|
||||||
|
|
||||||
This includes a number of remote media that we might want to keep
|
### Using Docker (Recommended)
|
||||||
(e.g. avatar images of users on remote home servers).
|
|
||||||
|
|
||||||
The main idea behind focusing on remote media is that a server
|
The easiest way to run Cleanmedia is using Docker. You can either use it as a standalone container or integrate it with your docker-compose.yml:
|
||||||
should be able to refetch remote media in case it is needed.
|
|
||||||
|
|
||||||
#### Purging "local" media (optional)
|
```yaml
|
||||||
|
services:
|
||||||
|
cleanmedia:
|
||||||
|
hostname: cleanmedia
|
||||||
|
image: rogsme/cleanmedia
|
||||||
|
volumes:
|
||||||
|
- /your/dendrite/config/location:/etc/dendrite
|
||||||
|
- /your/dendrite/media/location:/var/dendrite/media
|
||||||
|
environment:
|
||||||
|
- CRON=0 0 * * * # Run daily at midnight
|
||||||
|
- CLEANMEDIA_OPTS=-c /etc/dendrite/dendrite.yaml -t 30 -l # 30 day retention, include local files
|
||||||
|
depends_on:
|
||||||
|
- monolith
|
||||||
|
```
|
||||||
|
⚠ **MAKE SURE YOUR MOUNTPOINTS CORRESPOND TO THE MOUNTPOINTS OF YOUR DENDRITE DOCKER INSTALLATION!**
|
||||||
|
|
||||||
It also makes sense to delete local media, and it is possible using the
|
#### Environment Variables
|
||||||
option -l, but that is more complicated. (Local means, originating by
|
|
||||||
users on our homeserver.)
|
|
||||||
|
|
||||||
a) we might be the only source of our user's media, so any local media
|
- `CRON`: Cron schedule expression (default: `0 0 * * *` - daily at midnight)
|
||||||
that we purge might not be retrievable by anyone anymore - ever.
|
- `CLEANMEDIA_OPTS`: Command line options for cleanmedia (default: `-c /etc/dendrite/dendrite.yaml -t 30 -n -l`)
|
||||||
|
|
||||||
b) it is not easy to decide which local media are safe to purge.
|
#### Docker Volume Mounts
|
||||||
|
|
||||||
Possible scenarios: local media older than Y days, rooms that have been
|
- `/etc/dendrite`: Dendrite configuration directory
|
||||||
left by all users and are thus "unreachable", rooms that have been
|
- `/var/dendrite/media`: Dendrite media storage directory
|
||||||
upgraded but have users left in it, media that has not been "accessed"
|
|
||||||
the last Y days, ....
|
|
||||||
|
|
||||||
Finding out these things and setting all these policies is way more
|
### Manual Installation
|
||||||
difficult and in some cases we do not have the information we'd need
|
|
||||||
(e.g. when media has been accessed the last time).
|
|
||||||
|
|
||||||
Right now, we purge all older local media, except for user avatar
|
Cleanmedia uses uv for dependency management. To install:
|
||||||
images.
|
|
||||||
|
|
||||||
#### Sanity checks
|
```bash
|
||||||
|
# Install uv if you haven't already
|
||||||
|
pip install uv
|
||||||
|
|
||||||
In addition, we perform some sanity checks and warns if inconsistencies
|
# Install dependencies
|
||||||
occur:
|
uv sync
|
||||||
|
```
|
||||||
|
|
||||||
1) Are there thumbnails in the db that do not have corresponding media
|
#### Requirements
|
||||||
file entries (in the db)?
|
|
||||||
|
|
||||||
## Requirements
|
- Python >= 3.9
|
||||||
|
- uv for dependency management
|
||||||
- Python >= 3.8
|
- Required packages (automatically installed by uv):
|
||||||
- psycopg2
|
- psycopg2
|
||||||
- yaml
|
- pyyaml
|
||||||
|
- Development dependencies for testing and linting
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
## Todo
|
### Command Line Options
|
||||||
|
|
||||||
- Sanity checks: Are files on the file system that the db does not
|
- `-c`, `--config`: Path to dendrite.yaml config file (default: config.yaml)
|
||||||
know about?
|
- `-m`, `--mxid`: Delete a specific media ID
|
||||||
|
- `-u`, `--userid`: Delete all media from a local user ('@user:domain.com')
|
||||||
|
- `-t`, `--days`: Keep remote media for specified number of days (default: 30)
|
||||||
|
- `-l`, `--local`: Include local user media in cleanup
|
||||||
|
- `-n`, `--dryrun`: Simulate cleanup without modifying files
|
||||||
|
- `-q`, `--quiet`: Reduce output verbosity
|
||||||
|
- `-d`, `--debug`: Increase output verbosity
|
||||||
|
|
||||||
## LICENSE
|
### Docker Usage Examples
|
||||||
|
|
||||||
|
1. Run with default settings (daily cleanup, 30-day retention):
|
||||||
|
```bash
|
||||||
|
docker compose up -d cleanmedia
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Run with custom schedule and options:
|
||||||
|
```yaml
|
||||||
|
environment:
|
||||||
|
- CRON=0 */6 * * * # Run every 6 hours
|
||||||
|
- CLEANMEDIA_OPTS=-c /etc/dendrite/dendrite.yaml -t 14 -l -d # 14 day retention with debug logging
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Run a one-off cleanup:
|
||||||
|
```bash
|
||||||
|
docker compose run --rm cleanmedia python cleanmedia.py -c /etc/dendrite/dendrite.yaml -t 1 -l -d -n
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run cleanmedia.py --help
|
||||||
|
```
|
||||||
|
|
||||||
|
### How it Works
|
||||||
|
|
||||||
|
#### Remote Media Purge (Default)
|
||||||
|
- Scans database for media entries where user_id is empty (remote media)
|
||||||
|
- Deletes entries and files older than the specified retention period
|
||||||
|
- Includes cleanup of associated thumbnails
|
||||||
|
- Preserves remote avatar images of users
|
||||||
|
|
||||||
|
#### Local Media Purge (Optional)
|
||||||
|
- Activated with the `-l` flag
|
||||||
|
- Removes media uploaded by local server users
|
||||||
|
- Preserves user avatar images
|
||||||
|
- Use with caution as local media might not be retrievable after deletion
|
||||||
|
|
||||||
|
### Sanity Checks
|
||||||
|
|
||||||
|
The tool performs consistency checks and warns about:
|
||||||
|
- Thumbnails in the database without corresponding media entries
|
||||||
|
- Missing files that should exist according to the database
|
||||||
|
- Invalid file paths or permissions issues
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
The project includes a comprehensive test suite using pytest:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run tests
|
||||||
|
uv run pytest
|
||||||
|
|
||||||
|
# Run tests with coverage report
|
||||||
|
uv run pytest --cov=. --cov-report=xml
|
||||||
|
|
||||||
|
# Run specific test file
|
||||||
|
uv run pytest tests/test_cleanmedia.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Code Quality
|
||||||
|
|
||||||
|
Multiple tools ensure code quality:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run linting
|
||||||
|
uv run ruff check
|
||||||
|
|
||||||
|
# Run formatting check
|
||||||
|
uv run ruff format --check
|
||||||
|
|
||||||
|
# Run type checking
|
||||||
|
uv run mypy .
|
||||||
|
```
|
||||||
|
|
||||||
|
The project uses pre-commit hooks for consistent code quality. Install them with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run pre-commit install
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
This code is released under the GNU GPL v3 or any later version.
|
This code is released under the GNU GPL v3 or any later version.
|
||||||
|
|
||||||
**There is no warranty for correctness or data that might be
|
**Warning**: There is no warranty for correctness or data that might be accidentally deleted. Use with caution and always test with `--dryrun` first!
|
||||||
accidentally deleted. Assume the worst and hope for the best!**
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Contributions are welcome! Please ensure you:
|
||||||
|
1. Add tests for new functionality
|
||||||
|
2. Follow the existing code style (enforced by ruff)
|
||||||
|
3. Update documentation as needed
|
||||||
|
4. Run the test suite and linting before submitting PRs
|
||||||
|
313
cleanmedia
313
cleanmedia
@ -1,313 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
import argparse
|
|
||||||
import logging
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from functools import cached_property
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Optional, Union, List, Tuple
|
|
||||||
try:
|
|
||||||
import psycopg2, psycopg2.extensions # noqa: E401
|
|
||||||
import yaml
|
|
||||||
except ImportError:
|
|
||||||
raise Exception("Please install psycopg2 and pyyaml")
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------
|
|
||||||
class File:
|
|
||||||
"""A file in our db together with (hopefully) a physical file and thumbnails"""
|
|
||||||
|
|
||||||
def __init__(self, media_repo: 'MediaRepository', media_id: str, creation_ts: int, base64hash: str):
|
|
||||||
# The MediaRepository in which this file is recorded
|
|
||||||
self.repo = media_repo
|
|
||||||
self.media_id = media_id
|
|
||||||
# creation_ts is seconds since the epoch
|
|
||||||
self.create_date = datetime.fromtimestamp(creation_ts)
|
|
||||||
self.base64hash = base64hash
|
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def fullpath(self) -> Optional[Path]:
|
|
||||||
"""returns the directory in which the "file" and all thumbnails are located, or None if no file is known"""
|
|
||||||
if not self.base64hash:
|
|
||||||
return None
|
|
||||||
return self.repo.media_path / self.base64hash[0:1] / self.base64hash[1:2] / self.base64hash[2:]
|
|
||||||
|
|
||||||
def delete(self) -> bool:
|
|
||||||
"""Delete db entries, and the file itself
|
|
||||||
|
|
||||||
:returns: True on successful delete of file,
|
|
||||||
False or Exception on failure"""
|
|
||||||
res = True
|
|
||||||
if self.fullpath is None:
|
|
||||||
logging.info(f"No known path for file id '{self.media_id}', cannot delete file.")
|
|
||||||
res = False
|
|
||||||
elif not self.fullpath.is_dir():
|
|
||||||
logging.debug(f"Path for file id '{self.media_id}' is not a directory or does not exist, not deleting.")
|
|
||||||
res = False
|
|
||||||
else:
|
|
||||||
for file in self.fullpath.glob('*'):
|
|
||||||
# note: this does not handle directories in fullpath
|
|
||||||
file.unlink()
|
|
||||||
self.fullpath.rmdir()
|
|
||||||
logging.debug(f"Deleted directory {self.fullpath}")
|
|
||||||
|
|
||||||
with self.repo.conn.cursor() as cur:
|
|
||||||
cur.execute("DELETE from mediaapi_thumbnail WHERE media_id=%s;", (self.media_id,))
|
|
||||||
num_thumbnails = cur.rowcount
|
|
||||||
cur.execute("DELETE from mediaapi_media_repository WHERE media_id=%s;", (self.media_id,))
|
|
||||||
num_media = cur.rowcount
|
|
||||||
self.repo.conn.commit()
|
|
||||||
logging.debug(f"Deleted {num_media} + {num_thumbnails} db entries for media id {self.media_id}")
|
|
||||||
return res
|
|
||||||
|
|
||||||
def exists(self) -> bool:
|
|
||||||
"""returns True if the media file itself exists on the file system"""
|
|
||||||
if self.fullpath is None:
|
|
||||||
return False
|
|
||||||
return (self.fullpath / 'file').exists()
|
|
||||||
|
|
||||||
def has_thumbnail(self) -> int:
|
|
||||||
"""Returns the number of thumbnails associated with this file"""
|
|
||||||
with self.repo.conn.cursor() as cur:
|
|
||||||
cur.execute(f"select COUNT(media_id) from mediaapi_thumbnail WHERE media_id='{self.media_id}';")
|
|
||||||
row = cur.fetchone()
|
|
||||||
if row is None:
|
|
||||||
return 0
|
|
||||||
return int(row[0])
|
|
||||||
|
|
||||||
|
|
||||||
class MediaRepository:
|
|
||||||
"""A dendrite media repository"""
|
|
||||||
def __init__(self, media_path: Path, connection_string: str):
|
|
||||||
self.media_path = media_path
|
|
||||||
if not self.media_path.is_absolute():
|
|
||||||
logging.warn("The media path is relative, make sure you run this script in the correct directory!")
|
|
||||||
if not self.media_path.is_dir():
|
|
||||||
raise Exception("The configured media dir cannot be found!")
|
|
||||||
# List of current avatar imgs. init empty
|
|
||||||
self._avatar_media_ids: List[str] = []
|
|
||||||
|
|
||||||
self.db_conn_string = connection_string # psql db connection
|
|
||||||
self.conn = self.connect_db()
|
|
||||||
|
|
||||||
def connect_db(self) -> psycopg2.extensions.connection:
|
|
||||||
# postgresql://user:pass@localhost/database?params
|
|
||||||
if self.db_conn_string is None \
|
|
||||||
or not self.db_conn_string.startswith(("postgres://",
|
|
||||||
"postgresql://")):
|
|
||||||
errstr = "DB connection not a postgres one"
|
|
||||||
logging.error(errstr)
|
|
||||||
raise ValueError(errstr)
|
|
||||||
return psycopg2.connect(self.db_conn_string)
|
|
||||||
|
|
||||||
def get_single_media(self, mxid: str) -> Optional[File]:
|
|
||||||
"""Return `File` or `None`"""
|
|
||||||
with self.conn.cursor() as cur:
|
|
||||||
sql_str = "SELECT media_id, creation_ts, base64hash from mediaapi_media_repository WHERE media_id = %s;"
|
|
||||||
cur.execute(sql_str, (mxid,))
|
|
||||||
row = cur.fetchone()
|
|
||||||
if row is None:
|
|
||||||
return None
|
|
||||||
# creation_ts is ms since the epoch, so convert to seconds
|
|
||||||
return File(self, row[0], row[1] // 1000, row[2])
|
|
||||||
|
|
||||||
def get_local_user_media(self, user_id: str) -> List[File]:
|
|
||||||
"""Return all media created by a local user
|
|
||||||
|
|
||||||
:params:
|
|
||||||
:user_id: (`str`) of form "@user:servername.com"
|
|
||||||
:returns: `List[File]`
|
|
||||||
"""
|
|
||||||
with self.conn.cursor() as cur:
|
|
||||||
sql_str = "SELECT media_id, creation_ts, base64hash from mediaapi_media_repository WHERE user_id = %s;"
|
|
||||||
cur.execute(sql_str, (user_id,))
|
|
||||||
files = []
|
|
||||||
for row in cur.fetchall():
|
|
||||||
# creation_ts is ms since the epoch, so convert to seconds
|
|
||||||
f = File(self, row[0], row[1] // 1000, row[2])
|
|
||||||
files.append(f)
|
|
||||||
return files
|
|
||||||
|
|
||||||
def get_all_media(self, local: bool = False) -> List[File]:
|
|
||||||
"""Return List[File] of remote media or ALL media if local==True"""
|
|
||||||
with self.conn.cursor() as cur:
|
|
||||||
# media_id | media_origin | content_type | file_size_bytes | creation_ts | upload_name | base64hash | user_id
|
|
||||||
sql_str = "SELECT media_id, creation_ts, base64hash from mediaapi_media_repository"
|
|
||||||
if not local:
|
|
||||||
# only fetch remote media where user_id is empty
|
|
||||||
sql_str += " WHERE user_id = ''"
|
|
||||||
sql_str += ";"
|
|
||||||
cur.execute(sql_str)
|
|
||||||
files = []
|
|
||||||
for row in cur.fetchall():
|
|
||||||
# creation_ts is ms since the epoch, so convert to seconds
|
|
||||||
f = File(self, row[0], row[1] // 1000, row[2])
|
|
||||||
files.append(f)
|
|
||||||
return files
|
|
||||||
|
|
||||||
def get_avatar_images(self) -> List[str]:
|
|
||||||
"""Get a list of media_id which are current avatar images
|
|
||||||
|
|
||||||
We don't want to clean up those. Save & cache them internally.
|
|
||||||
"""
|
|
||||||
media_id = []
|
|
||||||
with self.conn.cursor() as cur:
|
|
||||||
cur.execute("SELECT avatar_url FROM userapi_profiles WHERE avatar_url > '';")
|
|
||||||
for row in cur.fetchall():
|
|
||||||
url = row[0] # mxc://matrix.org/6e627f4c538563
|
|
||||||
try:
|
|
||||||
media_id.append(url[url.rindex("/") + 1:])
|
|
||||||
except ValueError:
|
|
||||||
logging.warn("No slash in URL '%s'!", url)
|
|
||||||
self._avatar_media_ids = media_id
|
|
||||||
return self._avatar_media_ids
|
|
||||||
|
|
||||||
def sanity_check_thumbnails(self) -> None:
|
|
||||||
"""Warn if we have thumbnails in the db that do not refer to existing media"""
|
|
||||||
with self.conn.cursor() as cur:
|
|
||||||
cur.execute("SELECT COUNT(media_id) from mediaapi_thumbnail WHERE media_id NOT IN (SELECT media_id FROM mediaapi_media_repository);")
|
|
||||||
row = cur.fetchone()
|
|
||||||
if row is not None and row[0]:
|
|
||||||
logging.error("You have {} thumbnails in your db that do not refer to media. This needs fixing (we don't do that)!".format(row[0]))
|
|
||||||
|
|
||||||
def clean_media_files(self, days: int, local: bool = False, dryrun: bool = False) -> int:
|
|
||||||
"""Clean out old media files from this repository
|
|
||||||
|
|
||||||
:params:
|
|
||||||
:days: (int) delete media files older than N days.
|
|
||||||
:local: (bool) Also delete media originating from local users
|
|
||||||
:dryrun: (bool) Do not actually delete any files (just count)
|
|
||||||
:returns: (int) The number of files that were/would be deleted
|
|
||||||
"""
|
|
||||||
if local:
|
|
||||||
# populate the cache of current avt img. so we don't delete them
|
|
||||||
mr.get_avatar_images()
|
|
||||||
|
|
||||||
cleantime = datetime.today() - timedelta(days=days)
|
|
||||||
logging.info("Deleting remote media older than %s", cleantime)
|
|
||||||
num_deleted = 0
|
|
||||||
files = mr.get_all_media(local)
|
|
||||||
for file in [f for f in files if f.media_id not in mr._avatar_media_ids]:
|
|
||||||
if file.create_date < cleantime:
|
|
||||||
num_deleted += 1
|
|
||||||
if dryrun: # the great pretender
|
|
||||||
logging.info(f"Pretending to delete file id {file.media_id} on path {file.fullpath}.")
|
|
||||||
if not file.exists():
|
|
||||||
logging.info(f"File id {file.media_id} does not physically exist (path {file.fullpath}).")
|
|
||||||
else:
|
|
||||||
file.delete()
|
|
||||||
info_str = "Deleted %d files during the run."
|
|
||||||
if dryrun:
|
|
||||||
info_str = "%d files would have been deleted during the run."
|
|
||||||
logging.info(info_str, num_deleted)
|
|
||||||
|
|
||||||
return num_deleted
|
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------
|
|
||||||
def read_config(conf_file: Union[str, Path]) -> Tuple[Path, str]:
|
|
||||||
"""Read in the dendrite config file and return db creds and media path"""
|
|
||||||
try:
|
|
||||||
with open(conf_file) as f:
|
|
||||||
config = yaml.safe_load(f)
|
|
||||||
except FileNotFoundError:
|
|
||||||
errstr = f"Config file {conf_file} not found. Use the --help option to find out more."
|
|
||||||
logging.error(errstr)
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
if "media_api" not in config:
|
|
||||||
logging.error("Missing section media_api")
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
CONN_STR = None
|
|
||||||
if "global" in config and "database" in config["global"]:
|
|
||||||
CONN_STR = config["global"]["database"].get("connection_string", None)
|
|
||||||
elif "database" in config["media_api"]:
|
|
||||||
logging.debug("No database section in global, but one in media_api, using that")
|
|
||||||
CONN_STR = config["media_api"]["database"].get("connection_string", None)
|
|
||||||
|
|
||||||
if CONN_STR is None:
|
|
||||||
logging.error("Did not find connection string to media database.")
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
BASE_PATH = Path(config["media_api"].get("base_path", None))
|
|
||||||
|
|
||||||
if BASE_PATH is None:
|
|
||||||
logging.error("Missing base_path in media_api")
|
|
||||||
exit(1)
|
|
||||||
return (BASE_PATH, CONN_STR)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_options() -> argparse.Namespace:
|
|
||||||
loglevel = logging.INFO # default logging level
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
prog='cleanmedia',
|
|
||||||
description='Deletes 30 day old remote media files from dendrite servers')
|
|
||||||
parser.add_argument('-c', '--config', default="config.yaml", help="location of the dendrite.yaml config file.")
|
|
||||||
parser.add_argument('-m', '--mxid', dest="mxid",
|
|
||||||
help="Just delete media <MXID>. (no cleanup otherwise)")
|
|
||||||
parser.add_argument('-u', '--userid', dest="userid",
|
|
||||||
help="Delete all media by local user '\\@user:domain.com'. (ie, a user on hour homeserver. no cleanup otherwise)")
|
|
||||||
parser.add_argument('-t', '--days', dest="days",
|
|
||||||
default="30", type=int,
|
|
||||||
help="Keep remote media for <DAYS> days.")
|
|
||||||
parser.add_argument('-l', '--local', action='store_true',
|
|
||||||
help="Also purge local (ie, from *our* users) media.")
|
|
||||||
parser.add_argument('-n', '--dryrun', action='store_true',
|
|
||||||
help="Dry run (don't actually modify any files).")
|
|
||||||
parser.add_argument('-q', '--quiet', action='store_true', help="Reduce output verbosity.")
|
|
||||||
parser.add_argument('-d', '--debug', action='store_true', help="Increase output verbosity.")
|
|
||||||
args: argparse.Namespace = parser.parse_args()
|
|
||||||
if args.debug:
|
|
||||||
loglevel = logging.DEBUG
|
|
||||||
elif args.quiet:
|
|
||||||
loglevel = logging.WARNING
|
|
||||||
logging.basicConfig(level=loglevel, format='%(levelname)s - %(message)s')
|
|
||||||
return args
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
args = parse_options()
|
|
||||||
(MEDIA_PATH, CONN_STR) = read_config(args.config)
|
|
||||||
mr = MediaRepository(MEDIA_PATH, CONN_STR)
|
|
||||||
|
|
||||||
if args.mxid:
|
|
||||||
# Just clean a single media
|
|
||||||
logging.info("Attempting to delete media '%s'", args.mxid)
|
|
||||||
file = mr.get_single_media(args.mxid)
|
|
||||||
if file:
|
|
||||||
logging.info("Found media with id '%s'", args.mxid)
|
|
||||||
if not args.dryrun:
|
|
||||||
file.delete()
|
|
||||||
elif args.userid:
|
|
||||||
logging.info("Attempting to delete media by user '%s'", args.userid)
|
|
||||||
files = mr.get_local_user_media(args.userid)
|
|
||||||
num_deleted = 0
|
|
||||||
for file in files:
|
|
||||||
num_deleted += 1
|
|
||||||
if args.dryrun: # the great pretender
|
|
||||||
logging.info(f"Pretending to delete file id {file.media_id} on path {file.fullpath}.")
|
|
||||||
else:
|
|
||||||
file.delete()
|
|
||||||
info_str = "Deleted %d files during the run."
|
|
||||||
if args.dryrun:
|
|
||||||
info_str = "%d files would have been deleted during the run."
|
|
||||||
logging.info(info_str, num_deleted)
|
|
||||||
|
|
||||||
else: # main clean out...
|
|
||||||
# Sanity checks
|
|
||||||
mr.sanity_check_thumbnails() # warn in case of superfluous thumbnails
|
|
||||||
|
|
||||||
mr.clean_media_files(args.days, args.local, args.dryrun)
|
|
413
cleanmedia.py
Executable file
413
cleanmedia.py
Executable file
@ -0,0 +1,413 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""Media cleanup utility for Dendrite servers."""
|
||||||
|
|
||||||
|
"""
|
||||||
|
CleanMedia.
|
||||||
|
Copyright (C) 2024 Sebastian Spaeth
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from functools import cached_property
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Tuple, TypeAlias, Union
|
||||||
|
|
||||||
|
try:
|
||||||
|
import psycopg2
|
||||||
|
import psycopg2.extensions
|
||||||
|
import yaml
|
||||||
|
except ImportError as err:
|
||||||
|
raise ImportError("Required dependencies not found. Please install psycopg2 and pyyaml.") from err
|
||||||
|
|
||||||
|
# Type aliases
|
||||||
|
DBConnection: TypeAlias = psycopg2.extensions.connection
|
||||||
|
MediaID: TypeAlias = str
|
||||||
|
Timestamp: TypeAlias = int
|
||||||
|
Base64Hash: TypeAlias = str
|
||||||
|
UserID: TypeAlias = str
|
||||||
|
|
||||||
|
|
||||||
|
class File:
|
||||||
|
"""Represent a media file with its metadata and physical storage location."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
media_repo: "MediaRepository",
|
||||||
|
media_id: MediaID,
|
||||||
|
creation_ts: Timestamp,
|
||||||
|
base64hash: Base64Hash,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize a File object."""
|
||||||
|
self.repo = media_repo
|
||||||
|
self.media_id = media_id
|
||||||
|
self.create_date = datetime.fromtimestamp(creation_ts)
|
||||||
|
self.base64hash = base64hash
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def fullpath(self) -> Path | None:
|
||||||
|
"""Get the directory containing the file and its thumbnails.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Path to directory or None if no file location is known
|
||||||
|
"""
|
||||||
|
if not self.base64hash:
|
||||||
|
return None
|
||||||
|
return self.repo.media_path / self.base64hash[0:1] / self.base64hash[1:2] / self.base64hash[2:]
|
||||||
|
|
||||||
|
def delete(self) -> bool:
|
||||||
|
"""Remove file from filesystem and database.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if deletion was successful, False otherwise
|
||||||
|
"""
|
||||||
|
if not self._delete_files():
|
||||||
|
return False
|
||||||
|
|
||||||
|
return self._delete_db_entries()
|
||||||
|
|
||||||
|
def _delete_files(self) -> bool:
|
||||||
|
"""Remove physical files from filesystem.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if files were deleted or didn't exist, False on error
|
||||||
|
"""
|
||||||
|
if self.fullpath is None:
|
||||||
|
logging.info(f"No known path for file id '{self.media_id}', cannot delete file.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not self.fullpath.is_dir():
|
||||||
|
logging.debug(f"Path for file id '{self.media_id}' is not a directory or does not exist.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
for file in self.fullpath.glob("*"):
|
||||||
|
file.unlink()
|
||||||
|
self.fullpath.rmdir()
|
||||||
|
logging.debug(f"Deleted directory {self.fullpath}")
|
||||||
|
return True
|
||||||
|
except OSError as err:
|
||||||
|
logging.error(f"Failed to delete files for {self.media_id}: {err}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _delete_db_entries(self) -> bool:
|
||||||
|
"""Remove file entries from database.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if database entries were deleted successfully
|
||||||
|
"""
|
||||||
|
with self.repo.conn.cursor() as cur:
|
||||||
|
cur.execute("DELETE from mediaapi_thumbnail WHERE media_id=%s;", (self.media_id,))
|
||||||
|
num_thumbnails = cur.rowcount
|
||||||
|
cur.execute("DELETE from mediaapi_media_repository WHERE media_id=%s;", (self.media_id,))
|
||||||
|
num_media = cur.rowcount
|
||||||
|
self.repo.conn.commit()
|
||||||
|
logging.debug(f"Deleted {num_media} + {num_thumbnails} db entries for media id {self.media_id}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def exists(self) -> bool:
|
||||||
|
"""Check if the media file exists on the filesystem.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if file exists, False otherwise
|
||||||
|
"""
|
||||||
|
if self.fullpath is None:
|
||||||
|
return False
|
||||||
|
return (self.fullpath / "file").exists()
|
||||||
|
|
||||||
|
def has_thumbnail(self) -> int:
|
||||||
|
"""Count thumbnails associated with this file.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Number of thumbnails
|
||||||
|
"""
|
||||||
|
with self.repo.conn.cursor() as cur:
|
||||||
|
cur.execute("SELECT COUNT(media_id) FROM mediaapi_thumbnail WHERE media_id = %s;", (self.media_id,))
|
||||||
|
row = cur.fetchone()
|
||||||
|
return int(row[0]) if row else 0
|
||||||
|
|
||||||
|
|
||||||
|
class MediaRepository:
|
||||||
|
"""Handle media storage and retrieval for a Dendrite server."""
|
||||||
|
|
||||||
|
def __init__(self, media_path: Path, connection_string: str) -> None:
|
||||||
|
"""Initialize MediaRepository.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
media_path: Path to media storage directory
|
||||||
|
connection_string: PostgreSQL connection string
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If media_path doesn't exist or connection string is invalid
|
||||||
|
"""
|
||||||
|
self._validate_media_path(media_path)
|
||||||
|
self.media_path = media_path
|
||||||
|
self._avatar_media_ids: List[MediaID] = []
|
||||||
|
self.db_conn_string = connection_string
|
||||||
|
self.conn = self.connect_db()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _validate_media_path(path: Path) -> None:
|
||||||
|
if not path.is_absolute():
|
||||||
|
logging.warning("Media path is relative. Ensure correct working directory!")
|
||||||
|
if not path.is_dir():
|
||||||
|
raise ValueError("Media directory not found")
|
||||||
|
|
||||||
|
def connect_db(self) -> DBConnection:
|
||||||
|
"""Establish database connection.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
PostgreSQL connection object
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If connection string is invalid
|
||||||
|
"""
|
||||||
|
if not self.db_conn_string or not self.db_conn_string.startswith(("postgres://", "postgresql://")):
|
||||||
|
raise ValueError("Invalid PostgreSQL connection string")
|
||||||
|
return psycopg2.connect(self.db_conn_string)
|
||||||
|
|
||||||
|
def get_single_media(self, mxid: MediaID) -> File | None:
|
||||||
|
"""Retrieve a single media file by ID."""
|
||||||
|
with self.conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"SELECT media_id, creation_ts, base64hash from mediaapi_media_repository WHERE media_id = %s;",
|
||||||
|
(mxid,),
|
||||||
|
)
|
||||||
|
row = cur.fetchone()
|
||||||
|
return File(self, row[0], row[1] // 1000, row[2]) if row else None
|
||||||
|
|
||||||
|
def get_local_user_media(self, user_id: UserID) -> List[File]:
|
||||||
|
"""Get all media files created by a local user.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: User ID in format "@user:servername.com"
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of File objects
|
||||||
|
"""
|
||||||
|
with self.conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"SELECT media_id, creation_ts, base64hash FROM mediaapi_media_repository WHERE user_id = %s;",
|
||||||
|
(user_id,),
|
||||||
|
)
|
||||||
|
return [File(self, row[0], row[1] // 1000, row[2]) for row in cur.fetchall()]
|
||||||
|
|
||||||
|
def get_all_media(self, local: bool = False) -> List[File]:
|
||||||
|
"""Get all media files or only remote ones.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
local: If True, include local media files
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of File objects
|
||||||
|
"""
|
||||||
|
with self.conn.cursor() as cur:
|
||||||
|
query = """SELECT media_id, creation_ts, base64hash
|
||||||
|
FROM mediaapi_media_repository"""
|
||||||
|
if not local:
|
||||||
|
query += " WHERE user_id = ''"
|
||||||
|
cur.execute(query)
|
||||||
|
return [File(self, row[0], row[1] // 1000, row[2]) for row in cur.fetchall()]
|
||||||
|
|
||||||
|
def get_avatar_images(self) -> List[MediaID]:
|
||||||
|
"""Get media IDs of current avatar images.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of media IDs
|
||||||
|
"""
|
||||||
|
with self.conn.cursor() as cur:
|
||||||
|
cur.execute("SELECT avatar_url FROM userapi_profiles WHERE avatar_url > '';")
|
||||||
|
media_ids = []
|
||||||
|
for (url,) in cur.fetchall():
|
||||||
|
try:
|
||||||
|
media_ids.append(url[url.rindex("/") + 1 :])
|
||||||
|
except ValueError:
|
||||||
|
logging.warning("Invalid avatar URL: %s", url)
|
||||||
|
self._avatar_media_ids = media_ids
|
||||||
|
return media_ids
|
||||||
|
|
||||||
|
def sanity_check_thumbnails(self) -> None:
|
||||||
|
"""Check for orphaned thumbnail entries in database."""
|
||||||
|
with self.conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""SELECT COUNT(media_id) FROM mediaapi_thumbnail
|
||||||
|
WHERE NOT EXISTS (SELECT media_id FROM mediaapi_media_repository);""",
|
||||||
|
)
|
||||||
|
if (row := cur.fetchone()) and (count := row[0]):
|
||||||
|
logging.error(
|
||||||
|
"You have %d thumbnails in your db that do not refer to media. "
|
||||||
|
"This needs fixing (we don't do that)!",
|
||||||
|
count,
|
||||||
|
)
|
||||||
|
|
||||||
|
def clean_media_files(self, days: int, local: bool = False, dryrun: bool = False) -> int:
|
||||||
|
"""Remove old media files.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
days: Delete files older than this many days
|
||||||
|
local: If True, include local media files
|
||||||
|
dryrun: If True, only simulate deletion
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Number of files deleted (or that would be deleted in dryrun mode)
|
||||||
|
"""
|
||||||
|
if local:
|
||||||
|
self.get_avatar_images()
|
||||||
|
|
||||||
|
cutoff_date = datetime.today() - timedelta(days=days)
|
||||||
|
logging.info("Deleting remote media older than %s", cutoff_date)
|
||||||
|
|
||||||
|
files_to_delete = [
|
||||||
|
f
|
||||||
|
for f in self.get_all_media(local)
|
||||||
|
if f.media_id not in self._avatar_media_ids and f.create_date < cutoff_date
|
||||||
|
]
|
||||||
|
|
||||||
|
for file in files_to_delete:
|
||||||
|
if dryrun:
|
||||||
|
logging.info(f"Would delete file {file.media_id} at {file.fullpath}")
|
||||||
|
if not file.exists():
|
||||||
|
logging.info(f"File {file.media_id} doesn't exist at {file.fullpath}")
|
||||||
|
else:
|
||||||
|
file.delete()
|
||||||
|
|
||||||
|
action = "Would have deleted" if dryrun else "Deleted"
|
||||||
|
logging.info("%s %d files", action, len(files_to_delete))
|
||||||
|
return len(files_to_delete)
|
||||||
|
|
||||||
|
|
||||||
|
def read_config(conf_file: Union[str, Path]) -> Tuple[Path, str]:
|
||||||
|
"""Read database credentials and media path from config.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
conf_file: Path to Dendrite YAML config file
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (media_path, connection_string)
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
SystemExit: If config file is invalid or missing required fields
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with open(conf_file) as f:
|
||||||
|
config = yaml.safe_load(f)
|
||||||
|
except FileNotFoundError:
|
||||||
|
logging.error("Config file %s not found. Use --help for usage.", conf_file)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if "media_api" not in config:
|
||||||
|
logging.error("Missing media_api section in config")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
conn_string = None
|
||||||
|
if "global" in config and "database" in config["global"]:
|
||||||
|
conn_string = config["global"]["database"].get("connection_string")
|
||||||
|
elif "database" in config["media_api"]:
|
||||||
|
logging.debug("Using database config from media_api section")
|
||||||
|
conn_string = config["media_api"]["database"].get("connection_string")
|
||||||
|
|
||||||
|
if not conn_string:
|
||||||
|
logging.error("Database connection string not found in config")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
base_path = config["media_api"].get("base_path")
|
||||||
|
if not base_path:
|
||||||
|
logging.error("base_path not found in media_api config")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
return Path(base_path), conn_string
|
||||||
|
|
||||||
|
|
||||||
|
def parse_options() -> argparse.Namespace:
|
||||||
|
"""Parse command line arguments.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Parsed argument namespace
|
||||||
|
"""
|
||||||
|
parser = argparse.ArgumentParser(prog="cleanmedia", description="Delete old media files from Dendrite servers")
|
||||||
|
parser.add_argument("-c", "--config", default="config.yaml", help="Path to dendrite.yaml config file")
|
||||||
|
parser.add_argument("-m", "--mxid", help="Delete specific media ID")
|
||||||
|
parser.add_argument("-u", "--userid", help="Delete all media from local user '@user:domain.com'")
|
||||||
|
parser.add_argument("-t", "--days", type=int, default=30, help="Keep remote media for DAYS days")
|
||||||
|
parser.add_argument("-l", "--local", action="store_true", help="Include local user media in cleanup")
|
||||||
|
parser.add_argument("-n", "--dryrun", action="store_true", help="Simulate cleanup without modifying files")
|
||||||
|
parser.add_argument("-q", "--quiet", action="store_true", help="Reduce output verbosity")
|
||||||
|
parser.add_argument("-d", "--debug", action="store_true", help="Increase output verbosity")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
log_level = logging.INFO
|
||||||
|
if args.debug:
|
||||||
|
log_level = logging.DEBUG
|
||||||
|
elif args.quiet:
|
||||||
|
log_level = logging.WARNING
|
||||||
|
logging.basicConfig(level=log_level, format="%(levelname)s - %(message)s")
|
||||||
|
|
||||||
|
return args
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""Execute the media cleanup process."""
|
||||||
|
args = parse_options()
|
||||||
|
media_path, conn_string = read_config(args.config)
|
||||||
|
repo = MediaRepository(media_path, conn_string)
|
||||||
|
|
||||||
|
if args.mxid:
|
||||||
|
process_single_media(repo, args)
|
||||||
|
elif args.userid:
|
||||||
|
process_user_media(repo, args)
|
||||||
|
else:
|
||||||
|
repo.sanity_check_thumbnails()
|
||||||
|
repo.clean_media_files(args.days, args.local, args.dryrun)
|
||||||
|
|
||||||
|
|
||||||
|
def process_single_media(repo: MediaRepository, args: argparse.Namespace) -> None:
|
||||||
|
"""Handle deletion of a single media file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
repo: MediaRepository instance
|
||||||
|
args: Parsed command line arguments
|
||||||
|
"""
|
||||||
|
logging.info("Attempting to delete media '%s'", args.mxid)
|
||||||
|
if file := repo.get_single_media(args.mxid):
|
||||||
|
logging.info("Found media with id '%s'", args.mxid)
|
||||||
|
if not args.dryrun:
|
||||||
|
file.delete()
|
||||||
|
|
||||||
|
|
||||||
|
def process_user_media(repo: MediaRepository, args: argparse.Namespace) -> None:
|
||||||
|
"""Handle deletion of all media from a user.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
repo: MediaRepository instance
|
||||||
|
args: Parsed command line arguments
|
||||||
|
"""
|
||||||
|
logging.info("Attempting to delete media by user '%s'", args.userid)
|
||||||
|
files = repo.get_local_user_media(args.userid)
|
||||||
|
|
||||||
|
for file in files:
|
||||||
|
if args.dryrun:
|
||||||
|
logging.info("Would delete file %s at %s", file.media_id, file.fullpath)
|
||||||
|
else:
|
||||||
|
file.delete()
|
||||||
|
|
||||||
|
action = "Would delete" if args.dryrun else "Deleted"
|
||||||
|
logging.info("%s %d files", action, len(files))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
88
pyproject.toml
Normal file
88
pyproject.toml
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
[project]
|
||||||
|
name = "cleanmedia"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = ""
|
||||||
|
authors = [{ name = "Roger Gonzalez", email = "roger@rogs.me" }]
|
||||||
|
requires-python = "~=3.9"
|
||||||
|
readme = "README.md"
|
||||||
|
license = "GPL-3.0-or-later"
|
||||||
|
dependencies = [
|
||||||
|
"psycopg2>=2.9.10,<3",
|
||||||
|
"pyyaml>=6.0.2,<7",
|
||||||
|
]
|
||||||
|
|
||||||
|
[dependency-groups]
|
||||||
|
dev = [
|
||||||
|
"python-lsp-ruff>=2.2.2,<3",
|
||||||
|
"pre-commit>=4.0.1,<5",
|
||||||
|
"python-lsp-server>=1.12.0,<2",
|
||||||
|
"ruff>=0.8.1,<0.9",
|
||||||
|
"mypy>=1.13.0,<2",
|
||||||
|
"pylsp-mypy>=0.6.9,<0.7",
|
||||||
|
"types-pyyaml>=6.0.12.20240917,<7",
|
||||||
|
"types-psycopg2>=2.9.21.20241019,<3",
|
||||||
|
"pytest>=8.3.4,<9",
|
||||||
|
"pytest-mock>=3.14.0,<4",
|
||||||
|
"pytest-coverage>=0.0,<0.1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.mypy]
|
||||||
|
ignore_missing_imports = true
|
||||||
|
strict = true
|
||||||
|
exclude = ["migrations"]
|
||||||
|
|
||||||
|
[tool.pylsp-mypy]
|
||||||
|
enabled = true
|
||||||
|
live_mode = true
|
||||||
|
report_progress = true
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
pythonpath = ["."]
|
||||||
|
testpaths = ["tests"]
|
||||||
|
python_files = ["test_*.py"]
|
||||||
|
addopts = "-v --tb=short"
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
exclude = [
|
||||||
|
".bzr",
|
||||||
|
".direnv",
|
||||||
|
".eggs",
|
||||||
|
".git",
|
||||||
|
".git-rewrite",
|
||||||
|
".hg",
|
||||||
|
".ipynb_checkpoints",
|
||||||
|
".mypy_cache",
|
||||||
|
".nox",
|
||||||
|
".pants.d",
|
||||||
|
".pyenv",
|
||||||
|
".pytest_cache",
|
||||||
|
".pytype",
|
||||||
|
".ruff_cache",
|
||||||
|
".svn",
|
||||||
|
".tox",
|
||||||
|
".venv",
|
||||||
|
".vscode",
|
||||||
|
"__pypackages__",
|
||||||
|
"_build",
|
||||||
|
"buck-out",
|
||||||
|
"build",
|
||||||
|
"dist",
|
||||||
|
"node_modules",
|
||||||
|
"site-packages",
|
||||||
|
"venv",
|
||||||
|
"__init__.py"
|
||||||
|
]
|
||||||
|
|
||||||
|
line-length = 121
|
||||||
|
indent-width = 4
|
||||||
|
target-version = "py39"
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
select = ["E", "F", "PL", "B", "A", "C4", "TID", "ERA", "RET", "W", "C90", "ARG", "Q", "FLY", "SIM", "COM", "D"]
|
||||||
|
ignore = ["E402", "PLW2901"]
|
||||||
|
|
||||||
|
[tool.ruff.lint.pylint]
|
||||||
|
max-args = 6
|
||||||
|
|
||||||
|
[tool.ruff.lint.pydocstyle]
|
||||||
|
convention = "pep257"
|
418
tests/test_cleanmedia.py
Normal file
418
tests/test_cleanmedia.py
Normal file
@ -0,0 +1,418 @@
|
|||||||
|
"""Tests for cleanmedia module."""
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Tuple
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from pytest_mock import MockerFixture
|
||||||
|
|
||||||
|
from cleanmedia import File, MediaRepository, parse_options, process_single_media, process_user_media, read_config
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_db_conn(mocker: MockerFixture) -> Tuple[Any, Any]:
|
||||||
|
"""Create mock database connection and cursor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mocker: pytest-mock fixture
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of mock connection and cursor
|
||||||
|
"""
|
||||||
|
conn_mock = mocker.Mock()
|
||||||
|
cursor_mock = mocker.Mock()
|
||||||
|
ctx_manager = mocker.Mock()
|
||||||
|
ctx_manager.__enter__ = mocker.Mock(return_value=cursor_mock)
|
||||||
|
ctx_manager.__exit__ = mocker.Mock(return_value=None)
|
||||||
|
conn_mock.cursor.return_value = ctx_manager
|
||||||
|
return conn_mock, cursor_mock
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def media_repo(tmp_path: Any, mock_db_conn: Tuple[Any, Any], mocker: MockerFixture) -> MediaRepository:
|
||||||
|
"""Create MediaRepository instance with mocked database connection.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tmp_path: pytest temporary directory fixture
|
||||||
|
mock_db_conn: Mock database connection fixture
|
||||||
|
mocker: pytest-mock fixture
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Configured MediaRepository instance
|
||||||
|
"""
|
||||||
|
conn_mock, _ = mock_db_conn
|
||||||
|
media_path = tmp_path / "media"
|
||||||
|
media_path.mkdir()
|
||||||
|
mocker.patch("cleanmedia.MediaRepository.connect_db", return_value=conn_mock)
|
||||||
|
return MediaRepository(media_path, "postgresql://fake")
|
||||||
|
|
||||||
|
|
||||||
|
def test_file_init(mocker: MockerFixture) -> None:
|
||||||
|
"""Test File class initialization."""
|
||||||
|
repo = mocker.Mock()
|
||||||
|
file = File(repo, "mxid123", 1600000000, "base64hash123")
|
||||||
|
assert file.media_id == "mxid123"
|
||||||
|
assert file.create_date == datetime.fromtimestamp(1600000000)
|
||||||
|
assert file.base64hash == "base64hash123"
|
||||||
|
assert file.repo == repo
|
||||||
|
|
||||||
|
|
||||||
|
def test_file_fullpath(media_repo: MediaRepository) -> None:
|
||||||
|
"""Test File.fullpath property returns correct path."""
|
||||||
|
file = File(media_repo, "mxid123", 1600000000, "abc123")
|
||||||
|
expected_path = media_repo.media_path / "a" / "b" / "c123"
|
||||||
|
assert file.fullpath == expected_path
|
||||||
|
|
||||||
|
|
||||||
|
def test_file_exists_no_path(media_repo: MediaRepository) -> None:
|
||||||
|
"""Test File.exists returns False when fullpath is None."""
|
||||||
|
file = File(media_repo, "mxid123", 1600000000, "") # Empty hash ensures fullpath is None
|
||||||
|
assert file.exists() is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_file_delete_no_path(media_repo: MediaRepository) -> None:
|
||||||
|
"""Test File._delete_files when file path is None."""
|
||||||
|
file = File(media_repo, "mxid123", 1600000000, "")
|
||||||
|
assert file._delete_files() is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_file_delete_oserror(media_repo: MediaRepository, mocker: MockerFixture, caplog: Any) -> None:
|
||||||
|
"""Test File._delete_files when OSError occurs."""
|
||||||
|
file = File(media_repo, "mxid123", 1600000000, "abc123")
|
||||||
|
|
||||||
|
# Create directory structure
|
||||||
|
file_path = media_repo.media_path / "a" / "b" / "c123"
|
||||||
|
file_path.mkdir(parents=True)
|
||||||
|
(file_path / "file").touch()
|
||||||
|
|
||||||
|
# Mock Path.glob to raise OSError
|
||||||
|
mocker.patch.object(Path, "glob", side_effect=OSError("Permission denied"))
|
||||||
|
|
||||||
|
assert file._delete_files() is False
|
||||||
|
assert "Failed to delete files for mxid123: Permission denied" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_file_fullpath_none_if_no_hash(media_repo: MediaRepository) -> None:
|
||||||
|
"""Test File.fullpath returns None when hash is empty."""
|
||||||
|
file = File(media_repo, "mxid123", 1600000000, "")
|
||||||
|
assert file.fullpath is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_file_exists(media_repo: MediaRepository) -> None:
|
||||||
|
"""Test File.exists returns True when file exists."""
|
||||||
|
file = File(media_repo, "mxid123", 1600000000, "abc123")
|
||||||
|
file_path = media_repo.media_path / "a" / "b" / "c123"
|
||||||
|
file_path.mkdir(parents=True)
|
||||||
|
(file_path / "file").touch()
|
||||||
|
assert file.exists() is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_file_not_exists(media_repo: MediaRepository) -> None:
|
||||||
|
"""Test File.exists returns False when file doesn't exist."""
|
||||||
|
file = File(media_repo, "mxid123", 1600000000, "abc123")
|
||||||
|
assert file.exists() is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_file_delete(media_repo: MediaRepository, mock_db_conn: Tuple[Any, Any]) -> None:
|
||||||
|
"""Test File.delete removes files and database entries."""
|
||||||
|
_, cursor_mock = mock_db_conn
|
||||||
|
file = File(media_repo, "mxid123", 1600000000, "abc123")
|
||||||
|
|
||||||
|
file_path = media_repo.media_path / "a" / "b" / "c123"
|
||||||
|
file_path.mkdir(parents=True)
|
||||||
|
(file_path / "file").touch()
|
||||||
|
(file_path / "thumb").touch()
|
||||||
|
|
||||||
|
assert file.delete() is True
|
||||||
|
assert not file_path.exists()
|
||||||
|
|
||||||
|
cursor_mock.execute.assert_any_call("DELETE from mediaapi_thumbnail WHERE media_id=%s;", ("mxid123",))
|
||||||
|
cursor_mock.execute.assert_any_call("DELETE from mediaapi_media_repository WHERE media_id=%s;", ("mxid123",))
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_single_media(media_repo: MediaRepository, mock_db_conn: Tuple[Any, Any]) -> None:
|
||||||
|
"""Test MediaRepository.get_single_media returns correct File object."""
|
||||||
|
_, cursor_mock = mock_db_conn
|
||||||
|
cursor_mock.fetchone.return_value = ("mxid123", 1600000000000, "abc123")
|
||||||
|
|
||||||
|
file = media_repo.get_single_media("mxid123")
|
||||||
|
assert file is not None
|
||||||
|
assert file.media_id == "mxid123"
|
||||||
|
assert file.base64hash == "abc123"
|
||||||
|
|
||||||
|
cursor_mock.execute.assert_called_with(
|
||||||
|
"SELECT media_id, creation_ts, base64hash from mediaapi_media_repository WHERE media_id = %s;",
|
||||||
|
("mxid123",),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_single_media_not_found(media_repo: MediaRepository, mock_db_conn: Tuple[Any, Any]) -> None:
|
||||||
|
"""Test MediaRepository.get_single_media returns None when media not found."""
|
||||||
|
_, cursor_mock = mock_db_conn
|
||||||
|
cursor_mock.fetchone.return_value = None
|
||||||
|
|
||||||
|
file = media_repo.get_single_media("mxid123")
|
||||||
|
assert file is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_clean_media_files(media_repo: MediaRepository, mock_db_conn: Tuple[Any, Any]) -> None:
|
||||||
|
"""Test MediaRepository.clean_media_files deletes old files."""
|
||||||
|
_, cursor_mock = mock_db_conn
|
||||||
|
|
||||||
|
old_date = int((datetime.now() - timedelta(days=31)).timestamp())
|
||||||
|
new_date = int((datetime.now() - timedelta(days=1)).timestamp())
|
||||||
|
|
||||||
|
cursor_mock.fetchall.return_value = [
|
||||||
|
("old_file", old_date * 1000, "abc123"),
|
||||||
|
("new_file", new_date * 1000, "def456"),
|
||||||
|
]
|
||||||
|
|
||||||
|
media_repo._avatar_media_ids = []
|
||||||
|
|
||||||
|
num_deleted = media_repo.clean_media_files(30, False, False)
|
||||||
|
assert num_deleted == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_clean_media_files_dryrun(media_repo: MediaRepository, mock_db_conn: Tuple[Any, Any]) -> None:
|
||||||
|
"""Test MediaRepository.clean_media_files in dry run mode."""
|
||||||
|
_, cursor_mock = mock_db_conn
|
||||||
|
|
||||||
|
old_date = int((datetime.now() - timedelta(days=31)).timestamp())
|
||||||
|
cursor_mock.fetchall.return_value = [
|
||||||
|
("old_file", old_date * 1000, "abc123"),
|
||||||
|
]
|
||||||
|
|
||||||
|
media_repo._avatar_media_ids = []
|
||||||
|
|
||||||
|
num_deleted = media_repo.clean_media_files(30, False, True)
|
||||||
|
assert num_deleted == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_clean_media_files_local(media_repo: MediaRepository, mocker: MockerFixture) -> None:
|
||||||
|
"""Test clean_media_files fetches avatar images when local=True."""
|
||||||
|
mock_get_avatars = mocker.patch.object(media_repo, "get_avatar_images")
|
||||||
|
media_repo.get_all_media = mocker.Mock(return_value=[]) # type: ignore
|
||||||
|
|
||||||
|
media_repo.clean_media_files(30, local=True)
|
||||||
|
mock_get_avatars.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
def test_sanity_check_thumbnails(media_repo: MediaRepository, mock_db_conn: Tuple[Any, Any], caplog: Any) -> None:
|
||||||
|
"""Test MediaRepository.sanity_check_thumbnails logs correct message."""
|
||||||
|
_, cursor_mock = mock_db_conn
|
||||||
|
cursor_mock.fetchone.return_value = (5,)
|
||||||
|
|
||||||
|
media_repo.sanity_check_thumbnails()
|
||||||
|
assert "You have 5 thumbnails in your db that do not refer to media" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_avatar_images(media_repo: MediaRepository, mock_db_conn: Tuple[Any, Any]) -> None:
|
||||||
|
"""Test MediaRepository.get_avatar_images returns correct list."""
|
||||||
|
_, cursor_mock = mock_db_conn
|
||||||
|
cursor_mock.fetchall.return_value = [
|
||||||
|
("mxc://matrix.org/abc123",),
|
||||||
|
("mxc://matrix.org/def456",),
|
||||||
|
]
|
||||||
|
|
||||||
|
avatar_ids = media_repo.get_avatar_images()
|
||||||
|
assert avatar_ids == ["abc123", "def456"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_avatar_images_invalid_url(media_repo: MediaRepository, mock_db_conn: Tuple[Any, Any], caplog: Any) -> None:
|
||||||
|
"""Test get_avatar_images with invalid URL."""
|
||||||
|
_, cursor_mock = mock_db_conn
|
||||||
|
cursor_mock.fetchall.return_value = [("invalid_url",)]
|
||||||
|
|
||||||
|
avatar_ids = media_repo.get_avatar_images()
|
||||||
|
assert avatar_ids == []
|
||||||
|
assert "Invalid avatar URL: invalid_url" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_media_path_absolute(tmp_path: Path) -> None:
|
||||||
|
"""Test _validate_media_path with absolute path."""
|
||||||
|
MediaRepository._validate_media_path(tmp_path)
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_media_path_not_exists(tmp_path: Path) -> None:
|
||||||
|
"""Test _validate_media_path with non-existent directory."""
|
||||||
|
invalid_path = tmp_path / "nonexistent"
|
||||||
|
with pytest.raises(ValueError, match="Media directory not found"):
|
||||||
|
MediaRepository._validate_media_path(invalid_path)
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_media_path_relative(caplog: Any) -> None:
|
||||||
|
"""Test _validate_media_path with relative path."""
|
||||||
|
relative_path = Path(".")
|
||||||
|
MediaRepository._validate_media_path(relative_path)
|
||||||
|
assert "Media path is relative" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_connect_db_success(mocker: MockerFixture) -> None:
|
||||||
|
"""Test successful database connection."""
|
||||||
|
mock_connect = mocker.patch("psycopg2.connect")
|
||||||
|
repo = MediaRepository(Path("/tmp"), "postgresql://fake")
|
||||||
|
repo.connect_db()
|
||||||
|
mock_connect.assert_called_with("postgresql://fake")
|
||||||
|
|
||||||
|
|
||||||
|
def test_connect_db_invalid_string(tmp_path: Path) -> None:
|
||||||
|
"""Test connect_db with invalid connection string."""
|
||||||
|
with pytest.raises(ValueError, match="Invalid PostgreSQL connection string"):
|
||||||
|
repo = MediaRepository(tmp_path, "invalid")
|
||||||
|
repo.connect_db()
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_local_user_media(media_repo: MediaRepository, mock_db_conn: Tuple[Any, Any]) -> None:
|
||||||
|
"""Test get_local_user_media returns correct files."""
|
||||||
|
_, cursor_mock = mock_db_conn
|
||||||
|
cursor_mock.fetchall.return_value = [
|
||||||
|
("media1", 1600000000000, "hash1"),
|
||||||
|
("media2", 1600000000000, "hash2"),
|
||||||
|
]
|
||||||
|
|
||||||
|
files = media_repo.get_local_user_media("@user:domain.com")
|
||||||
|
assert len(files) == 2 # noqa PLR2004
|
||||||
|
assert files[0].media_id == "media1"
|
||||||
|
assert files[1].media_id == "media2"
|
||||||
|
|
||||||
|
cursor_mock.execute.assert_called_with(
|
||||||
|
"SELECT media_id, creation_ts, base64hash FROM mediaapi_media_repository WHERE user_id = %s;",
|
||||||
|
("@user:domain.com",),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_process_single_media(media_repo: MediaRepository) -> None:
|
||||||
|
"""Test process_single_media deletes file."""
|
||||||
|
args = MagicMock()
|
||||||
|
args.mxid = "test_media"
|
||||||
|
args.dryrun = False
|
||||||
|
|
||||||
|
file_mock = MagicMock()
|
||||||
|
media_repo.get_single_media = MagicMock(return_value=file_mock) # type: ignore
|
||||||
|
|
||||||
|
process_single_media(media_repo, args)
|
||||||
|
|
||||||
|
media_repo.get_single_media.assert_called_once_with("test_media")
|
||||||
|
file_mock.delete.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
def test_process_user_media(media_repo: MediaRepository) -> None:
|
||||||
|
"""Test process_user_media deletes all user files."""
|
||||||
|
args = MagicMock()
|
||||||
|
args.userid = "@test:domain.com"
|
||||||
|
args.dryrun = False
|
||||||
|
|
||||||
|
file1, file2 = MagicMock(), MagicMock()
|
||||||
|
media_repo.get_local_user_media = MagicMock(return_value=[file1, file2]) # type: ignore
|
||||||
|
|
||||||
|
process_user_media(media_repo, args)
|
||||||
|
|
||||||
|
media_repo.get_local_user_media.assert_called_once_with("@test:domain.com")
|
||||||
|
file1.delete.assert_called_once()
|
||||||
|
file2.delete.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_config_missing_file() -> None:
|
||||||
|
"""Test read_config with missing config file."""
|
||||||
|
with pytest.raises(SystemExit):
|
||||||
|
read_config("nonexistent.yaml")
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_config_invalid_content(tmp_path: Path) -> None:
|
||||||
|
"""Test read_config with invalid config content."""
|
||||||
|
config_file = tmp_path / "config.yaml"
|
||||||
|
config_file.write_text("invalid: true")
|
||||||
|
|
||||||
|
with pytest.raises(SystemExit):
|
||||||
|
read_config(config_file)
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_config_valid(tmp_path: Path) -> None:
|
||||||
|
"""Test read_config with valid config."""
|
||||||
|
config_file = tmp_path / "config.yaml"
|
||||||
|
config_file.write_text("""
|
||||||
|
media_api:
|
||||||
|
base_path: /media/path
|
||||||
|
database:
|
||||||
|
connection_string: postgresql://user:pass@localhost/db
|
||||||
|
""")
|
||||||
|
|
||||||
|
path, conn_string = read_config(config_file)
|
||||||
|
assert path == Path("/media/path")
|
||||||
|
assert conn_string == "postgresql://user:pass@localhost/db"
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_config_global_database(tmp_path: Path) -> None:
|
||||||
|
"""Test read_config gets connection string from global database section."""
|
||||||
|
config_file = tmp_path / "config.yaml"
|
||||||
|
config_file.write_text("""
|
||||||
|
media_api:
|
||||||
|
base_path: /media/path
|
||||||
|
global:
|
||||||
|
database:
|
||||||
|
connection_string: postgresql://global/db
|
||||||
|
""")
|
||||||
|
|
||||||
|
path, conn_string = read_config(config_file)
|
||||||
|
assert conn_string == "postgresql://global/db"
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_config_missing_conn_string(tmp_path: Path) -> None:
|
||||||
|
"""Test read_config exits when connection string is missing."""
|
||||||
|
config_file = tmp_path / "config.yaml"
|
||||||
|
config_file.write_text("""
|
||||||
|
media_api:
|
||||||
|
base_path: /media/path
|
||||||
|
database: {}
|
||||||
|
""")
|
||||||
|
|
||||||
|
with pytest.raises(SystemExit):
|
||||||
|
read_config(config_file)
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_config_missing_base_path(tmp_path: Path) -> None:
|
||||||
|
"""Test read_config exits when base_path is missing."""
|
||||||
|
config_file = tmp_path / "config.yaml"
|
||||||
|
config_file.write_text("""
|
||||||
|
media_api:
|
||||||
|
database:
|
||||||
|
connection_string: postgresql://fake/db
|
||||||
|
""")
|
||||||
|
|
||||||
|
with pytest.raises(SystemExit):
|
||||||
|
read_config(config_file)
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_options_defaults(mocker: MockerFixture) -> None:
|
||||||
|
"""Test parse_options default values."""
|
||||||
|
mocker.patch("sys.argv", ["cleanmedia"])
|
||||||
|
args = parse_options()
|
||||||
|
|
||||||
|
assert args.config == "config.yaml"
|
||||||
|
assert args.days == 30 # noqa PLR2004
|
||||||
|
assert not args.local
|
||||||
|
assert not args.dryrun
|
||||||
|
|
||||||
|
|
||||||
|
def test_file_has_thumbnails(media_repo: MediaRepository, mock_db_conn: Tuple[Any, Any]) -> None:
|
||||||
|
"""Test File.has_thumbnail returns correct count."""
|
||||||
|
_, cursor_mock = mock_db_conn
|
||||||
|
cursor_mock.fetchone.return_value = (3,)
|
||||||
|
|
||||||
|
file = File(media_repo, "mxid123", 1600000000, "abc123")
|
||||||
|
assert file.has_thumbnail() == 3 # noqa PLR2004
|
||||||
|
cursor_mock.execute.assert_called_with(
|
||||||
|
"SELECT COUNT(media_id) FROM mediaapi_thumbnail WHERE media_id = %s;",
|
||||||
|
("mxid123",),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_file_has_no_thumbnails(media_repo: MediaRepository, mock_db_conn: Tuple[Any, Any]) -> None:
|
||||||
|
"""Test File.has_thumbnail returns 0 when no thumbnails exist."""
|
||||||
|
_, cursor_mock = mock_db_conn
|
||||||
|
cursor_mock.fetchone.return_value = None
|
||||||
|
|
||||||
|
file = File(media_repo, "mxid123", 1600000000, "abc123")
|
||||||
|
assert file.has_thumbnail() == 0
|
752
uv.lock
generated
Normal file
752
uv.lock
generated
Normal file
@ -0,0 +1,752 @@
|
|||||||
|
version = 1
|
||||||
|
revision = 1
|
||||||
|
requires-python = ">=3.9, <4"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "attrs"
|
||||||
|
version = "25.3.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cattrs"
|
||||||
|
version = "24.1.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "attrs" },
|
||||||
|
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
|
||||||
|
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/29/7b/da4aa2f95afb2f28010453d03d6eedf018f9e085bd001f039e15731aba89/cattrs-24.1.3.tar.gz", hash = "sha256:981a6ef05875b5bb0c7fb68885546186d306f10f0f6718fe9b96c226e68821ff", size = 426684 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3c/ee/d68a3de23867a9156bab7e0a22fb9a0305067ee639032a22982cf7f725e7/cattrs-24.1.3-py3-none-any.whl", hash = "sha256:adf957dddd26840f27ffbd060a6c4dd3b2192c5b7c2c0525ef1bd8131d8a83f5", size = 66462 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfgv"
|
||||||
|
version = "3.4.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cleanmedia"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = { virtual = "." }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "psycopg2" },
|
||||||
|
{ name = "pyyaml" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dev-dependencies]
|
||||||
|
dev = [
|
||||||
|
{ name = "mypy" },
|
||||||
|
{ name = "pre-commit" },
|
||||||
|
{ name = "pylsp-mypy" },
|
||||||
|
{ name = "pytest" },
|
||||||
|
{ name = "pytest-coverage" },
|
||||||
|
{ name = "pytest-mock" },
|
||||||
|
{ name = "python-lsp-ruff" },
|
||||||
|
{ name = "python-lsp-server" },
|
||||||
|
{ name = "ruff" },
|
||||||
|
{ name = "types-psycopg2" },
|
||||||
|
{ name = "types-pyyaml" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata]
|
||||||
|
requires-dist = [
|
||||||
|
{ name = "psycopg2", specifier = ">=2.9.10,<3" },
|
||||||
|
{ name = "pyyaml", specifier = ">=6.0.2,<7" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata.requires-dev]
|
||||||
|
dev = [
|
||||||
|
{ name = "mypy", specifier = ">=1.13.0,<2" },
|
||||||
|
{ name = "pre-commit", specifier = ">=4.0.1,<5" },
|
||||||
|
{ name = "pylsp-mypy", specifier = ">=0.6.9,<0.7" },
|
||||||
|
{ name = "pytest", specifier = ">=8.3.4,<9" },
|
||||||
|
{ name = "pytest-coverage", specifier = ">=0.0,<0.1" },
|
||||||
|
{ name = "pytest-mock", specifier = ">=3.14.0,<4" },
|
||||||
|
{ name = "python-lsp-ruff", specifier = ">=2.2.2,<3" },
|
||||||
|
{ name = "python-lsp-server", specifier = ">=1.12.0,<2" },
|
||||||
|
{ name = "ruff", specifier = ">=0.8.1,<0.9" },
|
||||||
|
{ name = "types-psycopg2", specifier = ">=2.9.21.20241019,<3" },
|
||||||
|
{ name = "types-pyyaml", specifier = ">=6.0.12.20240917,<7" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorama"
|
||||||
|
version = "0.4.6"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "coverage"
|
||||||
|
version = "7.7.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/6b/bf/3effb7453498de9c14a81ca21e1f92e6723ce7ebdc5402ae30e4dcc490ac/coverage-7.7.1.tar.gz", hash = "sha256:199a1272e642266b90c9f40dec7fd3d307b51bf639fa0d15980dc0b3246c1393", size = 810332 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/66/b3/b3d86d8e534747e817f63bbb0ebf696fd44f37ae07e52dd0cc74c95a0542/coverage-7.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:553ba93f8e3c70e1b0031e4dfea36aba4e2b51fe5770db35e99af8dc5c5a9dfe", size = 210948 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/12/1d/844f3bf5b7bced37acbae50f463788f4d7c5977a27563214d89ebfe90941/coverage-7.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:44683f2556a56c9a6e673b583763096b8efbd2df022b02995609cf8e64fc8ae0", size = 211385 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d4/b5/0866a89d0818d471437d73b66a3aff73890a09246a97b7dc273189fffa75/coverage-7.7.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02fad4f8faa4153db76f9246bc95c1d99f054f4e0a884175bff9155cf4f856cb", size = 240510 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/14/d0/7a1f41d04081a8e0b95e6db2f9a598c94b3dfe60c5e8b2ffb3ac74347420/coverage-7.7.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c181ceba2e6808ede1e964f7bdc77bd8c7eb62f202c63a48cc541e5ffffccb6", size = 238420 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/72/4e/aa470597ceaee2ab0ec973ee2760f177a728144d1dca3c866a35a04b3798/coverage-7.7.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80b5b207a8b08c6a934b214e364cab2fa82663d4af18981a6c0a9e95f8df7602", size = 239557 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/49/7b/0267bd6465dbfe97f55de1f57f1bd54c7b2ed796a0db68ac6ea6f39c51b4/coverage-7.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:25fe40967717bad0ce628a0223f08a10d54c9d739e88c9cbb0f77b5959367542", size = 239466 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/65/e3/898fe437b7bc37f70b3742010cc0faf2f00c5abbe79961c54c6c5cda903c/coverage-7.7.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:881cae0f9cbd928c9c001487bb3dcbfd0b0af3ef53ae92180878591053be0cb3", size = 238184 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cf/92/84ea2e213b7ac09ea4f04038863775a080aec06812d39da8c21ce612af2b/coverage-7.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c90e9141e9221dd6fbc16a2727a5703c19443a8d9bf7d634c792fa0287cee1ab", size = 238479 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/70/d4/1acf676058541b00cf7b64a8422cf871cebd4c718e067db18d84018a4e0b/coverage-7.7.1-cp310-cp310-win32.whl", hash = "sha256:ae13ed5bf5542d7d4a0a42ff5160e07e84adc44eda65ddaa635c484ff8e55917", size = 213521 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ba/36/9a490e442961d3af01c420498c078fa2ac1abf4a248c80b0ac7199f31f98/coverage-7.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:171e9977c6a5d2b2be9efc7df1126fd525ce7cad0eb9904fe692da007ba90d81", size = 214418 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c2/4c/5118ca60ed4141ec940c8cbaf1b2ebe8911be0f03bfc028c99f63de82c44/coverage-7.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1165490be0069e34e4f99d08e9c5209c463de11b471709dfae31e2a98cbd49fd", size = 211064 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e8/6c/0e9aac4cf5dba49feede79109fdfd2fafca3bdbc02992bcf9b25d58351dd/coverage-7.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:44af11c00fd3b19b8809487630f8a0039130d32363239dfd15238e6d37e41a48", size = 211501 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/23/1a/570666f276815722f0a94f92b61e7123d66b166238e0f9f224f1a38f17cf/coverage-7.7.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fbba59022e7c20124d2f520842b75904c7b9f16c854233fa46575c69949fb5b9", size = 244128 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e8/0d/cb23f89eb8c7018429c6cf8cc436b4eb917f43e81354d99c86c435ab1813/coverage-7.7.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:af94fb80e4f159f4d93fb411800448ad87b6039b0500849a403b73a0d36bb5ae", size = 241818 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/54/fd/584a5d099bba4e79ac3893d57e0bd53034f7187c30f940e6a581bfd38c8f/coverage-7.7.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eae79f8e3501133aa0e220bbc29573910d096795882a70e6f6e6637b09522133", size = 243602 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/78/d7/a28b6a5ee64ff1e4a66fbd8cd7b9372471c951c3a0c4ec9d1d0f47819f53/coverage-7.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e33426a5e1dc7743dd54dfd11d3a6c02c5d127abfaa2edd80a6e352b58347d1a", size = 243247 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b2/9e/210814fae81ea7796f166529a32b443dead622a8c1ad315d0779520635c6/coverage-7.7.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b559adc22486937786731dac69e57296cb9aede7e2687dfc0d2696dbd3b1eb6b", size = 241422 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/99/5e/80ed1955fa8529bdb72dc11c0a3f02a838285250c0e14952e39844993102/coverage-7.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b838a91e84e1773c3436f6cc6996e000ed3ca5721799e7789be18830fad009a2", size = 241958 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7e/26/f0bafc8103284febc4e3a3cd947b49ff36c50711daf3d03b3e11b23bc73a/coverage-7.7.1-cp311-cp311-win32.whl", hash = "sha256:2c492401bdb3a85824669d6a03f57b3dfadef0941b8541f035f83bbfc39d4282", size = 213571 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c1/fe/fef0a0201af72422fb9634b5c6079786bb405ac09cce5661fdd54a66e773/coverage-7.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:1e6f867379fd033a0eeabb1be0cffa2bd660582b8b0c9478895c509d875a9d9e", size = 214488 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cf/b0/4eaba302a86ec3528231d7cfc954ae1929ec5d42b032eb6f5b5f5a9155d2/coverage-7.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:eff187177d8016ff6addf789dcc421c3db0d014e4946c1cc3fbf697f7852459d", size = 211253 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fd/68/21b973e6780a3f2457e31ede1aca6c2f84bda4359457b40da3ae805dcf30/coverage-7.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2444fbe1ba1889e0b29eb4d11931afa88f92dc507b7248f45be372775b3cef4f", size = 211504 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d1/b4/c19e9c565407664390254252496292f1e3076c31c5c01701ffacc060e745/coverage-7.7.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:177d837339883c541f8524683e227adcaea581eca6bb33823a2a1fdae4c988e1", size = 245566 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7b/0e/f9829cdd25e5083638559c8c267ff0577c6bab19dacb1a4fcfc1e70e41c0/coverage-7.7.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:15d54ecef1582b1d3ec6049b20d3c1a07d5e7f85335d8a3b617c9960b4f807e0", size = 242455 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/29/57/a3ada2e50a665bf6d9851b5eb3a9a07d7e38f970bdd4d39895f311331d56/coverage-7.7.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75c82b27c56478d5e1391f2e7b2e7f588d093157fa40d53fd9453a471b1191f2", size = 244713 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0f/d3/f15c7d45682a73eca0611427896016bad4c8f635b0fc13aae13a01f8ed9d/coverage-7.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:315ff74b585110ac3b7ab631e89e769d294f303c6d21302a816b3554ed4c81af", size = 244476 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/19/3b/64540074e256082b220e8810fd72543eff03286c59dc91976281dc0a559c/coverage-7.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4dd532dac197d68c478480edde74fd4476c6823355987fd31d01ad9aa1e5fb59", size = 242695 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8a/c1/9cad25372ead7f9395a91bb42d8ae63e6cefe7408eb79fd38797e2b763eb/coverage-7.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:385618003e3d608001676bb35dc67ae3ad44c75c0395d8de5780af7bb35be6b2", size = 243888 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/66/c6/c3e6c895bc5b95ccfe4cb5838669dbe5226ee4ad10604c46b778c304d6f9/coverage-7.7.1-cp312-cp312-win32.whl", hash = "sha256:63306486fcb5a827449464f6211d2991f01dfa2965976018c9bab9d5e45a35c8", size = 213744 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cc/8a/6df2fcb4c3e38ec6cd7e211ca8391405ada4e3b1295695d00aa07c6ee736/coverage-7.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:37351dc8123c154fa05b7579fdb126b9f8b1cf42fd6f79ddf19121b7bdd4aa04", size = 214546 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ec/2a/1a254eaadb01c163b29d6ce742aa380fc5cfe74a82138ce6eb944c42effa/coverage-7.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:eebd927b86761a7068a06d3699fd6c20129becf15bb44282db085921ea0f1585", size = 211277 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cf/00/9636028365efd4eb6db71cdd01d99e59f25cf0d47a59943dbee32dd1573b/coverage-7.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2a79c4a09765d18311c35975ad2eb1ac613c0401afdd9cb1ca4110aeb5dd3c4c", size = 211551 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6f/c8/14aed97f80363f055b6cd91e62986492d9fe3b55e06b4b5c82627ae18744/coverage-7.7.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b1c65a739447c5ddce5b96c0a388fd82e4bbdff7251396a70182b1d83631019", size = 245068 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d6/76/9c5fe3f900e01d7995b0cda08fc8bf9773b4b1be58bdd626f319c7d4ec11/coverage-7.7.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:392cc8fd2b1b010ca36840735e2a526fcbd76795a5d44006065e79868cc76ccf", size = 242109 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c0/81/760993bb536fb674d3a059f718145dcd409ed6d00ae4e3cbf380019fdfd0/coverage-7.7.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9bb47cc9f07a59a451361a850cb06d20633e77a9118d05fd0f77b1864439461b", size = 244129 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/00/be/1114a19f93eae0b6cd955dabb5bee80397bd420d846e63cd0ebffc134e3d/coverage-7.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b4c144c129343416a49378e05c9451c34aae5ccf00221e4fa4f487db0816ee2f", size = 244201 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/06/8d/9128fd283c660474c7dc2b1ea5c66761bc776b970c1724989ed70e9d6eee/coverage-7.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bc96441c9d9ca12a790b5ae17d2fa6654da4b3962ea15e0eabb1b1caed094777", size = 242282 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d4/2a/6d7dbfe9c1f82e2cdc28d48f4a0c93190cf58f057fa91ba2391b92437fe6/coverage-7.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3d03287eb03186256999539d98818c425c33546ab4901028c8fa933b62c35c3a", size = 243570 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cf/3e/29f1e4ce3bb951bcf74b2037a82d94c5064b3334304a3809a95805628838/coverage-7.7.1-cp313-cp313-win32.whl", hash = "sha256:8fed429c26b99641dc1f3a79179860122b22745dd9af36f29b141e178925070a", size = 213772 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bc/3a/cf029bf34aefd22ad34f0e808eba8d5830f297a1acb483a2124f097ff769/coverage-7.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:092b134129a8bb940c08b2d9ceb4459af5fb3faea77888af63182e17d89e1cf1", size = 214575 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/92/4c/fb8b35f186a2519126209dce91ab8644c9a901cf04f8dfa65576ca2dd9e8/coverage-7.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3154b369141c3169b8133973ac00f63fcf8d6dbcc297d788d36afbb7811e511", size = 212113 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/59/90/e834ffc86fd811c5b570a64ee1895b20404a247ec18a896b9ba543b12097/coverage-7.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:264ff2bcce27a7f455b64ac0dfe097680b65d9a1a293ef902675fa8158d20b24", size = 212333 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a5/a1/27f0ad39569b3b02410b881c42e58ab403df13fcd465b475db514b83d3d3/coverage-7.7.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba8480ebe401c2f094d10a8c4209b800a9b77215b6c796d16b6ecdf665048950", size = 256566 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9f/3b/21fa66a1db1b90a0633e771a32754f7c02d60236a251afb1b86d7e15d83a/coverage-7.7.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:520af84febb6bb54453e7fbb730afa58c7178fd018c398a8fcd8e269a79bf96d", size = 252276 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d6/e5/4ab83a59b0f8ac4f0029018559fc4c7d042e1b4552a722e2bfb04f652296/coverage-7.7.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88d96127ae01ff571d465d4b0be25c123789cef88ba0879194d673fdea52f54e", size = 254616 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/db/7a/4224417c0ccdb16a5ba4d8d1fcfaa18439be1624c29435bb9bc88ccabdfb/coverage-7.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:0ce92c5a9d7007d838456f4b77ea159cb628187a137e1895331e530973dcf862", size = 255707 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/51/20/ff18a329ccaa3d035e2134ecf3a2e92a52d3be6704c76e74ca5589ece260/coverage-7.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0dab4ef76d7b14f432057fdb7a0477e8bffca0ad39ace308be6e74864e632271", size = 253876 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e4/e8/1d6f1a6651672c64f45ffad05306dad9c4c189bec694270822508049b2cb/coverage-7.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7e688010581dbac9cab72800e9076e16f7cccd0d89af5785b70daa11174e94de", size = 254687 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6b/ea/1b9a14cf3e2bc3fd9de23a336a8082091711c5f480b500782d59e84a8fe5/coverage-7.7.1-cp313-cp313t-win32.whl", hash = "sha256:e52eb31ae3afacdacfe50705a15b75ded67935770c460d88c215a9c0c40d0e9c", size = 214486 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cc/bb/faa6bcf769cb7b3b660532a30d77c440289b40636c7f80e498b961295d07/coverage-7.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a6b6b3bd121ee2ec4bd35039319f3423d0be282b9752a5ae9f18724bc93ebe7c", size = 215647 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/18/8a/c99478521fb4ea0370c72c8ce2c6a90973eeaccdb9e87b61b30ee090ace2/coverage-7.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:34a3bf6b92e6621fc4dcdaab353e173ccb0ca9e4bfbcf7e49a0134c86c9cd303", size = 210947 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e0/8c/fefae75825b99ac4b845ec48b7d585269838a10907bbcb1015b086b64b9f/coverage-7.7.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d6874929d624d3a670f676efafbbc747f519a6121b581dd41d012109e70a5ebd", size = 211369 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4f/bf/1a48c4b3cf533910bb52e6bf18666a11ec19dfba01330b89e51df0269259/coverage-7.7.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ba5ff236c87a7b7aa1441a216caf44baee14cbfbd2256d306f926d16b026578", size = 240136 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9d/63/22684c48520501edd4cb62ee27c016ea74935bf6a435d23c9cd75bd51c15/coverage-7.7.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:452735fafe8ff5918236d5fe1feac322b359e57692269c75151f9b4ee4b7e1bc", size = 238062 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/15/2b/a1f704e986671bbeffd1edb1e9ec6a98c4bfe40db3e6789147f008136256/coverage-7.7.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5f99a93cecf799738e211f9746dc83749b5693538fbfac279a61682ba309387", size = 239162 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/db/5a/5d98375fe1a42a61709c96b51b89959a537099c912d63cfe6b362b415fbe/coverage-7.7.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:11dd6f52c2a7ce8bf0a5f3b6e4a8eb60e157ffedc3c4b4314a41c1dfbd26ce58", size = 238938 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a1/1b/ac5cac616de92c7a8dae17c585e86dcda95f2c39bc438c91ed1c9a5893ba/coverage-7.7.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:b52edb940d087e2a96e73c1523284a2e94a4e66fa2ea1e2e64dddc67173bad94", size = 237164 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4f/19/ce7cb8e9269b74da0b4a337a78b1ff8896d8584b692e6b67c7bf53b15b1c/coverage-7.7.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:d2e73e2ac468536197e6b3ab79bc4a5c9da0f078cd78cfcc7fe27cf5d1195ef0", size = 238144 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/60/3a/bcd8e5c0bb76e1c9a275e817498e80d1f7cdc7020c941d616d9d22487776/coverage-7.7.1-cp39-cp39-win32.whl", hash = "sha256:18f544356bceef17cc55fcf859e5664f06946c1b68efcea6acdc50f8f6a6e776", size = 213542 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/78/b4/d9f39df48a336276582890c3833485ba43329d40aa4e269292c52408b1fb/coverage-7.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:d66ff48ab3bb6f762a153e29c0fc1eb5a62a260217bc64470d7ba602f5886d20", size = 214427 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f9/4e/a501ec475ed455c1ee1570063527afe2c06ab1039f8ff18eefecfbdac8fd/coverage-7.7.1-pp39.pp310.pp311-none-any.whl", hash = "sha256:5b7b02e50d54be6114cc4f6a3222fec83164f7c42772ba03b520138859b5fde1", size = 203014 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/52/26/9f53293ff4cc1d47d98367ce045ca2e62746d6be74a5c6851a474eabf59b/coverage-7.7.1-py3-none-any.whl", hash = "sha256:822fa99dd1ac686061e1219b67868e25d9757989cf2259f735a4802497d6da31", size = 203006 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.optional-dependencies]
|
||||||
|
toml = [
|
||||||
|
{ name = "tomli", marker = "python_full_version <= '3.11'" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "distlib"
|
||||||
|
version = "0.3.9"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "docstring-to-markdown"
|
||||||
|
version = "0.16"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "importlib-metadata" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/58/1f/16e6d4026e94224d662dcd344dcd3563bc8a74a272f2c27b325793cad5fc/docstring_to_markdown-0.16.tar.gz", hash = "sha256:097bf502fdf040b0d019688a7cc1abb89b98196801448721740e8aa3e5075627", size = 31157 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/47/a6/bf7cda3cc85ad1f8cfbf1ede45ade2c5c25f0a315f8a11e4a7943de66af7/docstring_to_markdown-0.16-py3-none-any.whl", hash = "sha256:f92cc42357b2c932f70ca2ebc79f7805039a34011ad381c1b6ac3481e81596ce", size = 23187 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "exceptiongroup"
|
||||||
|
version = "1.2.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "filelock"
|
||||||
|
version = "3.18.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "identify"
|
||||||
|
version = "2.6.9"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/9b/98/a71ab060daec766acc30fb47dfca219d03de34a70d616a79a38c6066c5bf/identify-2.6.9.tar.gz", hash = "sha256:d40dfe3142a1421d8518e3d3985ef5ac42890683e32306ad614a29490abeb6bf", size = 99249 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/07/ce/0845144ed1f0e25db5e7a79c2354c1da4b5ce392b8966449d5db8dca18f1/identify-2.6.9-py2.py3-none-any.whl", hash = "sha256:c98b4322da415a8e5a70ff6e51fbc2d2932c015532d77e9f8537b4ba7813b150", size = 99101 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "importlib-metadata"
|
||||||
|
version = "8.6.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "zipp" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/33/08/c1395a292bb23fd03bdf572a1357c5a733d3eecbab877641ceacab23db6e/importlib_metadata-8.6.1.tar.gz", hash = "sha256:310b41d755445d74569f993ccfc22838295d9fe005425094fad953d7f15c8580", size = 55767 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/79/9d/0fb148dc4d6fa4a7dd1d8378168d9b4cd8d4560a6fbf6f0121c5fc34eb68/importlib_metadata-8.6.1-py3-none-any.whl", hash = "sha256:02a89390c1e15fdfdc0d7c6b25cb3e62650d0494005c97d6f148bf5b9787525e", size = 26971 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iniconfig"
|
||||||
|
version = "2.1.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jedi"
|
||||||
|
version = "0.19.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "parso" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lsprotocol"
|
||||||
|
version = "2023.0.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "attrs" },
|
||||||
|
{ name = "cattrs" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/9d/f6/6e80484ec078d0b50699ceb1833597b792a6c695f90c645fbaf54b947e6f/lsprotocol-2023.0.1.tar.gz", hash = "sha256:cc5c15130d2403c18b734304339e51242d3018a05c4f7d0f198ad6e0cd21861d", size = 69434 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8d/37/2351e48cb3309673492d3a8c59d407b75fb6630e560eb27ecd4da03adc9a/lsprotocol-2023.0.1-py3-none-any.whl", hash = "sha256:c75223c9e4af2f24272b14c6375787438279369236cd568f596d4951052a60f2", size = 70826 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mypy"
|
||||||
|
version = "1.15.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "mypy-extensions" },
|
||||||
|
{ name = "tomli", marker = "python_full_version < '3.11'" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ce/43/d5e49a86afa64bd3839ea0d5b9c7103487007d728e1293f52525d6d5486a/mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43", size = 3239717 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/68/f8/65a7ce8d0e09b6329ad0c8d40330d100ea343bd4dd04c4f8ae26462d0a17/mypy-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:979e4e1a006511dacf628e36fadfecbcc0160a8af6ca7dad2f5025529e082c13", size = 10738433 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b4/95/9c0ecb8eacfe048583706249439ff52105b3f552ea9c4024166c03224270/mypy-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c4bb0e1bd29f7d34efcccd71cf733580191e9a264a2202b0239da95984c5b559", size = 9861472 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/84/09/9ec95e982e282e20c0d5407bc65031dfd0f0f8ecc66b69538296e06fcbee/mypy-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be68172e9fd9ad8fb876c6389f16d1c1b5f100ffa779f77b1fb2176fcc9ab95b", size = 11611424 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/78/13/f7d14e55865036a1e6a0a69580c240f43bc1f37407fe9235c0d4ef25ffb0/mypy-1.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7be1e46525adfa0d97681432ee9fcd61a3964c2446795714699a998d193f1a3", size = 12365450 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/48/e1/301a73852d40c241e915ac6d7bcd7fedd47d519246db2d7b86b9d7e7a0cb/mypy-1.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2e2c2e6d3593f6451b18588848e66260ff62ccca522dd231cd4dd59b0160668b", size = 12551765 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/77/ba/c37bc323ae5fe7f3f15a28e06ab012cd0b7552886118943e90b15af31195/mypy-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:6983aae8b2f653e098edb77f893f7b6aca69f6cffb19b2cc7443f23cce5f4828", size = 9274701 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/03/bc/f6339726c627bd7ca1ce0fa56c9ae2d0144604a319e0e339bdadafbbb599/mypy-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2922d42e16d6de288022e5ca321cd0618b238cfc5570e0263e5ba0a77dbef56f", size = 10662338 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e2/90/8dcf506ca1a09b0d17555cc00cd69aee402c203911410136cd716559efe7/mypy-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2ee2d57e01a7c35de00f4634ba1bbf015185b219e4dc5909e281016df43f5ee5", size = 9787540 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/05/05/a10f9479681e5da09ef2f9426f650d7b550d4bafbef683b69aad1ba87457/mypy-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:973500e0774b85d9689715feeffcc980193086551110fd678ebe1f4342fb7c5e", size = 11538051 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e9/9a/1f7d18b30edd57441a6411fcbc0c6869448d1a4bacbaee60656ac0fc29c8/mypy-1.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a95fb17c13e29d2d5195869262f8125dfdb5c134dc8d9a9d0aecf7525b10c2c", size = 12286751 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/72/af/19ff499b6f1dafcaf56f9881f7a965ac2f474f69f6f618b5175b044299f5/mypy-1.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1905f494bfd7d85a23a88c5d97840888a7bd516545fc5aaedff0267e0bb54e2f", size = 12421783 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/96/39/11b57431a1f686c1aed54bf794870efe0f6aeca11aca281a0bd87a5ad42c/mypy-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:c9817fa23833ff189db061e6d2eff49b2f3b6ed9856b4a0a73046e41932d744f", size = 9265618 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/98/3a/03c74331c5eb8bd025734e04c9840532226775c47a2c39b56a0c8d4f128d/mypy-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aea39e0583d05124836ea645f412e88a5c7d0fd77a6d694b60d9b6b2d9f184fd", size = 10793981 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f0/1a/41759b18f2cfd568848a37c89030aeb03534411eef981df621d8fad08a1d/mypy-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f2147ab812b75e5b5499b01ade1f4a81489a147c01585cda36019102538615f", size = 9749175 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/12/7e/873481abf1ef112c582db832740f4c11b2bfa510e829d6da29b0ab8c3f9c/mypy-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce436f4c6d218a070048ed6a44c0bbb10cd2cc5e272b29e7845f6a2f57ee4464", size = 11455675 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b3/d0/92ae4cde706923a2d3f2d6c39629134063ff64b9dedca9c1388363da072d/mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee", size = 12410020 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/46/8b/df49974b337cce35f828ba6fda228152d6db45fed4c86ba56ffe442434fd/mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e", size = 12498582 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/13/50/da5203fcf6c53044a0b699939f31075c45ae8a4cadf538a9069b165c1050/mypy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22", size = 9366614 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6a/9b/fd2e05d6ffff24d912f150b87db9e364fa8282045c875654ce7e32fffa66/mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445", size = 10788592 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/74/37/b246d711c28a03ead1fd906bbc7106659aed7c089d55fe40dd58db812628/mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d", size = 9753611 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a6/ac/395808a92e10cfdac8003c3de9a2ab6dc7cde6c0d2a4df3df1b815ffd067/mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5", size = 11438443 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d2/8b/801aa06445d2de3895f59e476f38f3f8d610ef5d6908245f07d002676cbf/mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036", size = 12402541 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c7/67/5a4268782eb77344cc613a4cf23540928e41f018a9a1ec4c6882baf20ab8/mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357", size = 12494348 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/83/3e/57bb447f7bbbfaabf1712d96f9df142624a386d98fb026a761532526057e/mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf", size = 9373648 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5a/fa/79cf41a55b682794abe71372151dbbf856e3008f6767057229e6649d294a/mypy-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e601a7fa172c2131bff456bb3ee08a88360760d0d2f8cbd7a75a65497e2df078", size = 10737129 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d3/33/dd8feb2597d648de29e3da0a8bf4e1afbda472964d2a4a0052203a6f3594/mypy-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:712e962a6357634fef20412699a3655c610110e01cdaa6180acec7fc9f8513ba", size = 9856335 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e4/b5/74508959c1b06b96674b364ffeb7ae5802646b32929b7701fc6b18447592/mypy-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95579473af29ab73a10bada2f9722856792a36ec5af5399b653aa28360290a5", size = 11611935 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6c/53/da61b9d9973efcd6507183fdad96606996191657fe79701b2c818714d573/mypy-1.15.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f8722560a14cde92fdb1e31597760dc35f9f5524cce17836c0d22841830fd5b", size = 12365827 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c1/72/965bd9ee89540c79a25778cc080c7e6ef40aa1eeac4d52cec7eae6eb5228/mypy-1.15.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1fbb8da62dc352133d7d7ca90ed2fb0e9d42bb1a32724c287d3c76c58cbaa9c2", size = 12541924 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/46/d0/f41645c2eb263e6c77ada7d76f894c580c9ddb20d77f0c24d34273a4dab2/mypy-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:d10d994b41fb3497719bbf866f227b3489048ea4bbbb5015357db306249f7980", size = 9271176 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/09/4e/a7d65c7322c510de2c409ff3828b03354a7c43f5a8ed458a7a131b41c7b9/mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e", size = 2221777 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mypy-extensions"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nodeenv"
|
||||||
|
version = "1.9.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "packaging"
|
||||||
|
version = "24.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parso"
|
||||||
|
version = "0.8.4"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/66/94/68e2e17afaa9169cf6412ab0f28623903be73d1b32e208d9e8e541bb086d/parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d", size = 400609 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c6/ac/dac4a63f978e4dcb3c6d3a78c4d8e0192a113d288502a1216950c41b1027/parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18", size = 103650 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "platformdirs"
|
||||||
|
version = "4.3.7"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/b6/2d/7d512a3913d60623e7eb945c6d1b4f0bddf1d0b7ada5225274c87e5b53d1/platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351", size = 21291 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6d/45/59578566b3275b8fd9157885918fcd0c4d74162928a5310926887b856a51/platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94", size = 18499 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pluggy"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pre-commit"
|
||||||
|
version = "4.2.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "cfgv" },
|
||||||
|
{ name = "identify" },
|
||||||
|
{ name = "nodeenv" },
|
||||||
|
{ name = "pyyaml" },
|
||||||
|
{ name = "virtualenv" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/08/39/679ca9b26c7bb2999ff122d50faa301e49af82ca9c066ec061cfbc0c6784/pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146", size = 193424 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd", size = 220707 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "psycopg2"
|
||||||
|
version = "2.9.10"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/62/51/2007ea29e605957a17ac6357115d0c1a1b60c8c984951c19419b3474cdfd/psycopg2-2.9.10.tar.gz", hash = "sha256:12ec0b40b0273f95296233e8750441339298e6a572f7039da5b260e3c8b60e11", size = 385672 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0a/a9/146b6bdc0d33539a359f5e134ee6dda9173fb8121c5b96af33fa299e50c4/psycopg2-2.9.10-cp310-cp310-win32.whl", hash = "sha256:5df2b672140f95adb453af93a7d669d7a7bf0a56bcd26f1502329166f4a61716", size = 1024527 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/47/50/c509e56f725fd2572b59b69bd964edaf064deebf1c896b2452f6b46fdfb3/psycopg2-2.9.10-cp310-cp310-win_amd64.whl", hash = "sha256:c6f7b8561225f9e711a9c47087388a97fdc948211c10a4bccbf0ba68ab7b3b5a", size = 1163735 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/20/a2/c51ca3e667c34e7852157b665e3d49418e68182081060231d514dd823225/psycopg2-2.9.10-cp311-cp311-win32.whl", hash = "sha256:47c4f9875125344f4c2b870e41b6aad585901318068acd01de93f3677a6522c2", size = 1024538 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/33/39/5a9a229bb5414abeb86e33b8fc8143ab0aecce5a7f698a53e31367d30caa/psycopg2-2.9.10-cp311-cp311-win_amd64.whl", hash = "sha256:0435034157049f6846e95103bd8f5a668788dd913a7c30162ca9503fdf542cb4", size = 1163736 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3d/16/4623fad6076448df21c1a870c93a9774ad8a7b4dd1660223b59082dd8fec/psycopg2-2.9.10-cp312-cp312-win32.whl", hash = "sha256:65a63d7ab0e067e2cdb3cf266de39663203d38d6a8ed97f5ca0cb315c73fe067", size = 1025113 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/66/de/baed128ae0fc07460d9399d82e631ea31a1f171c0c4ae18f9808ac6759e3/psycopg2-2.9.10-cp312-cp312-win_amd64.whl", hash = "sha256:4a579d6243da40a7b3182e0430493dbd55950c493d8c68f4eec0b302f6bbf20e", size = 1163951 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ae/49/a6cfc94a9c483b1fa401fbcb23aca7892f60c7269c5ffa2ac408364f80dc/psycopg2-2.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:91fd603a2155da8d0cfcdbf8ab24a2d54bca72795b90d2a3ed2b6da8d979dee2", size = 2569060 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5f/29/bc9639b9c50abd93a8274fd2deffbf70b2a65aa9e7881e63ea6bc4319e84/psycopg2-2.9.10-cp39-cp39-win32.whl", hash = "sha256:9d5b3b94b79a844a986d029eee38998232451119ad653aea42bb9220a8c5066b", size = 1025259 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2c/f8/0be7d99d24656b689d83ac167240c3527efb0b161d814fb1dd58329ddf75/psycopg2-2.9.10-cp39-cp39-win_amd64.whl", hash = "sha256:88138c8dedcbfa96408023ea2b0c369eda40fe5d75002c0964c78f46f11fa442", size = 1163878 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pylsp-mypy"
|
||||||
|
version = "0.6.9"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "mypy" },
|
||||||
|
{ name = "python-lsp-server" },
|
||||||
|
{ name = "tomli", marker = "python_full_version < '3.11'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/9e/5b/d0a6c54ae8863bec81911965047d92662a13bc38dfb6fee6388d8419a72b/pylsp_mypy-0.6.9.tar.gz", hash = "sha256:d28994a6ba123c3918ce3274a5cad768418650e575001002101c425ad6c7bbaa", size = 16908 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f3/41/6f7db581bcd31895b4e5acdfd25f49a278898babda3f01a6d224b611b119/pylsp_mypy-0.6.9-py3-none-any.whl", hash = "sha256:9b73ece2977b22b5fc75dfec1cc491bf2031955e3da4062d924a5cc22a278151", size = 11472 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest"
|
||||||
|
version = "8.3.5"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||||
|
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
|
||||||
|
{ name = "iniconfig" },
|
||||||
|
{ name = "packaging" },
|
||||||
|
{ name = "pluggy" },
|
||||||
|
{ name = "tomli", marker = "python_full_version < '3.11'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest-cov"
|
||||||
|
version = "6.0.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "coverage", extra = ["toml"] },
|
||||||
|
{ name = "pytest" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/be/45/9b538de8cef30e17c7b45ef42f538a94889ed6a16f2387a6c89e73220651/pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0", size = 66945 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/36/3b/48e79f2cd6a61dbbd4807b4ed46cb564b4fd50a76166b1c4ea5c1d9e2371/pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35", size = 22949 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest-cover"
|
||||||
|
version = "3.0.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "pytest-cov" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/30/27/20964101a7cdb260f8d6c4e854659026968321d10c90552b1fe7f6c5f913/pytest-cover-3.0.0.tar.gz", hash = "sha256:5bdb6c1cc3dd75583bb7bc2c57f5e1034a1bfcb79d27c71aceb0b16af981dbf4", size = 3211 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/71/9b/7b4700c462628e169bd859c6368d596a6aedc87936bde733bead9f875fce/pytest_cover-3.0.0-py2.py3-none-any.whl", hash = "sha256:578249955eb3b5f3991209df6e532bb770b647743b7392d3d97698dc02f39ebb", size = 3769 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest-coverage"
|
||||||
|
version = "0.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "pytest-cover" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/01/81/1d954849aed17b254d1c397eb4447a05eedce612a56b627c071df2ce00c1/pytest-coverage-0.0.tar.gz", hash = "sha256:db6af2cbd7e458c7c9fd2b4207cee75258243c8a81cad31a7ee8cfad5be93c05", size = 873 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5b/4b/d95b052f87db89a2383233c0754c45f6d3b427b7a4bcb771ac9316a6fae1/pytest_coverage-0.0-py2.py3-none-any.whl", hash = "sha256:dedd084c5e74d8e669355325916dc011539b190355021b037242514dee546368", size = 2013 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest-mock"
|
||||||
|
version = "3.14.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "pytest" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/c6/90/a955c3ab35ccd41ad4de556596fa86685bf4fc5ffcc62d22d856cfd4e29a/pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0", size = 32814 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f2/3b/b26f90f74e2986a82df6e7ac7e319b8ea7ccece1caec9f8ab6104dc70603/pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f", size = 9863 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "python-lsp-jsonrpc"
|
||||||
|
version = "1.1.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "ujson" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/48/b6/fd92e2ea4635d88966bb42c20198df1a981340f07843b5e3c6694ba3557b/python-lsp-jsonrpc-1.1.2.tar.gz", hash = "sha256:4688e453eef55cd952bff762c705cedefa12055c0aec17a06f595bcc002cc912", size = 15298 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cb/d9/656659d5b5d5f402b2b174cd0ba9bc827e07ce3c0bf88da65424baf64af8/python_lsp_jsonrpc-1.1.2-py3-none-any.whl", hash = "sha256:7339c2e9630ae98903fdaea1ace8c47fba0484983794d6aafd0bd8989be2b03c", size = 8805 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "python-lsp-ruff"
|
||||||
|
version = "2.2.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "cattrs" },
|
||||||
|
{ name = "lsprotocol" },
|
||||||
|
{ name = "python-lsp-server" },
|
||||||
|
{ name = "ruff" },
|
||||||
|
{ name = "tomli", marker = "python_full_version < '3.11'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ea/ec/475febe2f9e799f44afa476a2c0e063368d4289a65b80457ed737f6d05c0/python_lsp_ruff-2.2.2.tar.gz", hash = "sha256:3f80bdb0b4a8ee24624596a1cff60b28cc37771773730f9bf7d946ddff9f0cac", size = 15951 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c4/b1/d09777c49a5273d9a79fca24341284d588203dc8587120300e3f86d43858/python_lsp_ruff-2.2.2-py3-none-any.whl", hash = "sha256:7034d16c5cfdf07e932195649ebef569a7ddfcc5853fb2fee05fa7fc739afe3a", size = 11256 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "python-lsp-server"
|
||||||
|
version = "1.12.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "docstring-to-markdown" },
|
||||||
|
{ name = "importlib-metadata", marker = "python_full_version < '3.10'" },
|
||||||
|
{ name = "jedi" },
|
||||||
|
{ name = "pluggy" },
|
||||||
|
{ name = "python-lsp-jsonrpc" },
|
||||||
|
{ name = "ujson" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/cc/0f/3d63c5f37edca529a2a003a30add97dcce67a83a99dd932528f623aa1df9/python_lsp_server-1.12.2.tar.gz", hash = "sha256:fea039a36b3132774d0f803671184cf7dde0c688e7b924f23a6359a66094126d", size = 115054 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cb/e7/28010a326ef591e1409daf9d57a47de94156c147ad1befe74d8196d82729/python_lsp_server-1.12.2-py3-none-any.whl", hash = "sha256:750116459449184ba20811167cdf96f91296ae12f1f65ebd975c5c159388111b", size = 74773 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyyaml"
|
||||||
|
version = "6.0.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/65/d8/b7a1db13636d7fb7d4ff431593c510c8b8fca920ade06ca8ef20015493c5/PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", size = 184777 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0a/02/6ec546cd45143fdf9840b2c6be8d875116a64076218b61d68e12548e5839/PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", size = 172318 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0e/9a/8cc68be846c972bda34f6c2a93abb644fb2476f4dcc924d52175786932c9/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", size = 720891 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e9/6c/6e1b7f40181bc4805e2e07f4abc10a88ce4648e7e95ff1abe4ae4014a9b2/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", size = 722614 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3d/32/e7bd8535d22ea2874cef6a81021ba019474ace0d13a4819c2a4bce79bd6a/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", size = 737360 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d7/12/7322c1e30b9be969670b672573d45479edef72c9a0deac3bb2868f5d7469/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", size = 699006 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/82/72/04fcad41ca56491995076630c3ec1e834be241664c0c09a64c9a2589b507/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", size = 723577 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ed/5e/46168b1f2757f1fcd442bc3029cd8767d88a98c9c05770d8b420948743bb/PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", size = 144593 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/19/87/5124b1c1f2412bb95c59ec481eaf936cd32f0fe2a7b16b97b81c4c017a6a/PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", size = 162312 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ruff"
|
||||||
|
version = "0.8.6"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/da/00/089db7890ea3be5709e3ece6e46408d6f1e876026ec3fd081ee585fef209/ruff-0.8.6.tar.gz", hash = "sha256:dcad24b81b62650b0eb8814f576fc65cfee8674772a6e24c9b747911801eeaa5", size = 3473116 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d7/28/aa07903694637c2fa394a9f4fe93cf861ad8b09f1282fa650ef07ff9fe97/ruff-0.8.6-py3-none-linux_armv6l.whl", hash = "sha256:defed167955d42c68b407e8f2e6f56ba52520e790aba4ca707a9c88619e580e3", size = 10628735 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/43/827bb1448f1fcb0fb42e9c6edf8fb067ca8244923bf0ddf12b7bf949065c/ruff-0.8.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:54799ca3d67ae5e0b7a7ac234baa657a9c1784b48ec954a094da7c206e0365b1", size = 10386758 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/df/93/fc852a81c3cd315b14676db3b8327d2bb2d7508649ad60bfdb966d60738d/ruff-0.8.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e88b8f6d901477c41559ba540beeb5a671e14cd29ebd5683903572f4b40a9807", size = 10007808 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/94/e9/e0ed4af1794335fb280c4fac180f2bf40f6a3b859cae93a5a3ada27325ae/ruff-0.8.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0509e8da430228236a18a677fcdb0c1f102dd26d5520f71f79b094963322ed25", size = 10861031 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/82/68/da0db02f5ecb2ce912c2bef2aa9fcb8915c31e9bc363969cfaaddbc4c1c2/ruff-0.8.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:91a7ddb221779871cf226100e677b5ea38c2d54e9e2c8ed847450ebbdf99b32d", size = 10388246 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ac/1d/b85383db181639019b50eb277c2ee48f9f5168f4f7c287376f2b6e2a6dc2/ruff-0.8.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:248b1fb3f739d01d528cc50b35ee9c4812aa58cc5935998e776bf8ed5b251e75", size = 11424693 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ac/b7/30bc78a37648d31bfc7ba7105b108cb9091cd925f249aa533038ebc5a96f/ruff-0.8.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:bc3c083c50390cf69e7e1b5a5a7303898966be973664ec0c4a4acea82c1d4315", size = 12141921 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/60/b3/ee0a14cf6a1fbd6965b601c88d5625d250b97caf0534181e151504498f86/ruff-0.8.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:52d587092ab8df308635762386f45f4638badb0866355b2b86760f6d3c076188", size = 11692419 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ef/d6/c597062b2931ba3e3861e80bd2b147ca12b3370afc3889af46f29209037f/ruff-0.8.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:61323159cf21bc3897674e5adb27cd9e7700bab6b84de40d7be28c3d46dc67cf", size = 12981648 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/68/84/21f578c2a4144917985f1f4011171aeff94ab18dfa5303ac632da2f9af36/ruff-0.8.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ae4478b1471fc0c44ed52a6fb787e641a2ac58b1c1f91763bafbc2faddc5117", size = 11251801 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6c/aa/1ac02537c8edeb13e0955b5db86b5c050a1dcba54f6d49ab567decaa59c1/ruff-0.8.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0c000a471d519b3e6cfc9c6680025d923b4ca140ce3e4612d1a2ef58e11f11fe", size = 10849857 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/eb/00/020cb222252d833956cb3b07e0e40c9d4b984fbb2dc3923075c8f944497d/ruff-0.8.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:9257aa841e9e8d9b727423086f0fa9a86b6b420fbf4bf9e1465d1250ce8e4d8d", size = 10470852 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/00/56/e6d6578202a0141cd52299fe5acb38b2d873565f4670c7a5373b637cf58d/ruff-0.8.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:45a56f61b24682f6f6709636949ae8cc82ae229d8d773b4c76c09ec83964a95a", size = 10972997 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/be/31/dd0db1f4796bda30dea7592f106f3a67a8f00bcd3a50df889fbac58e2786/ruff-0.8.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:496dd38a53aa173481a7d8866bcd6451bd934d06976a2505028a50583e001b76", size = 11317760 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d4/70/cfcb693dc294e034c6fed837fa2ec98b27cc97a26db5d049345364f504bf/ruff-0.8.6-py3-none-win32.whl", hash = "sha256:e169ea1b9eae61c99b257dc83b9ee6c76f89042752cb2d83486a7d6e48e8f764", size = 8799729 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/60/22/ae6bcaa0edc83af42751bd193138bfb7598b2990939d3e40494d6c00698c/ruff-0.8.6-py3-none-win_amd64.whl", hash = "sha256:f1d70bef3d16fdc897ee290d7d20da3cbe4e26349f62e8a0274e7a3f4ce7a905", size = 9673857 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/91/f8/3765e053acd07baa055c96b2065c7fab91f911b3c076dfea71006666f5b0/ruff-0.8.6-py3-none-win_arm64.whl", hash = "sha256:7d7fc2377a04b6e04ffe588caad613d0c460eb2ecba4c0ccbbfe2bc973cbc162", size = 9149556 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tomli"
|
||||||
|
version = "2.2.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "types-psycopg2"
|
||||||
|
version = "2.9.21.20250318"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/09/29/9e86192ffa0a7ffc48d222f510026ec92aa93c7321ee24128480553661ec/types_psycopg2-2.9.21.20250318.tar.gz", hash = "sha256:eb6eac5bfb16adfd5f16b818918b9e26a40ede147e0f2bbffdf53a6ef7025a87", size = 26614 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a7/9c/34da1d5c2fe53c91f3382f45e18c58141cebef38e7204f676a93d1af6a1c/types_psycopg2-2.9.21.20250318-py3-none-any.whl", hash = "sha256:7296d111ad950bbd2fc979a1ab0572acae69047f922280e77db657c00d2c79c0", size = 24939 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "types-pyyaml"
|
||||||
|
version = "6.0.12.20250326"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/9b/66/f58e386be67589d5c3c9c0a368600783ac1321b7e6ee213c8f51848dbf0c/types_pyyaml-6.0.12.20250326.tar.gz", hash = "sha256:5e2d86d8706697803f361ba0b8188eef2999e1c372cd4faee4ebb0844b8a4190", size = 17346 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e9/1e/5609fea65117db83cc060342d4f6810f3cf1d3453b9f81bfe5f03f679633/types_pyyaml-6.0.12.20250326-py3-none-any.whl", hash = "sha256:961871cfbdc1ad8ae3cb6ae3f13007262bcfc168adc513119755a6e4d5d7ed65", size = 20398 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typing-extensions"
|
||||||
|
version = "4.13.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/0e/3e/b00a62db91a83fff600de219b6ea9908e6918664899a2d85db222f4fbf19/typing_extensions-4.13.0.tar.gz", hash = "sha256:0a4ac55a5820789d87e297727d229866c9650f6521b64206413c4fbada24d95b", size = 106520 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e0/86/39b65d676ec5732de17b7e3c476e45bb80ec64eb50737a8dce1a4178aba1/typing_extensions-4.13.0-py3-none-any.whl", hash = "sha256:c8dd92cc0d6425a97c18fbb9d1954e5ff92c1ca881a309c45f06ebc0b79058e5", size = 45683 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ujson"
|
||||||
|
version = "5.10.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f0/00/3110fd566786bfa542adb7932d62035e0c0ef662a8ff6544b6643b3d6fd7/ujson-5.10.0.tar.gz", hash = "sha256:b3cd8f3c5d8c7738257f1018880444f7b7d9b66232c64649f562d7ba86ad4bc1", size = 7154885 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7d/91/91678e49a9194f527e60115db84368c237ac7824992224fac47dcb23a5c6/ujson-5.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2601aa9ecdbee1118a1c2065323bda35e2c5a2cf0797ef4522d485f9d3ef65bd", size = 55354 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/de/2f/1ed8c9b782fa4f44c26c1c4ec686d728a4865479da5712955daeef0b2e7b/ujson-5.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:348898dd702fc1c4f1051bc3aacbf894caa0927fe2c53e68679c073375f732cf", size = 51808 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/51/bf/a3a38b2912288143e8e613c6c4c3f798b5e4e98c542deabf94c60237235f/ujson-5.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22cffecf73391e8abd65ef5f4e4dd523162a3399d5e84faa6aebbf9583df86d6", size = 51995 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b4/6d/0df8f7a6f1944ba619d93025ce468c9252aa10799d7140e07014dfc1a16c/ujson-5.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26b0e2d2366543c1bb4fbd457446f00b0187a2bddf93148ac2da07a53fe51569", size = 53566 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d5/ec/370741e5e30d5f7dc7f31a478d5bec7537ce6bfb7f85e72acefbe09aa2b2/ujson-5.10.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:caf270c6dba1be7a41125cd1e4fc7ba384bf564650beef0df2dd21a00b7f5770", size = 58499 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fe/29/72b33a88f7fae3c398f9ba3e74dc2e5875989b25f1c1f75489c048a2cf4e/ujson-5.10.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a245d59f2ffe750446292b0094244df163c3dc96b3ce152a2c837a44e7cda9d1", size = 997881 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/70/5c/808fbf21470e7045d56a282cf5e85a0450eacdb347d871d4eb404270ee17/ujson-5.10.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:94a87f6e151c5f483d7d54ceef83b45d3a9cca7a9cb453dbdbb3f5a6f64033f5", size = 1140631 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8f/6a/e1e8281408e6270d6ecf2375af14d9e2f41c402ab6b161ecfa87a9727777/ujson-5.10.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:29b443c4c0a113bcbb792c88bea67b675c7ca3ca80c3474784e08bba01c18d51", size = 1043511 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cb/ca/e319acbe4863919ec62498bc1325309f5c14a3280318dca10fe1db3cb393/ujson-5.10.0-cp310-cp310-win32.whl", hash = "sha256:c18610b9ccd2874950faf474692deee4223a994251bc0a083c114671b64e6518", size = 38626 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/78/ec/dc96ca379de33f73b758d72e821ee4f129ccc32221f4eb3f089ff78d8370/ujson-5.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:924f7318c31874d6bb44d9ee1900167ca32aa9b69389b98ecbde34c1698a250f", size = 42076 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/23/ec/3c551ecfe048bcb3948725251fb0214b5844a12aa60bee08d78315bb1c39/ujson-5.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a5b366812c90e69d0f379a53648be10a5db38f9d4ad212b60af00bd4048d0f00", size = 55353 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8d/9f/4731ef0671a0653e9f5ba18db7c4596d8ecbf80c7922dd5fe4150f1aea76/ujson-5.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:502bf475781e8167f0f9d0e41cd32879d120a524b22358e7f205294224c71126", size = 51813 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1f/2b/44d6b9c1688330bf011f9abfdb08911a9dc74f76926dde74e718d87600da/ujson-5.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b91b5d0d9d283e085e821651184a647699430705b15bf274c7896f23fe9c9d8", size = 51988 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/29/45/f5f5667427c1ec3383478092a414063ddd0dfbebbcc533538fe37068a0a3/ujson-5.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:129e39af3a6d85b9c26d5577169c21d53821d8cf68e079060602e861c6e5da1b", size = 53561 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/26/21/a0c265cda4dd225ec1be595f844661732c13560ad06378760036fc622587/ujson-5.10.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f77b74475c462cb8b88680471193064d3e715c7c6074b1c8c412cb526466efe9", size = 58497 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/28/36/8fde862094fd2342ccc427a6a8584fed294055fdee341661c78660f7aef3/ujson-5.10.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7ec0ca8c415e81aa4123501fee7f761abf4b7f386aad348501a26940beb1860f", size = 997877 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/90/37/9208e40d53baa6da9b6a1c719e0670c3f474c8fc7cc2f1e939ec21c1bc93/ujson-5.10.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ab13a2a9e0b2865a6c6db9271f4b46af1c7476bfd51af1f64585e919b7c07fd4", size = 1140632 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/89/d5/2626c87c59802863d44d19e35ad16b7e658e4ac190b0dead17ff25460b4c/ujson-5.10.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:57aaf98b92d72fc70886b5a0e1a1ca52c2320377360341715dd3933a18e827b1", size = 1043513 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2f/ee/03662ce9b3f16855770f0d70f10f0978ba6210805aa310c4eebe66d36476/ujson-5.10.0-cp311-cp311-win32.whl", hash = "sha256:2987713a490ceb27edff77fb184ed09acdc565db700ee852823c3dc3cffe455f", size = 38616 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3e/20/952dbed5895835ea0b82e81a7be4ebb83f93b079d4d1ead93fcddb3075af/ujson-5.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:f00ea7e00447918ee0eff2422c4add4c5752b1b60e88fcb3c067d4a21049a720", size = 42071 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e8/a6/fd3f8bbd80842267e2d06c3583279555e8354c5986c952385199d57a5b6c/ujson-5.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:98ba15d8cbc481ce55695beee9f063189dce91a4b08bc1d03e7f0152cd4bbdd5", size = 55642 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a8/47/dd03fd2b5ae727e16d5d18919b383959c6d269c7b948a380fdd879518640/ujson-5.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a9d2edbf1556e4f56e50fab7d8ff993dbad7f54bac68eacdd27a8f55f433578e", size = 51807 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/25/23/079a4cc6fd7e2655a473ed9e776ddbb7144e27f04e8fc484a0fb45fe6f71/ujson-5.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6627029ae4f52d0e1a2451768c2c37c0c814ffc04f796eb36244cf16b8e57043", size = 51972 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/04/81/668707e5f2177791869b624be4c06fb2473bf97ee33296b18d1cf3092af7/ujson-5.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8ccb77b3e40b151e20519c6ae6d89bfe3f4c14e8e210d910287f778368bb3d1", size = 53686 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bd/50/056d518a386d80aaf4505ccf3cee1c40d312a46901ed494d5711dd939bc3/ujson-5.10.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3caf9cd64abfeb11a3b661329085c5e167abbe15256b3b68cb5d914ba7396f3", size = 58591 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fc/d6/aeaf3e2d6fb1f4cfb6bf25f454d60490ed8146ddc0600fae44bfe7eb5a72/ujson-5.10.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6e32abdce572e3a8c3d02c886c704a38a1b015a1fb858004e03d20ca7cecbb21", size = 997853 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f8/d5/1f2a5d2699f447f7d990334ca96e90065ea7f99b142ce96e85f26d7e78e2/ujson-5.10.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a65b6af4d903103ee7b6f4f5b85f1bfd0c90ba4eeac6421aae436c9988aa64a2", size = 1140689 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f2/2c/6990f4ccb41ed93744aaaa3786394bca0875503f97690622f3cafc0adfde/ujson-5.10.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:604a046d966457b6cdcacc5aa2ec5314f0e8c42bae52842c1e6fa02ea4bda42e", size = 1043576 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/14/f5/a2368463dbb09fbdbf6a696062d0c0f62e4ae6fa65f38f829611da2e8fdd/ujson-5.10.0-cp312-cp312-win32.whl", hash = "sha256:6dea1c8b4fc921bf78a8ff00bbd2bfe166345f5536c510671bccececb187c80e", size = 38764 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/59/2d/691f741ffd72b6c84438a93749ac57bf1a3f217ac4b0ea4fd0e96119e118/ujson-5.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:38665e7d8290188b1e0d57d584eb8110951a9591363316dd41cf8686ab1d0abc", size = 42211 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0d/69/b3e3f924bb0e8820bb46671979770c5be6a7d51c77a66324cdb09f1acddb/ujson-5.10.0-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:618efd84dc1acbd6bff8eaa736bb6c074bfa8b8a98f55b61c38d4ca2c1f7f287", size = 55646 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/32/8a/9b748eb543c6cabc54ebeaa1f28035b1bd09c0800235b08e85990734c41e/ujson-5.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:38d5d36b4aedfe81dfe251f76c0467399d575d1395a1755de391e58985ab1c2e", size = 51806 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/39/50/4b53ea234413b710a18b305f465b328e306ba9592e13a791a6a6b378869b/ujson-5.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67079b1f9fb29ed9a2914acf4ef6c02844b3153913eb735d4bf287ee1db6e557", size = 51975 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b4/9d/8061934f960cdb6dd55f0b3ceeff207fcc48c64f58b43403777ad5623d9e/ujson-5.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7d0e0ceeb8fe2468c70ec0c37b439dd554e2aa539a8a56365fd761edb418988", size = 53693 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f5/be/7bfa84b28519ddbb67efc8410765ca7da55e6b93aba84d97764cd5794dbc/ujson-5.10.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:59e02cd37bc7c44d587a0ba45347cc815fb7a5fe48de16bf05caa5f7d0d2e816", size = 58594 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/48/eb/85d465abafb2c69d9699cfa5520e6e96561db787d36c677370e066c7e2e7/ujson-5.10.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2a890b706b64e0065f02577bf6d8ca3b66c11a5e81fb75d757233a38c07a1f20", size = 997853 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9f/76/2a63409fc05d34dd7d929357b7a45e3a2c96f22b4225cd74becd2ba6c4cb/ujson-5.10.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:621e34b4632c740ecb491efc7f1fcb4f74b48ddb55e65221995e74e2d00bbff0", size = 1140694 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/45/ed/582c4daba0f3e1688d923b5cb914ada1f9defa702df38a1916c899f7c4d1/ujson-5.10.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b9500e61fce0cfc86168b248104e954fead61f9be213087153d272e817ec7b4f", size = 1043580 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d7/0c/9837fece153051e19c7bade9f88f9b409e026b9525927824cdf16293b43b/ujson-5.10.0-cp313-cp313-win32.whl", hash = "sha256:4c4fc16f11ac1612f05b6f5781b384716719547e142cfd67b65d035bd85af165", size = 38766 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d7/72/6cb6728e2738c05bbe9bd522d6fc79f86b9a28402f38663e85a28fddd4a0/ujson-5.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:4573fd1695932d4f619928fd09d5d03d917274381649ade4328091ceca175539", size = 42212 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/97/94/50ff2f1b61d668907f20216873640ab19e0eaa77b51e64ee893f6adfb266/ujson-5.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dfef2814c6b3291c3c5f10065f745a1307d86019dbd7ea50e83504950136ed5b", size = 55421 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0c/b3/3d2ca621d8dbeaf6c5afd0725e1b4bbd465077acc69eff1e9302735d1432/ujson-5.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4734ee0745d5928d0ba3a213647f1c4a74a2a28edc6d27b2d6d5bd9fa4319e27", size = 51816 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8d/af/5dc103cb4d08f051f82d162a738adb9da488d1e3fafb9fd9290ea3eabf8e/ujson-5.10.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d47ebb01bd865fdea43da56254a3930a413f0c5590372a1241514abae8aa7c76", size = 52023 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5d/dd/b9a6027ba782b0072bf24a70929e15a58686668c32a37aebfcfaa9e00bdd/ujson-5.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dee5e97c2496874acbf1d3e37b521dd1f307349ed955e62d1d2f05382bc36dd5", size = 53622 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1f/28/bcf6df25c1a9f1989dc2ddc4ac8a80e246857e089f91a9079fd8a0a01459/ujson-5.10.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7490655a2272a2d0b072ef16b0b58ee462f4973a8f6bbe64917ce5e0a256f9c0", size = 58563 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9e/82/89404453a102d06d0937f6807c0a7ef2eec68b200b4ce4386127f3c28156/ujson-5.10.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ba17799fcddaddf5c1f75a4ba3fd6441f6a4f1e9173f8a786b42450851bd74f1", size = 998050 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/63/eb/2a4ea07165cad217bc842bb684b053bafa8ffdb818c47911c621e97a33fc/ujson-5.10.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2aff2985cef314f21d0fecc56027505804bc78802c0121343874741650a4d3d1", size = 1140672 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/72/53/d7bdf6afabeba3ed899f89d993c7f202481fa291d8c5be031c98a181eda4/ujson-5.10.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ad88ac75c432674d05b61184178635d44901eb749786c8eb08c102330e6e8996", size = 1043577 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/19/b1/75f5f0d18501fd34487e46829de3070724c7b350f1983ba7f07e0986720b/ujson-5.10.0-cp39-cp39-win32.whl", hash = "sha256:2544912a71da4ff8c4f7ab5606f947d7299971bdd25a45e008e467ca638d13c9", size = 38654 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/77/0d/50d2f9238f6d6683ead5ecd32d83d53f093a3c0047ae4c720b6d586cb80d/ujson-5.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:3ff201d62b1b177a46f113bb43ad300b424b7847f9c5d38b1b4ad8f75d4a282a", size = 42134 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/95/53/e5f5e733fc3525e65f36f533b0dbece5e5e2730b760e9beacf7e3d9d8b26/ujson-5.10.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5b6fee72fa77dc172a28f21693f64d93166534c263adb3f96c413ccc85ef6e64", size = 51846 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/59/1f/f7bc02a54ea7b47f3dc2d125a106408f18b0f47b14fc737f0913483ae82b/ujson-5.10.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:61d0af13a9af01d9f26d2331ce49bb5ac1fb9c814964018ac8df605b5422dcb3", size = 48103 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1a/3a/d3921b6f29bc744d8d6c56db5f8bbcbe55115fd0f2b79c3c43ff292cc7c9/ujson-5.10.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecb24f0bdd899d368b715c9e6664166cf694d1e57be73f17759573a6986dd95a", size = 47257 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f1/04/f4e3883204b786717038064afd537389ba7d31a72b437c1372297cb651ea/ujson-5.10.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fbd8fd427f57a03cff3ad6574b5e299131585d9727c8c366da4624a9069ed746", size = 48468 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/17/cd/9c6547169eb01a22b04cbb638804ccaeb3c2ec2afc12303464e0f9b2ee5a/ujson-5.10.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:beeaf1c48e32f07d8820c705ff8e645f8afa690cca1544adba4ebfa067efdc88", size = 54266 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/70/bf/ecd14d3cf6127f8a990b01f0ad20e257f5619a555f47d707c57d39934894/ujson-5.10.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:baed37ea46d756aca2955e99525cc02d9181de67f25515c468856c38d52b5f3b", size = 42224 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8d/96/a3a2356ca5a4b67fe32a0c31e49226114d5154ba2464bb1220a93eb383e8/ujson-5.10.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ba43cc34cce49cf2d4bc76401a754a81202d8aa926d0e2b79f0ee258cb15d3a4", size = 51855 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/73/3d/41e78e7500e75eb6b5a7ab06907a6df35603b92ac6f939b86f40e9fe2c06/ujson-5.10.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:ac56eb983edce27e7f51d05bc8dd820586c6e6be1c5216a6809b0c668bb312b8", size = 48059 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/be/14/e435cbe5b5189483adbba5fe328e88418ccd54b2b1f74baa4172384bb5cd/ujson-5.10.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f44bd4b23a0e723bf8b10628288c2c7c335161d6840013d4d5de20e48551773b", size = 47238 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e8/d9/b6f4d1e6bec20a3b582b48f64eaa25209fd70dc2892b21656b273bc23434/ujson-5.10.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c10f4654e5326ec14a46bcdeb2b685d4ada6911050aa8baaf3501e57024b804", size = 48457 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/23/1c/cfefabb5996e21a1a4348852df7eb7cfc69299143739e86e5b1071c78735/ujson-5.10.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0de4971a89a762398006e844ae394bd46991f7c385d7a6a3b93ba229e6dac17e", size = 54238 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/af/c4/fa70e77e1c27bbaf682d790bd09ef40e86807ada704c528ef3ea3418d439/ujson-5.10.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e1402f0564a97d2a52310ae10a64d25bcef94f8dd643fcf5d310219d915484f7", size = 42230 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "virtualenv"
|
||||||
|
version = "20.29.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "distlib" },
|
||||||
|
{ name = "filelock" },
|
||||||
|
{ name = "platformdirs" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/c7/9c/57d19fa093bcf5ac61a48087dd44d00655f85421d1aa9722f8befbf3f40a/virtualenv-20.29.3.tar.gz", hash = "sha256:95e39403fcf3940ac45bc717597dba16110b74506131845d9b687d5e73d947ac", size = 4320280 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c2/eb/c6db6e3001d58c6a9e67c74bb7b4206767caa3ccc28c6b9eaf4c23fb4e34/virtualenv-20.29.3-py3-none-any.whl", hash = "sha256:3e3d00f5807e83b234dfb6122bf37cfadf4be216c53a49ac059d02414f819170", size = 4301458 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zipp"
|
||||||
|
version = "3.21.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/3f/50/bad581df71744867e9468ebd0bcd6505de3b275e06f202c2cb016e3ff56f/zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4", size = 24545 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b7/1a/7e4798e9339adc931158c9d69ecc34f5e6791489d469f5e50ec15e35f458/zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931", size = 9630 },
|
||||||
|
]
|
Loading…
x
Reference in New Issue
Block a user