mirror of
https://github.com/django-q2/django-q2.git
synced 2026-09-15 21:47:53 +08:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2158edd652 | ||
|
|
3b01fc2bd7 | ||
|
|
ac83602a76 | ||
|
|
e3517bd4c6 | ||
|
|
a694a9f53c | ||
|
|
a4a4e05dfe |
@@ -1,4 +1,2 @@
|
||||
# flake8, black, isort
|
||||
b1d000d007f3f77069719523268a0c6256dc0860
|
||||
# move to ruff formatting/linting
|
||||
ad4d24e17c9424b17cd8ae65c2def7ecc74e63c1
|
||||
|
||||
15
.github/workflows/release.yml
vendored
15
.github/workflows/release.yml
vendored
@@ -11,23 +11,22 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.11
|
||||
python-version: 3.8
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get -y install gettext
|
||||
pip install uv==0.12.1 django==5.2
|
||||
python -m pip install pip setuptools django poetry
|
||||
# compile messages to get .mo files
|
||||
uv run django-admin compilemessages
|
||||
django-admin compilemessages
|
||||
- name: Build and publish package
|
||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
|
||||
run: |
|
||||
uv build
|
||||
uv publish --token ${{ secrets.PYPI_TOKEN }}
|
||||
run: poetry --build --username=__token__ --password=${{ secrets.PYPI_TOKEN }} publish
|
||||
|
||||
63
.github/workflows/test.yml
vendored
63
.github/workflows/test.yml
vendored
@@ -7,48 +7,18 @@ on:
|
||||
branches:
|
||||
- master
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Lint with ruff
|
||||
run: |
|
||||
pipx install ruff==0.4.10
|
||||
ruff format . --check && ruff check .
|
||||
- name: Check twine
|
||||
run: |
|
||||
python -m pip install twine poetry rstcheck
|
||||
poetry build
|
||||
rstcheck README.rst
|
||||
twine check dist/*
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version:
|
||||
- "3.10"
|
||||
- "3.11"
|
||||
- "3.12"
|
||||
- "3.13"
|
||||
- "3.14"
|
||||
django:
|
||||
- "5.2"
|
||||
- "6.0"
|
||||
- "6.1"
|
||||
exclude:
|
||||
# django 6.0 does not support earlier than 3.12
|
||||
- python-version: "3.10"
|
||||
django: "6.0"
|
||||
- python-version: "3.11"
|
||||
django: "6.0"
|
||||
# django 6.1 does not support earlier than 3.12
|
||||
- python-version: "3.10"
|
||||
django: "6.1"
|
||||
- python-version: "3.11"
|
||||
django: "6.1"
|
||||
python-version: [ "3.8", "3.9", "3.10", "3.11" ]
|
||||
django: [ "3.2", "4.1" ]
|
||||
|
||||
services:
|
||||
disque:
|
||||
image: efrecon/disque:1.0-rc1
|
||||
ports:
|
||||
- '7711:7711/tcp'
|
||||
mongodb:
|
||||
image: mongo
|
||||
ports:
|
||||
@@ -69,19 +39,20 @@ jobs:
|
||||
- 6379:6379
|
||||
options: --entrypoint redis-server
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install dependencies with Django ${{ matrix.django }}
|
||||
run: |
|
||||
python -m pip install uv==0.12.1
|
||||
uv sync --extra testing --python ${{ matrix.python-version }}
|
||||
uv pip install "django~=${{ matrix.django }}a1"
|
||||
python -m pip install --upgrade pip
|
||||
pip install poetry
|
||||
poetry add "django==${{ matrix.django }}"
|
||||
poetry install -E testing
|
||||
- name: Run Tests
|
||||
run: |
|
||||
uv run pytest --cov=./django_q --cov-report=xml
|
||||
poetry run pytest --cov=./django_q --cov-report=xml
|
||||
env:
|
||||
MONGO_HOST: "127.0.0.1"
|
||||
REDIS_HOST: "127.0.0.1"
|
||||
@@ -97,12 +68,16 @@ jobs:
|
||||
finish:
|
||||
needs: test
|
||||
runs-on: ubuntu-latest
|
||||
container: python:3.11-bookworm
|
||||
container: python:3-slim
|
||||
steps:
|
||||
- name: Upload to coveralls
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install coveralls
|
||||
python -m pip install coveralls flake8 black
|
||||
coveralls --service=github --finish
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Check flake8/black
|
||||
run: |
|
||||
flake8 .
|
||||
black --check .
|
||||
|
||||
145
CHANGELOG.md
145
CHANGELOG.md
@@ -1,149 +1,6 @@
|
||||
# Changelog
|
||||
|
||||
## [v1.11.0](https://github.com/django-q2/django-q2/tree/v1.11.0) (2026-08-10)
|
||||
|
||||
- AttributeError when start_event is None, and guard process faster stop (#305) https://github.com/django-q2/django-q2/pull/305
|
||||
- Add croniter as optional extra. (#336) https://github.com/django-q2/django-q2/pull/336
|
||||
- Migrate poetry to uv (#337) https://github.com/django-q2/django-q2/pull/337
|
||||
- Add support for python 3.13 and 3.14 (#338) https://github.com/django-q2/django-q2/pull/338
|
||||
- Add django 6.1 support (#340) https://github.com/django-q2/django-q2/pull/340
|
||||
- Postpone SECRET_KEY evaluation to the qcluster command execution (#332) https://github.com/django-q2/django-q2/pull/332
|
||||
|
||||
|
||||
## [v1.10.0](https://github.com/django-q2/django-q2/tree/v1.10.0) (2026-05-01)
|
||||
|
||||
- fix: Fix incorrect signal import (#308) https://github.com/django-q2/django-q2/pull/308
|
||||
- Add post_execute_in_worker signal (#309) https://github.com/django-q2/django-q2/pull/309
|
||||
- Fix BadSignature after the default Django cache expires (#311) https://github.com/django-q2/django-q2/pull/311
|
||||
- feat:add Simplified Chinese Translation (#314) https://github.com/django-q2/django-q2/pull/314
|
||||
- Update Django Q2 compatibility information (#316) https://github.com/django-q2/django-q2/pull/316
|
||||
- Don't close DB connection if async_task was called with sync=True (#301) https://github.com/django-q2/django-q2/pull/301
|
||||
- Convert queue size and count to string in monitor (#319) https://github.com/django-q2/django-q2/pull/319
|
||||
- Fix unbounded growth of Broker.set_stat cluster master list (#322) https://github.com/django-q2/django-q2/pull/322
|
||||
- Update Python base image to 3.9-slim-bookworm (#325) https://github.com/django-q2/django-q2/pull/325
|
||||
- feat: add ru locale and improve translations (#320) https://github.com/django-q2/django-q2/pull/320
|
||||
|
||||
|
||||
## [v1.9.0](https://github.com/django-q2/django-q2/tree/v1.9.0) (2025-12-04)
|
||||
|
||||
- Django 6.0 support https://github.com/django-q2/django-q2/pull/307
|
||||
- Fix to make tests work with redis-py > 5 https://github.com/django-q2/django-q2/pull/282
|
||||
|
||||
## [v1.8.0](https://github.com/django-q2/django-q2/tree/v1.8.0) (2025-04-25)
|
||||
|
||||
- Delete deprecated imports and unwrapping https://github.com/django-q2/django-q2/pull/261
|
||||
- Remove disque from CI https://github.com/django-q2/django-q2/pull/270
|
||||
- Avoid creating task run on Scheduler creation https://github.com/django-q2/django-q2/pull/268
|
||||
- Update tested versions, add python 3.13 support and django 5.2 support. Drop python 3.8 support https://github.com/django-q2/django-q2/pull/271
|
||||
- Move timeout function from ORM broker into class to allow easy customization https://github.com/django-q2/django-q2/pull/274
|
||||
|
||||
## [v1.7.6](https://github.com/django-q2/django-q2/tree/v1.7.6) (2025-01-12)
|
||||
|
||||
- Make poetry version fixed in CI https://github.com/django-q2/django-q2/pull/260
|
||||
|
||||
## [v1.7.5](https://github.com/django-q2/django-q2/tree/v1.7.5) (2025-01-12)
|
||||
|
||||
- perf: avoid checking success tasks when save limit is disabled https://github.com/django-q2/django-q2/pull/255
|
||||
- Fix install path for CHANGELOG.md https://github.com/django-q2/django-q2/pull/258
|
||||
|
||||
## [v1.7.4](https://github.com/django-q2/django-q2/tree/v1.7.4) (2024-11-03)
|
||||
|
||||
- Decrease the MAX_RSS set in test_cluster::test_max_rss https://github.com/django-q2/django-q2/pull/240
|
||||
- Fix BROKER_CLASS monkeypatch in test_brokers https://github.com/django-q2/django-q2/pull/239
|
||||
- Fix 'receive_message_wait_time_seconds' SQS broker management https://github.com/django-q2/django-q2/pull/243
|
||||
|
||||
## [v1.7.3](https://github.com/django-q2/django-q2/tree/v1.7.3) (2024-10-15)
|
||||
|
||||
- Catch missing SEGALRM with AttributeError instead of ValueError https://github.com/django-q2/django-q2/pull/223
|
||||
- Refactor timeout handling to handle AttributeError and ValueError for Windows users https://github.com/django-q2/django-q2/pull/234
|
||||
- Fix type check for args in scheduler.py E721 https://github.com/django-q2/django-q2/pull/233
|
||||
- Only trigger prometheus if configured https://github.com/django-q2/django-q2/pull/231
|
||||
- Fix missing ack_id when finishing task https://github.com/django-q2/django-q2/pull/224
|
||||
|
||||
|
||||
## [v1.7.2](https://github.com/django-q2/django-q2/tree/v1.7.2) (2024-09-09)
|
||||
|
||||
- Fix twine check
|
||||
|
||||
## [v1.7.1](https://github.com/django-q2/django-q2/tree/v1.7.1) (2024-09-08)
|
||||
|
||||
- Fixed date of v1.7.0
|
||||
- Fixed README.rst formatting which is blocking release of latest version
|
||||
|
||||
## [v1.7.0](https://github.com/django-q2/django-q2/tree/v1.7.0) (2024-09-08)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Remove support for Django 3.2 and 4.1 https://github.com/django-q2/django-q2/pull/183
|
||||
- Fix max attempts for value 1 https://github.com/django-q2/django-q2/pull/185
|
||||
- Replace black/isort with ruff https://github.com/django-q2/django-q2/pull/188
|
||||
- Fix repeating task after timeout https://github.com/django-q2/django-q2/pull/184
|
||||
- fix: Oracle ORM backend compatibility #180 https://github.com/django-q2/django-q2/pull/186
|
||||
- chore: Update CI for Django 4.2 Python 3.12 support https://github.com/django-q2/django-q2/pull/208
|
||||
- chore: Add Support Django 5.1 https://github.com/django-q2/django-q2/pull/207
|
||||
- Call mark_process_dead on worker pid if prometheus_client is installed https://github.com/django-q2/django-q2/pull/212
|
||||
- Add example project https://github.com/django-q2/django-q2/pull/215
|
||||
- Add index on succeeded tasks https://github.com/django-q2/django-q2/pull/164
|
||||
|
||||
## [v1.6.2](https://github.com/django-q2/django-q2/tree/v1.6.2) (2024-03-05)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Allow different broker on chain https://github.com/django-q2/django-q2/pull/156
|
||||
- Fix formatting issues in README.rst https://github.com/django-q2/django-q2/pull/159
|
||||
- Update docs to add cluster option to the async_task https://github.com/django-q2/django-q2/pull/157
|
||||
- Update release/test dependencies https://github.com/django-q2/django-q2/pull/147
|
||||
- Fix for Negative Repeat Count in Scheduler https://github.com/django-q2/django-q2/pull/146
|
||||
- Update django.po https://github.com/django-q2/django-q2/pull/138
|
||||
- Use importerror for b62_decode and avoid deprecation notification https://github.com/django-q2/django-q2/pull/134
|
||||
- Specify build system in pyproject.toml https://github.com/django-q2/django-q2/pull/131
|
||||
|
||||
## [v1.6.1](https://github.com/django-q2/django-q2/tree/v1.6.1) (2023-10-13)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Fix strict versions for python/django https://github.com/django-q2/django-q2/pull/130
|
||||
|
||||
## [v1.6.0](https://github.com/django-q2/django-q2/tree/v1.6.0) (2023-10-12)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Add support for Django 5 and python 12 https://github.com/django-q2/django-q2/pull/120
|
||||
- Fix for "apps not ready" in Windows and Mac https://github.com/django-q2/django-q2/pull/116
|
||||
- Update broken MongoClient link in Docs https://github.com/django-q2/django-q2/pull/127
|
||||
- Fix German Translation Typo https://github.com/django-q2/django-q2/pull/124
|
||||
- Update Add-ons install command in install.rst https://github.com/django-q2/django-q2/pull/115
|
||||
- DOCS: Correct health check import in examples.rst https://github.com/django-q2/django-q2/pull/110
|
||||
|
||||
## [v1.5.5](https://github.com/django-q2/django-q2/tree/v1.5.5) (2023-09-01)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Add documentation to migrate from django-q to django-q2 https://github.com/django-q2/django-q2/pull/108
|
||||
- Fix not picking up result from falsy result https://github.com/django-q2/django-q2/pull/107
|
||||
- Remove deprecated usage pkg_resources https://github.com/django-q2/django-q2/pull/103
|
||||
- Move worker, scheduler, pusher and monitor to separate files https://github.com/django-q2/django-q2/pull/100
|
||||
|
||||
## [v1.5.4](https://github.com/GDay/django-q2/tree/v1.5.4) (2023-06-29)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Rerun successful tasks https://github.com/django-q2/django-q2/pull/99
|
||||
|
||||
## [v1.5.3](https://github.com/GDay/django-q2/tree/v1.5.3) (2023-05-14)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Add post_spawn signal. https://github.com/django-q2/django-q2/pull/93
|
||||
- Post spawn docs https://github.com/django-q2/django-q2/pull/95
|
||||
- Make processes identifiable with uuid4 https://github.com/django-q2/django-q2/pull/91
|
||||
|
||||
## [v1.5.2](https://github.com/GDay/django-q2/tree/v1.5.2) (2023-04-13)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Added Django 4.2 to the test matrix, fixed deprecation warning https://github.com/GDay/django-q2/pull/89
|
||||
- Updated docs to show support for 4.2
|
||||
## [Unreleased](https://github.com/GDay/django-q2/tree/HEAD)
|
||||
|
||||
## [v1.5.1](https://github.com/GDay/django-q2/tree/v1.5.1) (2023-04-02)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM python:3.12
|
||||
FROM python:3.9
|
||||
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
RUN mkdir -p /app
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Sets the python version
|
||||
FROM python:3.13-slim-bookworm
|
||||
FROM python:3.9.5-slim
|
||||
|
||||
# Allows the logs generated by python apps to be rendered in the terminal
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
@@ -7,15 +7,18 @@ ENV PYTHONUNBUFFERED 1
|
||||
# Sets the default shell to bash
|
||||
ENV SHELL /bin/bash
|
||||
|
||||
RUN set -ex \
|
||||
&& apt update \
|
||||
&& apt-get install gcc gettext python3-dev --yes
|
||||
# Upgrades pip
|
||||
RUN pip install --upgrade pip
|
||||
|
||||
# Install UV
|
||||
RUN pip install uv==0.12.1
|
||||
# Install poetry
|
||||
RUN pip install poetry
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN uv sync --extra testing
|
||||
RUN poetry lock --no-update
|
||||
|
||||
RUN poetry config virtualenvs.create false
|
||||
|
||||
RUN poetry install -E testing
|
||||
|
||||
31
Makefile
31
Makefile
@@ -1,31 +0,0 @@
|
||||
dev:
|
||||
docker compose -f web-docker-compose.yaml up
|
||||
|
||||
build-test:
|
||||
docker compose -f test-services-docker-compose.yaml build --no-cache
|
||||
|
||||
lock:
|
||||
docker compose -f test-services-docker-compose.yaml run --rm django-q2 uv lock
|
||||
|
||||
test:
|
||||
docker compose -f test-services-docker-compose.yaml run --rm django-q2 uv run pytest
|
||||
|
||||
shell:
|
||||
docker compose -f test-services-docker-compose.yaml run --rm django-q2 uv run python manage.py shell
|
||||
|
||||
makemigrations:
|
||||
docker compose -f test-services-docker-compose.yaml run --rm django-q2 uv run python manage.py makemigrations
|
||||
|
||||
migrate:
|
||||
docker compose -f test-services-docker-compose.yaml run --rm django-q2 uv run python manage.py migrate
|
||||
|
||||
build-package:
|
||||
docker compose -f test-services-docker-compose.yaml run --rm django-q2 uv run django-admin compilemessages
|
||||
docker compose -f test-services-docker-compose.yaml run --rm django-q2 uv build
|
||||
|
||||
createsuperuser:
|
||||
docker compose -f web-docker-compose.yaml run --rm web python manage.py createsuperuser
|
||||
|
||||
format:
|
||||
docker compose -f test-services-docker-compose.yaml run --rm django-q2 uv run ruff format .
|
||||
docker compose -f test-services-docker-compose.yaml run --rm django-q2 uv run ruff check . --fix
|
||||
47
README.rst
47
README.rst
@@ -1,9 +1,11 @@
|
||||
A multiprocessing distributed task queue for Django
|
||||
---------------------------------------------------
|
||||
|
||||
|image0| |image1| |downloads|
|
||||
|image0| |image1| |docs| |downloads|
|
||||
|
||||
Django Q2 is a fork of Django Q. Big thanks to Ilan Steemers for starting this project. Unfortunately, development has stalled since June 2021. Django Q2 is the new updated version of Django Q, with dependencies updates, docs updates and several bug fixes. Original repository: https://github.com/Koed00/django-q
|
||||
::
|
||||
|
||||
Django Q2 is a fork of Django Q. Big thanks to Ilan Steemers for starting this project. Unfortunately, development has stalled since June 2021. Django Q2 is the new updated version of Django Q, with dependencies updates, docs updates and several bug fixes. Original repository: https://github.com/Koed00/django-q
|
||||
|
||||
Features
|
||||
~~~~~~~~
|
||||
@@ -25,7 +27,7 @@ Changes compared to the original Django-Q:
|
||||
- Dropped support for Disque (hasn't been updated in a long time)
|
||||
- Dropped Redis, Arrow and Blessed dependencies
|
||||
- Updated all current dependencies
|
||||
- Added tests for Django 4.x and 5.x
|
||||
- Added tests for Django 4.x
|
||||
- Added Turkish language
|
||||
- Improved admin area
|
||||
- Fixed a lot of issues
|
||||
@@ -35,13 +37,10 @@ See the `changelog <https://github.com/GDay/django-q2/blob/master/CHANGELOG.md>`
|
||||
Requirements
|
||||
~~~~~~~~~~~~
|
||||
|
||||
- `Django <https://www.djangoproject.com>`__ 5.2, 6.0 and 6.1
|
||||
- `Django <https://www.djangoproject.com>`__ > = 3.2
|
||||
- `Django-picklefield <https://github.com/gintas/django-picklefield>`__
|
||||
|
||||
Tested with:
|
||||
|
||||
* Python 3.10 to 3.14.
|
||||
* Django 5.2, 6.0 and 6.1.
|
||||
Tested with: Python 3.8, 3.9, 3.10, 3.11 Django 3.2.X and 4.1.X
|
||||
|
||||
Brokers
|
||||
~~~~~~~
|
||||
@@ -74,6 +73,7 @@ Installation
|
||||
|
||||
Read the full documentation at `https://django-q2.readthedocs.org <https://django-q2.readthedocs.org>`__
|
||||
|
||||
|
||||
Configuration
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
@@ -104,8 +104,6 @@ For full configuration options, see the `configuration documentation <https://dj
|
||||
Management Commands
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
For the management commands to work, you will need to install Blessed: <https://github.com/jquast/blessed>
|
||||
|
||||
Start a cluster with::
|
||||
|
||||
$ python manage.py qcluster
|
||||
@@ -156,6 +154,7 @@ Use `async_task` from your code to quickly offload tasks:
|
||||
|
||||
For more info see `Tasks <https://django-q2.readthedocs.org/en/latest/tasks.html>`__
|
||||
|
||||
|
||||
Schedule
|
||||
~~~~~~~~
|
||||
|
||||
@@ -200,22 +199,6 @@ Admin page or directly from your code:
|
||||
|
||||
For more info check the `Schedules <https://django-q2.readthedocs.org/en/latest/schedules.html>`__ documentation.
|
||||
|
||||
Development
|
||||
~~~~~~~~~~~
|
||||
|
||||
There is an example project that you can use to develop with. Docker (compose) is being used to set everything up.
|
||||
Please note that you will have to restart the django-q container when changes have been made to tasks or django-q.
|
||||
You can start the example project with:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
make dev
|
||||
|
||||
Create a superuser with:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
make createsuperuser
|
||||
|
||||
Testing
|
||||
~~~~~~~
|
||||
@@ -224,7 +207,7 @@ Running tests is easy with docker compose, it will also start the necessary data
|
||||
|
||||
.. code:: bash
|
||||
|
||||
make test
|
||||
docker-compose -f test-services-docker-compose.yaml run --rm django-q2 poetry run pytest
|
||||
|
||||
Locale
|
||||
~~~~~~
|
||||
@@ -232,6 +215,12 @@ Locale
|
||||
Currently available in English, German, Turkish, and French.
|
||||
Translation pull requests are always welcome.
|
||||
|
||||
Todo
|
||||
~~~~
|
||||
|
||||
- Better tests and coverage
|
||||
- Less dependencies?
|
||||
|
||||
Acknowledgements
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -248,5 +237,9 @@ Acknowledgements
|
||||
:target: https://github.com/GDay/django-q2/actions?query=workflow%3Atests
|
||||
.. |image1| image:: https://coveralls.io/repos/github/GDay/django-q2/badge.svg?branch=master
|
||||
:target: https://coveralls.io/github/GDay/django-q2?branch=master
|
||||
.. |docs| image:: https://readthedocs.org/projects/docs/badge/?version=latest
|
||||
:alt: Documentation Status
|
||||
:scale: 100
|
||||
:target: https://django-q2.readthedocs.org/
|
||||
.. |downloads| image:: https://img.shields.io/pypi/dm/django-q2
|
||||
:target: https://img.shields.io/pypi/dm/django-q2
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Note that this file needs to have the executable bit set for it to work with later localstack implementations.
|
||||
|
||||
export DEFAULT_REGION=us-west-2
|
||||
|
||||
create_sqs() {
|
||||
QUEUE_NAME="$1"
|
||||
TIMEOUT=${2:-60}
|
||||
DL_QUEUE_URL=$(awslocal sqs create-queue --queue-name "dl-$QUEUE_NAME" --query QueueUrl --output text)
|
||||
echo ">>> Created $DL_QUEUE_URL queue!"
|
||||
DL_QUEUE_ARN=$(awslocal sqs get-queue-attributes --queue-url "$DL_QUEUE_URL" --attribute-names QueueArn --query Attributes.QueueArn --output text)
|
||||
awslocal sqs create-queue --queue-name "$QUEUE_NAME" --attributes '{
|
||||
"RedrivePolicy": "{\"deadLetterTargetArn\": \"'"$DL_QUEUE_ARN"'\",\"maxReceiveCount\":\"3\"}",
|
||||
"VisibilityTimeout": "'"$TIMEOUT"'"
|
||||
}'
|
||||
}
|
||||
|
||||
# Create SQS queues
|
||||
create_sqs testing
|
||||
@@ -1,3 +1,8 @@
|
||||
VERSION = (1, 11, 0)
|
||||
import django
|
||||
|
||||
VERSION = (1, 5, 1)
|
||||
|
||||
if django.VERSION < (3, 2):
|
||||
default_app_config = "django_q.apps.DjangoQConfig"
|
||||
|
||||
__all__ = ["conf", "cluster", "models", "tasks"]
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
"""Admin module for Django."""
|
||||
|
||||
from django.contrib import admin
|
||||
from django.db.models.expressions import OuterRef, Subquery
|
||||
from django.template.defaultfilters import truncatechars
|
||||
from django.urls import reverse
|
||||
from django.utils.html import format_html
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
@@ -12,37 +10,10 @@ from django_q.models import Failure, OrmQ, Schedule, Success, Task
|
||||
from django_q.tasks import async_task
|
||||
|
||||
|
||||
def resubmit_task(model_admin, request, queryset):
|
||||
"""Submit selected tasks back to the queue."""
|
||||
for task in queryset:
|
||||
async_task(
|
||||
task.func,
|
||||
*task.args or (),
|
||||
hook=task.hook,
|
||||
group=task.group,
|
||||
cluster=task.cluster,
|
||||
**task.kwargs or {},
|
||||
)
|
||||
if isinstance(model_admin, FailAdmin):
|
||||
task.delete()
|
||||
|
||||
|
||||
resubmit_task.short_description = _("Resubmit selected tasks to queue")
|
||||
|
||||
|
||||
class TaskAdmin(admin.ModelAdmin):
|
||||
"""model admin for success tasks."""
|
||||
|
||||
list_display = (
|
||||
"name",
|
||||
"group",
|
||||
"func",
|
||||
"cluster",
|
||||
"started",
|
||||
"stopped",
|
||||
"time_taken",
|
||||
)
|
||||
actions = [resubmit_task]
|
||||
list_display = ("name", "group", "func", "cluster", "started", "stopped", "time_taken")
|
||||
|
||||
def has_add_permission(self, request):
|
||||
"""Don't allow adds."""
|
||||
@@ -61,29 +32,28 @@ class TaskAdmin(admin.ModelAdmin):
|
||||
"""Set all fields readonly."""
|
||||
return list(self.readonly_fields) + [field.name for field in obj._meta.fields]
|
||||
|
||||
@admin.display(description=_("Time Taken"))
|
||||
def time_taken(self, obj):
|
||||
return obj.time_taken()
|
||||
|
||||
def retry_failed(FailAdmin, request, queryset):
|
||||
"""Submit selected tasks back to the queue."""
|
||||
for task in queryset:
|
||||
async_task(task.func, *task.args or (), hook=task.hook,
|
||||
group=task.group, cluster=task.cluster, **task.kwargs or {})
|
||||
task.delete()
|
||||
|
||||
|
||||
retry_failed.short_description = _("Resubmit selected tasks to queue")
|
||||
|
||||
|
||||
class FailAdmin(admin.ModelAdmin):
|
||||
"""model admin for failed tasks."""
|
||||
|
||||
list_display = (
|
||||
"name",
|
||||
"group",
|
||||
"func",
|
||||
"cluster",
|
||||
"started",
|
||||
"stopped",
|
||||
"short_result",
|
||||
)
|
||||
list_display = ("name", "group", "func", "cluster", "started", "stopped", "short_result")
|
||||
|
||||
def has_add_permission(self, request):
|
||||
"""Don't allow adds."""
|
||||
return False
|
||||
|
||||
actions = [resubmit_task]
|
||||
actions = [retry_failed]
|
||||
search_fields = ("name", "func", "group")
|
||||
list_filter = ("group", "cluster")
|
||||
readonly_fields = []
|
||||
@@ -92,10 +62,6 @@ class FailAdmin(admin.ModelAdmin):
|
||||
"""Set all fields readonly."""
|
||||
return list(self.readonly_fields) + [field.name for field in obj._meta.fields]
|
||||
|
||||
@admin.display(description=_("Short result"))
|
||||
def short_result(self, obj):
|
||||
return truncatechars(obj.result, 100)
|
||||
|
||||
|
||||
class ScheduleAdmin(admin.ModelAdmin):
|
||||
"""model admin for schedules"""
|
||||
@@ -147,7 +113,7 @@ class ScheduleAdmin(admin.ModelAdmin):
|
||||
url = reverse("admin:django_q_success_change", args=(obj.task_id,))
|
||||
else:
|
||||
url = reverse("admin:django_q_failure_change", args=(obj.task_id,))
|
||||
return format_html('<a href="{}">[{}]</a>', url, obj.task_name)
|
||||
return format_html(f'<a href="{url}">[{obj.task_name}]</a>')
|
||||
return None
|
||||
|
||||
get_last_run.allow_tags = True
|
||||
@@ -158,17 +124,7 @@ class QueueAdmin(admin.ModelAdmin):
|
||||
"""queue admin for ORM broker"""
|
||||
|
||||
list_display = ("id", "key", "name", "group", "func", "lock", "task_id")
|
||||
fields = (
|
||||
"key",
|
||||
"lock",
|
||||
"task_id",
|
||||
"name",
|
||||
"group",
|
||||
"func",
|
||||
"args",
|
||||
"kwargs",
|
||||
"q_options",
|
||||
)
|
||||
fields = ("key", "lock", "task_id", "name", "group", "func", "args", "kwargs", "q_options")
|
||||
readonly_fields = fields[2:]
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
|
||||
@@ -106,13 +106,8 @@ class Broker:
|
||||
return
|
||||
key_list = self.cache.get(Conf.Q_STAT, [])
|
||||
if key not in key_list:
|
||||
# Prune stale entries whose per-stat value has expired, so the
|
||||
# master list cannot grow without bound across cluster restarts.
|
||||
key_list = [k for k in key_list if self.cache.get(k) is not None]
|
||||
key_list.append(key)
|
||||
# timeout=None: master list lifetime is managed by membership
|
||||
# changes, not by TTL refresh on every heartbeat.
|
||||
self.cache.set(Conf.Q_STAT, key_list, None)
|
||||
self.cache.set(Conf.Q_STAT, key_list)
|
||||
return self.cache.set(key, value, timeout)
|
||||
|
||||
def get_stat(self, key: str):
|
||||
@@ -187,7 +182,7 @@ def get_broker(list_key: str = None) -> Broker:
|
||||
|
||||
return ironmq.IronMQBroker(list_key=list_key)
|
||||
# SQS
|
||||
elif isinstance(Conf.SQS, dict):
|
||||
elif type(Conf.SQS) == dict:
|
||||
from django_q.brokers import aws_sqs
|
||||
|
||||
return aws_sqs.Sqs(list_key=list_key)
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import copy
|
||||
|
||||
from boto3 import Session
|
||||
from botocore.client import ClientError
|
||||
|
||||
@@ -80,15 +78,15 @@ class Sqs(Broker):
|
||||
|
||||
@staticmethod
|
||||
def get_connection(list_key: str = None) -> Session:
|
||||
config_cloned = copy.deepcopy(Conf.SQS)
|
||||
if "aws_region" in config_cloned:
|
||||
config_cloned["region_name"] = config_cloned["aws_region"]
|
||||
del config_cloned["aws_region"]
|
||||
config = Conf.SQS
|
||||
if "aws_region" in config:
|
||||
config["region_name"] = config["aws_region"]
|
||||
del config["aws_region"]
|
||||
|
||||
if "receive_message_wait_time_seconds" in config_cloned:
|
||||
del config_cloned["receive_message_wait_time_seconds"]
|
||||
if "receive_message_wait_time_seconds" in config:
|
||||
del config["receive_message_wait_time_seconds"]
|
||||
|
||||
return Session(**config_cloned)
|
||||
return Session(**config)
|
||||
|
||||
def get_queue(self):
|
||||
self.sqs = self.connection.resource("sqs")
|
||||
|
||||
@@ -10,6 +10,10 @@ from django_q.conf import Conf, logger
|
||||
from django_q.models import OrmQ
|
||||
|
||||
|
||||
def _timeout():
|
||||
return timezone.now() + timedelta(seconds=Conf.RETRY)
|
||||
|
||||
|
||||
class ORM(Broker):
|
||||
@staticmethod
|
||||
def get_connection(list_key: str = None):
|
||||
@@ -24,9 +28,6 @@ class ORM(Broker):
|
||||
logger.debug("Broker in an atomic transaction")
|
||||
return OrmQ.objects.using(Conf.ORM)
|
||||
|
||||
def timeout(self, task):
|
||||
return timezone.now() + timedelta(seconds=Conf.RETRY)
|
||||
|
||||
def queue_size(self) -> int:
|
||||
return (
|
||||
self.get_connection()
|
||||
@@ -36,9 +37,7 @@ class ORM(Broker):
|
||||
|
||||
def lock_size(self) -> int:
|
||||
return (
|
||||
self.get_connection()
|
||||
.filter(key=self.list_key, lock__gt=timezone.now())
|
||||
.count()
|
||||
self.get_connection().filter(key=self.list_key, lock__gt=timezone.now()).count()
|
||||
)
|
||||
|
||||
def purge_queue(self):
|
||||
@@ -63,9 +62,7 @@ class ORM(Broker):
|
||||
return package.pk
|
||||
|
||||
def dequeue(self):
|
||||
tasks = self.get_connection().filter(
|
||||
key=self.list_key, lock__lt=timezone.now()
|
||||
)[
|
||||
tasks = self.get_connection().filter(key=self.list_key, lock__lt=timezone.now())[
|
||||
0 : Conf.BULK # noqa: E203
|
||||
]
|
||||
if tasks:
|
||||
@@ -74,7 +71,7 @@ class ORM(Broker):
|
||||
if (
|
||||
self.get_connection()
|
||||
.filter(id=task.id, lock=task.lock)
|
||||
.update(lock=self.timeout(task))
|
||||
.update(lock=_timeout())
|
||||
):
|
||||
task_list.append((task.pk, task.payload))
|
||||
# else don't process, as another cluster has been faster than us on
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
# Standard
|
||||
import multiprocessing
|
||||
import os
|
||||
from django_q.scheduler import Scheduler
|
||||
from django_q.puller import Puller
|
||||
from django_q.worker import Pool
|
||||
import signal
|
||||
from django_q.monitor import Monitor
|
||||
import socket
|
||||
import uuid
|
||||
from multiprocessing import Event, Process, Value, current_process
|
||||
from multiprocessing import Event, Process, current_process
|
||||
from time import sleep
|
||||
|
||||
# Django
|
||||
@@ -22,27 +24,17 @@ from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
# Local
|
||||
import django_q.tasks
|
||||
from django_q.brokers import Broker, get_broker
|
||||
from django_q.conf import (
|
||||
Conf,
|
||||
get_ppid,
|
||||
logger,
|
||||
prometheus_multiprocess,
|
||||
psutil,
|
||||
setproctitle,
|
||||
)
|
||||
from django_q.humanhash import humanize
|
||||
from django_q.monitor import monitor
|
||||
from django_q.pusher import pusher
|
||||
from django_q.queues import Queue
|
||||
from django_q.scheduler import scheduler
|
||||
from django_q.status import Stat, Status
|
||||
from django_q.worker import worker
|
||||
|
||||
|
||||
def get_mp_context():
|
||||
return multiprocessing.get_context("fork")
|
||||
|
||||
|
||||
class Cluster:
|
||||
def __init__(self, broker: Broker = None):
|
||||
@@ -65,9 +57,8 @@ class Cluster:
|
||||
# Start Sentinel
|
||||
self.stop_event = Event()
|
||||
self.start_event = Event()
|
||||
self.sentinel = get_mp_context().Process(
|
||||
self.sentinel = Process(
|
||||
target=Sentinel,
|
||||
name=f"Process-{uuid.uuid4().hex}",
|
||||
args=(
|
||||
self.stop_event,
|
||||
self.start_event,
|
||||
@@ -78,30 +69,7 @@ class Cluster:
|
||||
)
|
||||
self.sentinel.start()
|
||||
logger.info(_("Q Cluster %(name)s starting.") % {"name": self.name})
|
||||
# Wait for the sentinel to set start_event by polling the event's state. We
|
||||
# cannot call wait() as that will deadlock if a signal is received while blocked
|
||||
# in wait(), as the sentinel will block in start_event.set() while this process
|
||||
# is blocked in sentinel.join(). It is also necessary to check for start_event
|
||||
# being set to None by the signal handler.
|
||||
while self.start_event and not self.start_event.is_set():
|
||||
# While waiting for the sentinel to start, also check for sentinel premature
|
||||
# death
|
||||
if not self.sentinel.is_alive():
|
||||
if self.sentinel.exitcode == 0:
|
||||
# the cluster was stopped via SIGTERM/SIGINT and sentinel early
|
||||
# termination was intentional -- exit quietly
|
||||
break
|
||||
logger.error(
|
||||
_(
|
||||
"Q Cluster %(name)s failed to start. Sentinel exited "
|
||||
"with exit code %(exitcode)s."
|
||||
)
|
||||
% {"name": self.name, "exitcode": self.sentinel.exitcode}
|
||||
)
|
||||
raise RuntimeError(
|
||||
f"Q Cluster {self.name} failed to start: sentinel exited "
|
||||
f"with code {self.sentinel.exitcode}"
|
||||
)
|
||||
while not self.start_event.is_set():
|
||||
sleep(0.1)
|
||||
return self.pid
|
||||
|
||||
@@ -180,22 +148,17 @@ class Sentinel:
|
||||
self.tob = timezone.now()
|
||||
self.stop_event = stop_event
|
||||
self.start_event = start_event
|
||||
self.pool_size = Conf.WORKERS
|
||||
self.pool = []
|
||||
self.timeout = timeout or Conf.TIMEOUT
|
||||
self.task_queue = (
|
||||
Queue(maxsize=Conf.QUEUE_LIMIT) if Conf.QUEUE_LIMIT else Queue()
|
||||
)
|
||||
self.result_queue = Queue()
|
||||
self.event_out = Event()
|
||||
self.monitor = None
|
||||
self.pusher = None
|
||||
logger.info(
|
||||
_("%(name)s main at %(id)s") % {"name": self.name, "id": current_process().pid}
|
||||
)
|
||||
if start:
|
||||
self.start()
|
||||
|
||||
def queue_name(self):
|
||||
# multi-queue: cluster name is (broker's) queue_name
|
||||
return self.broker.list_key if self.broker else "--"
|
||||
return self.broker.list_key if self.broker else '--'
|
||||
|
||||
def start(self):
|
||||
self.broker.ping()
|
||||
@@ -206,210 +169,140 @@ class Sentinel:
|
||||
if not self.start_event.is_set() and not self.stop_event.is_set():
|
||||
return Conf.STARTING
|
||||
elif self.start_event.is_set() and not self.stop_event.is_set():
|
||||
if self.result_queue.empty() and self.task_queue.empty():
|
||||
if self.monitor.is_idle and self.pool.is_done:
|
||||
return Conf.IDLE
|
||||
return Conf.WORKING
|
||||
elif self.stop_event.is_set() and self.start_event.is_set():
|
||||
if self.monitor.is_alive() or self.pusher.is_alive() or len(self.pool) > 0:
|
||||
if self.monitor.is_alive or self.puller.is_alive or len(self.pool.workers) > 0:
|
||||
return Conf.STOPPING
|
||||
return Conf.STOPPED
|
||||
|
||||
def spawn_process(self, target, *args) -> Process:
|
||||
"""
|
||||
:type target: function or class
|
||||
"""
|
||||
p = get_mp_context().Process(
|
||||
target=target, args=args, name=f"Process-{uuid.uuid4().hex}"
|
||||
)
|
||||
p.daemon = True
|
||||
if target == worker:
|
||||
p.daemon = Conf.DAEMONIZE_WORKERS
|
||||
p.timer = args[2]
|
||||
self.pool.append(p)
|
||||
p.start()
|
||||
return p
|
||||
|
||||
def spawn_pusher(self) -> Process:
|
||||
return self.spawn_process(pusher, self.task_queue, self.event_out, self.broker)
|
||||
|
||||
def spawn_worker(self):
|
||||
self.spawn_process(
|
||||
worker, self.task_queue, self.result_queue, Value("f", -1), self.timeout
|
||||
)
|
||||
|
||||
def spawn_monitor(self) -> Process:
|
||||
return self.spawn_process(monitor, self.result_queue, self.broker)
|
||||
|
||||
def reincarnate(self, process):
|
||||
"""
|
||||
:param process: the process to reincarnate
|
||||
:type process: Process or None
|
||||
"""
|
||||
# close connections before spawning new process
|
||||
if not Conf.SYNC:
|
||||
db.connections.close_all()
|
||||
if process == self.monitor:
|
||||
self.monitor = self.spawn_monitor()
|
||||
logger.critical(
|
||||
_("reincarnated monitor %(name)s after sudden death")
|
||||
% {"name": process.name}
|
||||
)
|
||||
elif process == self.pusher:
|
||||
self.pusher = self.spawn_pusher()
|
||||
logger.critical(
|
||||
_("reincarnated pusher %(name)s after sudden death")
|
||||
% {"name": process.name}
|
||||
)
|
||||
else:
|
||||
# check if prometheus is proper configurated
|
||||
prometheus_path = os.getenv(
|
||||
"PROMETHEUS_MULTIPROC_DIR", os.getenv("prometheus_multiproc_dir")
|
||||
)
|
||||
|
||||
if prometheus_multiprocess and prometheus_path:
|
||||
prometheus_multiprocess.mark_process_dead(process.pid)
|
||||
|
||||
self.pool.remove(process)
|
||||
self.spawn_worker()
|
||||
if process.timer.value == 0:
|
||||
# only need to terminate on timeout, otherwise we risk destabilizing
|
||||
# the queues
|
||||
task_name = ""
|
||||
if psutil:
|
||||
try:
|
||||
process_name = psutil.Process(process.pid).name()
|
||||
name_splits = process_name.split(" ")
|
||||
task_name = (
|
||||
name_splits[3]
|
||||
if len(name_splits) >= 4 and name_splits[2] == "processing"
|
||||
else ""
|
||||
)
|
||||
except psutil.NoSuchProcess:
|
||||
pass
|
||||
process.terminate()
|
||||
if task_name:
|
||||
msg = _(
|
||||
"reincarnated worker %(name)s after timeout while processing task %(task_name)s"
|
||||
) % {"name": process.name, "task_name": task_name}
|
||||
else:
|
||||
msg = _("reincarnated worker %(name)s after timeout") % {
|
||||
"name": process.name
|
||||
}
|
||||
logger.critical(msg)
|
||||
elif int(process.timer.value) == -2:
|
||||
logger.info(_("recycled worker %(name)s") % {"name": process.name})
|
||||
else:
|
||||
logger.critical(
|
||||
_("reincarnated worker %(name)s after death")
|
||||
% {"name": process.name}
|
||||
)
|
||||
|
||||
self.reincarnations += 1
|
||||
|
||||
def spawn_cluster(self):
|
||||
self.pool = []
|
||||
Stat(self).save()
|
||||
# close connections before spawning new process
|
||||
if not Conf.SYNC:
|
||||
db.connections.close_all()
|
||||
# spawn worker pool
|
||||
for __ in range(self.pool_size):
|
||||
self.spawn_worker()
|
||||
# spawn auxiliary
|
||||
self.monitor = self.spawn_monitor()
|
||||
self.pusher = self.spawn_pusher()
|
||||
self.pool = Pool()
|
||||
self.puller = Puller()
|
||||
self.monitor = Monitor()
|
||||
self.scheduler = Scheduler()
|
||||
# set worker cpu affinity if needed
|
||||
if psutil and Conf.CPU_AFFINITY:
|
||||
set_cpu_affinity(Conf.CPU_AFFINITY, [w.pid for w in self.pool])
|
||||
set_cpu_affinity(Conf.CPU_AFFINITY, [w.process.pid for w in self.pool.workers])
|
||||
Stat(self).save()
|
||||
|
||||
|
||||
def guard(self):
|
||||
logger.info(
|
||||
_("%(name)s guarding cluster %(cluster_name)s")
|
||||
% {
|
||||
"name": current_process().name,
|
||||
"cluster_name": humanize(self.cluster_id.hex)
|
||||
+ f" [{self.queue_name()}]",
|
||||
"cluster_name": humanize(self.cluster_id.hex) + f" [{self.queue_name()}]",
|
||||
}
|
||||
)
|
||||
self.start_event.set()
|
||||
Stat(self).save()
|
||||
logger.info(
|
||||
_("Q Cluster %(cluster_name)s running.")
|
||||
% {
|
||||
"cluster_name": humanize(self.cluster_id.hex)
|
||||
+ f" [{self.queue_name()}]"
|
||||
}
|
||||
% {"cluster_name": humanize(self.cluster_id.hex) + f" [{self.queue_name()}]"}
|
||||
)
|
||||
counter = 0
|
||||
cycle = Conf.GUARD_CYCLE # guard loop sleep in seconds
|
||||
# Guard loop. Runs at least once
|
||||
while True:
|
||||
# Check Workers
|
||||
for p in self.pool:
|
||||
with p.timer.get_lock():
|
||||
# Are you alive?
|
||||
if not p.is_alive() or p.timer.value == 0:
|
||||
self.reincarnate(p)
|
||||
continue
|
||||
# Decrement timer if work is being done
|
||||
if p.timer.value > 0:
|
||||
p.timer.value -= cycle
|
||||
# Check Monitor
|
||||
if not self.monitor.is_alive():
|
||||
self.reincarnate(self.monitor)
|
||||
# Check Pusher
|
||||
if not self.pusher.is_alive():
|
||||
self.reincarnate(self.pusher)
|
||||
# Call scheduler once a minute (or so)
|
||||
counter += cycle
|
||||
if counter >= 30 and Conf.SCHEDULER:
|
||||
counter = 0
|
||||
scheduler(broker=self.broker)
|
||||
# Save current status
|
||||
while not self.stop_event.is_set() or not counter:
|
||||
# Check if the pool of workers is healthy
|
||||
logger.info("Check if pool is healthy")
|
||||
if not self.pool.is_healthy:
|
||||
# reincarnate workers that died
|
||||
print("reincarnate workers")
|
||||
self.pool.reincarnate_stopped_workers()
|
||||
|
||||
print("Check if puller is healthy")
|
||||
if not self.puller.is_alive:
|
||||
self.puller.reincarnate_process()
|
||||
|
||||
print("Check if monitor is healthy")
|
||||
if not self.monitor.is_alive:
|
||||
self.monitor.reincarnate_process()
|
||||
|
||||
print("Check if scheduler is healthy")
|
||||
if not self.scheduler.is_alive:
|
||||
self.scheduler.reincarnate_process()
|
||||
|
||||
|
||||
print("add tasks and mark workers idle")
|
||||
for worker in self.pool.get_done_workers():
|
||||
# put result in task_queue to be picked up by monitor for processing
|
||||
self.monitor.add_task(worker.get_result())
|
||||
# mark task back to idle or reincarnate to be picked up for a new task
|
||||
if worker.is_recycle:
|
||||
worker.reincarnate_process()
|
||||
else:
|
||||
worker.mark_idle()
|
||||
|
||||
# check if monitor has items to process
|
||||
print("run monitor item")
|
||||
self.monitor.run_item()
|
||||
|
||||
print("Add task to worker pool")
|
||||
if self.puller.has_results:
|
||||
self.pool.add_task(self.puller.get_result())
|
||||
|
||||
# delegate tasks to workers that are now available
|
||||
print("delegate tasks")
|
||||
self.pool.delegate_tasks()
|
||||
|
||||
logger.info("sleep")
|
||||
counter += 1
|
||||
sleep(Conf.GUARD_CYCLE)
|
||||
Stat(self).save()
|
||||
if self.stop_event.wait(cycle):
|
||||
break
|
||||
self.stop()
|
||||
|
||||
def stop(self):
|
||||
Stat(self).save()
|
||||
name = current_process().name
|
||||
logger.info(_("%(name)s stopping cluster processes") % {"name": name})
|
||||
# Stopping pusher
|
||||
self.event_out.set()
|
||||
# Wait for it to stop
|
||||
while self.pusher.is_alive():
|
||||
sleep(0.1)
|
||||
Stat(self).save()
|
||||
# Put poison pills in the queue
|
||||
for __ in range(len(self.pool)):
|
||||
self.task_queue.put("STOP")
|
||||
self.task_queue.close()
|
||||
# wait for the task queue to empty
|
||||
self.task_queue.join_thread()
|
||||
# Wait for all the workers to exit
|
||||
while len(self.pool):
|
||||
for p in self.pool:
|
||||
if not p.is_alive():
|
||||
self.pool.remove(p)
|
||||
sleep(0.1)
|
||||
Stat(self).save()
|
||||
# Finally stop the monitor
|
||||
self.result_queue.put("STOP")
|
||||
self.result_queue.close()
|
||||
# Wait for the result queue to empty
|
||||
self.result_queue.join_thread()
|
||||
logger.info(_("%(name)s waiting for the monitor.") % {"name": name})
|
||||
# Wait for everything to close or time out
|
||||
count = 0
|
||||
if not self.timeout:
|
||||
self.timeout = 30
|
||||
while self.status() == Conf.STOPPING and count < self.timeout * 10:
|
||||
sleep(0.1)
|
||||
Stat(self).save()
|
||||
count += 1
|
||||
# Final status
|
||||
Stat(self).save()
|
||||
# Stopping guard
|
||||
self.stop_event.set()
|
||||
logger.debug(_("Guard has stopped"))
|
||||
# Stop scheduler
|
||||
self.scheduler.stop_scheduler()
|
||||
|
||||
# End all workers gracefully
|
||||
for __ in range(Conf.WORKERS):
|
||||
self.pool.add_task("STOP")
|
||||
|
||||
# make sure the tasks queue in the pool is empty and workers are idle max timeout 20 sec
|
||||
time_passed = 0
|
||||
while not self.pool.is_done and time_passed <= 20:
|
||||
self.monitor.run_item()
|
||||
self.pool.delegate_tasks()
|
||||
time_passed += 0.5
|
||||
sleep(0.5)
|
||||
if time_passed >= 20:
|
||||
logger.error(_("Couldn't terminate tasks within 20 seconds, killing processes now"))
|
||||
for worker in self.pool.workers:
|
||||
worker.process.kill()
|
||||
|
||||
logger.debug(_("All tasks were processed and workers where stopped"))
|
||||
|
||||
self.monitor.add_task("STOP")
|
||||
while not self.monitor.is_done:
|
||||
# in the case the monitor was behind, let's run through all
|
||||
self.monitor.run_item()
|
||||
sleep(0.5)
|
||||
|
||||
logger.debug(_("All tasks were saved"))
|
||||
|
||||
self.puller.stop_puller()
|
||||
|
||||
# make sure all processes are terminated
|
||||
for worker in self.pool.workers:
|
||||
worker.process.terminate()
|
||||
|
||||
self.monitor.process.terminate()
|
||||
self.puller.process.terminate()
|
||||
self.scheduler.process.terminate()
|
||||
|
||||
logger.debug(_("All processes were terminated"))
|
||||
|
||||
|
||||
def set_cpu_affinity(n: int, process_ids: list, actual: bool = not Conf.TESTING):
|
||||
|
||||
@@ -1,22 +1,15 @@
|
||||
import logging
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
from copy import deepcopy
|
||||
from multiprocessing import cpu_count
|
||||
from signal import signal
|
||||
from warnings import warn
|
||||
|
||||
import pkg_resources
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from django_q.queues import Queue
|
||||
|
||||
# The "selectable" entry points were introduced in importlib_metadata 3.6 and Python 3.10.
|
||||
if sys.version_info < (3, 10):
|
||||
from importlib_metadata import entry_points
|
||||
else:
|
||||
from importlib.metadata import entry_points
|
||||
from queue import Queue
|
||||
|
||||
# optional
|
||||
try:
|
||||
@@ -39,11 +32,6 @@ try:
|
||||
except ModuleNotFoundError:
|
||||
setproctitle = None
|
||||
|
||||
try:
|
||||
from prometheus_client import multiprocess as prometheus_multiprocess
|
||||
except ModuleNotFoundError:
|
||||
prometheus_multiprocess = None
|
||||
|
||||
|
||||
class Conf:
|
||||
"""
|
||||
@@ -56,18 +44,15 @@ class Conf:
|
||||
conf = {}
|
||||
|
||||
_Q_CLUSTER_NAME = os.getenv("Q_CLUSTER_NAME")
|
||||
if (
|
||||
_Q_CLUSTER_NAME
|
||||
and _Q_CLUSTER_NAME != conf.get("name")
|
||||
and _Q_CLUSTER_NAME != conf.get("cluster_name")
|
||||
):
|
||||
if _Q_CLUSTER_NAME and _Q_CLUSTER_NAME != conf.get("name") and \
|
||||
_Q_CLUSTER_NAME != conf.get("cluster_name"):
|
||||
conf["cluster_name"] = _Q_CLUSTER_NAME
|
||||
alt_conf = conf.pop("ALT_CLUSTERS")
|
||||
if isinstance(alt_conf, dict):
|
||||
alt_conf = alt_conf.get(_Q_CLUSTER_NAME)
|
||||
if isinstance(alt_conf, dict):
|
||||
alt_conf.pop("name", None)
|
||||
alt_conf.pop("cluster_name", None)
|
||||
alt_conf.pop('name', None)
|
||||
alt_conf.pop('cluster_name', None)
|
||||
conf.update(alt_conf)
|
||||
|
||||
# Redis server configuration . Follows standard redis keywords
|
||||
@@ -106,7 +91,7 @@ class Conf:
|
||||
CLUSTER_NAME = conf.get("cluster_name", PREFIX)
|
||||
|
||||
# Log output level
|
||||
LOG_LEVEL = conf.get("log_level", "INFO")
|
||||
LOG_LEVEL = conf.get("log_level", "DEBUG")
|
||||
|
||||
# Maximum number of successful tasks kept in the database. 0 saves everything.
|
||||
# -1 saves none
|
||||
@@ -127,7 +112,7 @@ class Conf:
|
||||
)
|
||||
|
||||
# Guard loop sleep in seconds. Should be between 0 and 60 seconds.
|
||||
GUARD_CYCLE = conf.get("guard_cycle", 0.5)
|
||||
GUARD_CYCLE = conf.get("guard_cycle", 1)
|
||||
|
||||
# Disable the scheduler
|
||||
SCHEDULER = conf.get("scheduler", True)
|
||||
@@ -212,12 +197,7 @@ class Conf:
|
||||
|
||||
# Use the secret key for package signing
|
||||
# Django itself should raise an error if it's not configured
|
||||
# but suppress the exception early to allow other parts of the app to still run.
|
||||
# For example any "python manage.py ..." command still runs even if SECRET_KEY is not set.
|
||||
try:
|
||||
SECRET_KEY = settings.SECRET_KEY
|
||||
except ImproperlyConfigured:
|
||||
SECRET_KEY = None
|
||||
SECRET_KEY = settings.SECRET_KEY
|
||||
|
||||
# The redis stats key
|
||||
Q_STAT = f"django_q:{PREFIX}:cluster"
|
||||
@@ -260,6 +240,7 @@ class Conf:
|
||||
# logger
|
||||
logger = logging.getLogger("django-q")
|
||||
|
||||
|
||||
# Set up standard logging handler in case there is none
|
||||
if not logger.hasHandlers():
|
||||
logger.setLevel(level=getattr(logging, Conf.LOG_LEVEL))
|
||||
@@ -274,6 +255,7 @@ if not logger.hasHandlers():
|
||||
|
||||
# Error Reporting Interface
|
||||
class ErrorReporter:
|
||||
|
||||
# initialize with iterator of reporters (better name, targets?)
|
||||
def __init__(self, reporters):
|
||||
self.targets = [target for target in reporters]
|
||||
@@ -292,7 +274,9 @@ if Conf.ERROR_REPORTER:
|
||||
# iterate through the configured error reporters,
|
||||
# and instantiate an ErrorReporter using the provided config
|
||||
for name, conf in error_conf.items():
|
||||
for entry in entry_points(group="djangoq.errorreporters", name=name):
|
||||
for entry in pkg_resources.iter_entry_points(
|
||||
"djangoq.errorreporters", name
|
||||
):
|
||||
Reporter = entry.load()
|
||||
reporters.append(Reporter(**conf))
|
||||
error_reporter = ErrorReporter(reporters)
|
||||
|
||||
@@ -2,24 +2,16 @@ import datetime
|
||||
import time
|
||||
import zlib
|
||||
|
||||
from django.core.signing import (
|
||||
BadSignature,
|
||||
JSONSerializer,
|
||||
SignatureExpired,
|
||||
b64_decode,
|
||||
dumps,
|
||||
)
|
||||
from django.core.signing import BadSignature, JSONSerializer, SignatureExpired
|
||||
from django.core.signing import Signer as Sgnr
|
||||
from django.core.signing import TimestampSigner as TsS
|
||||
from django.core.signing import b64_decode, dumps
|
||||
|
||||
try:
|
||||
from django.core.signing import b62_decode
|
||||
from django.core.signing import base62
|
||||
except ImportError:
|
||||
# fallback for django 3.x
|
||||
# For django < 4.0
|
||||
from django.utils.baseconv import base62
|
||||
|
||||
b62_decode = base62.decode
|
||||
|
||||
from django.utils.crypto import constant_time_compare
|
||||
from django.utils.encoding import force_bytes, force_str
|
||||
|
||||
@@ -45,9 +37,7 @@ def loads(
|
||||
"""
|
||||
# TimestampSigner.unsign() returns str but base64 and zlib compression
|
||||
# operate on bytes.
|
||||
base64d = force_bytes(
|
||||
TimestampSigner(key=key, salt=salt).unsign(s, max_age=max_age)
|
||||
)
|
||||
base64d = force_bytes(TimestampSigner(key, salt=salt).unsign(s, max_age=max_age))
|
||||
decompress = False
|
||||
if base64d[:1] == b".":
|
||||
# It's compressed; uncompress it first
|
||||
@@ -84,7 +74,7 @@ class TimestampSigner(Signer, TsS):
|
||||
"""
|
||||
result = super(TimestampSigner, self).unsign(value)
|
||||
value, timestamp = result.rsplit(self.sep, 1)
|
||||
timestamp = b62_decode(timestamp)
|
||||
timestamp = base62.decode(timestamp)
|
||||
if max_age is not None:
|
||||
if isinstance(max_age, datetime.timedelta):
|
||||
max_age = max_age.total_seconds()
|
||||
|
||||
@@ -1,7 +1,26 @@
|
||||
class TimeoutException(SystemExit):
|
||||
"""
|
||||
Exception for when a worker takes too long to complete a task
|
||||
Raising SystemExit will make sure the function terminates gracefully.
|
||||
"""
|
||||
import signal
|
||||
from typing import Optional
|
||||
|
||||
class TimeoutException(SystemExit):
|
||||
"""Exception for when a worker takes too long to complete a task"""
|
||||
pass
|
||||
|
||||
|
||||
class TimeoutHandler:
|
||||
def __init__(self, timeout: Optional[int] = None):
|
||||
self._timeout = timeout
|
||||
|
||||
def raise_timeout_exception(self, signum, frame):
|
||||
raise TimeoutException('Task exceeded maximum timeout value '
|
||||
'({0} seconds)'.format(self._timeout))
|
||||
|
||||
def __enter__(self):
|
||||
if self._timeout is None:
|
||||
return
|
||||
signal.signal(signal.SIGALRM, self.raise_timeout_exception)
|
||||
signal.alarm(self._timeout)
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
"""When getting out of the timeout, reset the alarm, so it won't trigger"""
|
||||
signal.alarm(0)
|
||||
signal.signal(signal.SIGALRM, signal.SIG_DFL)
|
||||
|
||||
43
django_q/helpers.py
Normal file
43
django_q/helpers.py
Normal file
@@ -0,0 +1,43 @@
|
||||
from copy import Error
|
||||
from django_q.worker import WorkerProcess
|
||||
from django_q.monitor import Monitor
|
||||
from django_q.models import Task
|
||||
from typing import Optional, Sequence, Tuple
|
||||
from django_q.conf import logger
|
||||
from django_q.queue_task import QueueTask
|
||||
from django_q.puller import Puller
|
||||
from django_q.scheduler import Scheduler
|
||||
|
||||
|
||||
def run_scheduler_once(broker=None) -> None:
|
||||
Scheduler.schedule_tasks(broker)
|
||||
|
||||
def get_scheduled_tasks(broker=None) -> Sequence[QueueTask]:
|
||||
try:
|
||||
return Puller.get_tasks_from_broker(broker=broker)
|
||||
except ValueError:
|
||||
logger.exception("Couldn't get items from broker")
|
||||
return []
|
||||
|
||||
def run_task(task=None) -> QueueTask:
|
||||
if task is None:
|
||||
|
||||
scheduled_tasks = get_scheduled_tasks()
|
||||
if not len(scheduled_tasks):
|
||||
raise ValueError("No tasks scheduled and no task given to run for worker")
|
||||
task = scheduled_tasks[0]
|
||||
|
||||
return WorkerProcess.run_task(task)
|
||||
|
||||
def save_task(task, broker=None) -> Tuple[QueueTask, Optional[Task]]:
|
||||
return Monitor.save_task(task, broker)
|
||||
|
||||
def run_cluster_once(workers, tasks=[], broker=None) -> None:
|
||||
if not len(tasks):
|
||||
run_scheduler_once(broker=broker)
|
||||
tasks = get_scheduled_tasks(broker=broker)
|
||||
|
||||
for idx, worker in enumerate(range(workers)):
|
||||
if len(tasks) >= idx + 1:
|
||||
task = run_task(tasks[idx])
|
||||
save_task(task, broker=broker)
|
||||
@@ -4,7 +4,6 @@ humanhash: Human-readable representations of digests.
|
||||
The simplest ways to use this module are the :func:`humanize` and :func:`uuid`
|
||||
functions. For tighter control over the output, see :class:`HumanHasher`.
|
||||
"""
|
||||
|
||||
import operator
|
||||
import uuid as uuidlib
|
||||
from argparse import ArgumentError
|
||||
@@ -271,6 +270,7 @@ DEFAULT_WORDLIST = (
|
||||
|
||||
|
||||
class HumanHasher:
|
||||
|
||||
"""
|
||||
Transforms hex digests to human-readable strings.
|
||||
|
||||
@@ -290,6 +290,7 @@ class HumanHasher:
|
||||
self.wordlist = wordlist
|
||||
|
||||
def humanize(self, hexdigest, words=4, separator="-"):
|
||||
|
||||
"""
|
||||
Humanize a given hexadecimal digest.
|
||||
|
||||
@@ -313,6 +314,7 @@ class HumanHasher:
|
||||
|
||||
@staticmethod
|
||||
def compress(bytes, target):
|
||||
|
||||
"""
|
||||
Compress a list of byte values to a fixed target length.
|
||||
|
||||
@@ -351,6 +353,7 @@ class HumanHasher:
|
||||
return checksums
|
||||
|
||||
def uuid(self, **params):
|
||||
|
||||
"""
|
||||
Generate a UUID with a human-readable representation.
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-04-17 21:10+0000\n"
|
||||
"POT-Creation-Date: 2023-01-26 01:38+0000\n"
|
||||
"PO-Revision-Date: 2018-08-05 18:28+0200\n"
|
||||
"Last-Translator: Jonas Winkler\n"
|
||||
"Language-Team: \n"
|
||||
@@ -17,45 +17,37 @@ msgstr ""
|
||||
"X-Generator: Poedit 2.1.1\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
|
||||
#: admin.py:30
|
||||
#: admin.py:43
|
||||
msgid "Resubmit selected tasks to queue"
|
||||
msgstr "Ausgewählte Aufgaben erneut ausführen"
|
||||
|
||||
#: admin.py:64
|
||||
msgid "Time Taken"
|
||||
msgstr ""
|
||||
|
||||
#: admin.py:95
|
||||
msgid "Short result"
|
||||
msgstr ""
|
||||
|
||||
#: admin.py:142 models.py:412
|
||||
#: admin.py:107 models.py:293
|
||||
#, fuzzy
|
||||
#| msgid "Success"
|
||||
msgid "success"
|
||||
msgstr "erfolg"
|
||||
|
||||
#: admin.py:154 models.py:414
|
||||
#: admin.py:119 models.py:295
|
||||
msgid "last_run"
|
||||
msgstr ""
|
||||
|
||||
#: cluster.py:75
|
||||
#: cluster.py:79
|
||||
#, python-format
|
||||
msgid "Q Cluster %(name)s starting."
|
||||
msgstr "Q-Cluster %(name)s wird gestartet."
|
||||
|
||||
#: cluster.py:83
|
||||
#: cluster.py:87
|
||||
#, fuzzy, python-format
|
||||
#| msgid "Q Cluster-{} stopping."
|
||||
msgid "Q Cluster %(name)s stopping."
|
||||
msgstr "Q-Cluster {name} wird gestoppt."
|
||||
|
||||
#: cluster.py:86
|
||||
#: cluster.py:90
|
||||
#, python-format
|
||||
msgid "Q Cluster %(name)s has stopped."
|
||||
msgstr "Q-Cluster %(name)s wurde gestoppt."
|
||||
|
||||
#: cluster.py:93
|
||||
#: cluster.py:97
|
||||
#, python-format
|
||||
msgid "%(name)s got signal %(signal)s"
|
||||
msgstr "%(name)s erhielt das Signal %(signal)s"
|
||||
@@ -70,7 +62,7 @@ msgstr "Monitor %(name)s wurde nach unerwartetem Absturz neu gestartet"
|
||||
msgid "reincarnated pusher %(name)s after sudden death"
|
||||
msgstr "Pusher %(name)s wurde nach unerwartetem Absturz neu gestartet"
|
||||
|
||||
#: cluster.py:262
|
||||
#: cluster.py:250
|
||||
#, fuzzy, python-format
|
||||
#| msgid "reincarnated worker %(name)s after timeout"
|
||||
msgid ""
|
||||
@@ -78,57 +70,123 @@ msgid ""
|
||||
"%(task_name)s"
|
||||
msgstr "Worker %(name)s wurde nach Zeitüberschreitung neu gestartet"
|
||||
|
||||
#: cluster.py:265
|
||||
#: cluster.py:255
|
||||
#, python-format
|
||||
msgid "reincarnated worker %(name)s after timeout"
|
||||
msgstr "Worker %(name)s wurde nach Zeitüberschreitung neu gestartet"
|
||||
|
||||
#: cluster.py:270
|
||||
#: cluster.py:260
|
||||
#, python-format
|
||||
msgid "recycled worker %(name)s"
|
||||
msgstr "Worker %(name)s wurde wiederverwendet"
|
||||
|
||||
#: cluster.py:273
|
||||
#: cluster.py:263
|
||||
#, python-format
|
||||
msgid "reincarnated worker %(name)s after death"
|
||||
msgstr "Worker %(name)s wurde nach unerwartetem Absturz neu gestartet"
|
||||
|
||||
#: cluster.py:297
|
||||
#: cluster.py:287
|
||||
#, python-format
|
||||
msgid "%(name)s guarding cluster %(cluster_name)s"
|
||||
msgstr "%(name)s beschützt das Cluster %(cluster_name)s"
|
||||
|
||||
#: cluster.py:307
|
||||
#: cluster.py:296
|
||||
#, python-format
|
||||
msgid "Q Cluster %(cluster_name)s running."
|
||||
msgstr "Q-Cluster %(cluster_name)s läuft."
|
||||
|
||||
#: cluster.py:346
|
||||
#: cluster.py:332
|
||||
#, python-format
|
||||
msgid "%(name)s stopping cluster processes"
|
||||
msgstr "%(name)s hält Cluster-Prozesse an"
|
||||
|
||||
#: cluster.py:371
|
||||
#: cluster.py:357
|
||||
#, python-format
|
||||
msgid "%(name)s waiting for the monitor."
|
||||
msgstr "%(name)s wartet auf den Monitor."
|
||||
|
||||
#: cluster.py:394
|
||||
#: cluster.py:383
|
||||
#, fuzzy, python-format
|
||||
#| msgid "%(process_name)s pushing tasks at %(id)s"
|
||||
msgid "%(name)s pushing tasks at %(id)s"
|
||||
msgstr "%(process_name)s veröffentlicht Aufagaben auf %(id)s"
|
||||
|
||||
#: cluster.py:407
|
||||
#, python-format
|
||||
msgid "queueing from %(list_key)s"
|
||||
msgstr "Einreihen von %(list_key)s"
|
||||
|
||||
#: cluster.py:411
|
||||
#, python-format
|
||||
msgid "%(name)s stopped pushing tasks"
|
||||
msgstr "%(name)s veröffentlicht keine Aufgaben mehr"
|
||||
|
||||
#: cluster.py:426
|
||||
#, python-format
|
||||
msgid "%(name)s monitoring at %(id)s"
|
||||
msgstr "%(name)s beobachtet auf %(id)s"
|
||||
|
||||
#: cluster.py:445
|
||||
#, python-format
|
||||
msgid "Processed '%(info_name)s' (%(task_name)s)"
|
||||
msgstr "[%(task_name)s] - '%(info_name)s' wurde verarbeitet"
|
||||
|
||||
#: cluster.py:451
|
||||
#, python-format
|
||||
msgid "Failed '%(info_name)s' (%(task_name)s) - %(task_result)s"
|
||||
msgstr "'%(info_name)s' (%(task_name)s) ist fehlgeschlagen - %(task_result)s"
|
||||
|
||||
#: cluster.py:458
|
||||
#, python-format
|
||||
msgid "%(name)s stopped monitoring results"
|
||||
msgstr "%(name)s überwacht keine Ergebnisse mehr"
|
||||
|
||||
#: cluster.py:474
|
||||
#, python-format
|
||||
msgid "%(proc_name)s ready for work at %(id)s"
|
||||
msgstr "%(proc_name)s ist bereit für Arbeit auf %(id)s"
|
||||
|
||||
#: cluster.py:494
|
||||
#, fuzzy, python-format
|
||||
#| msgid "%(proc_name)s processing '%(func_name)s' (%(task_name)s)"
|
||||
msgid "%(proc_name)s processing %(task_name)s '%(func_name)s'"
|
||||
msgstr "%(proc_name)s verarbeitet '%(func_name)s' (%(task_name)s)"
|
||||
|
||||
#: cluster.py:546
|
||||
#, python-format
|
||||
msgid "%(proc_name)s stopped doing work"
|
||||
msgstr "%(proc_name)s hat die Arbeit beendet"
|
||||
|
||||
#: cluster.py:751
|
||||
#, python-format
|
||||
msgid "%(process_name)s failed to create a task from schedule [%(schedule)s]"
|
||||
msgstr ""
|
||||
"%(process_name)s konnte keine Aufgabe von Zeitplan [%(schedule)s] erstellen"
|
||||
|
||||
#: cluster.py:762
|
||||
#, fuzzy, python-format
|
||||
#| msgid "%(process_name)s created a task from schedule [%(schedule)s]"
|
||||
msgid ""
|
||||
"%(process_name)s created task %(task_name)s from schedule [%(schedule)s]"
|
||||
msgstr ""
|
||||
"%(process_name)s hat eine Aufgabe des Zeitplans [%(schedule)s] erstellt"
|
||||
|
||||
#: cluster.py:808
|
||||
msgid "Skipping cpu affinity because psutil was not found."
|
||||
msgstr "Cpu-Affinität wird übersprungen, da psutil nicht gefunden wurde."
|
||||
|
||||
#: cluster.py:399
|
||||
#: cluster.py:813
|
||||
msgid "Faking cpu affinity because it is not supported on this platform"
|
||||
msgstr ""
|
||||
"Vortäuschen von CPU-Affinität, da diese auf dieser Plattform nicht "
|
||||
"unterstützt wird"
|
||||
|
||||
#: cluster.py:421
|
||||
#: cluster.py:835
|
||||
#, python-format
|
||||
msgid "%(pid)s will use cpu %(affinity)s"
|
||||
msgstr "%(pid)s wird CPU %(affinity)s benutzen"
|
||||
|
||||
#: conf.py:122
|
||||
#: conf.py:90
|
||||
#, python-format
|
||||
msgid ""
|
||||
"SAVE_LIMIT_PER (%(option)s) is not a valid option. Options are: 'group', "
|
||||
@@ -138,28 +196,28 @@ msgstr ""
|
||||
"'group', 'name', 'func' und None. Standard ist None."
|
||||
|
||||
#. Translators: Cluster status descriptions
|
||||
#: conf.py:239
|
||||
#: conf.py:207
|
||||
msgid "Starting"
|
||||
msgstr "Wird gestartet"
|
||||
|
||||
#: conf.py:240
|
||||
#: conf.py:208
|
||||
msgid "Working"
|
||||
msgstr "Arbeitet"
|
||||
|
||||
#: conf.py:241
|
||||
#: conf.py:209
|
||||
msgid "Idle"
|
||||
msgstr "Leerlauf"
|
||||
|
||||
#: conf.py:242
|
||||
#: conf.py:210
|
||||
msgid "Stopped"
|
||||
msgstr "Gestoppt"
|
||||
|
||||
#: conf.py:243
|
||||
#: conf.py:211
|
||||
msgid "Stopping"
|
||||
msgstr "Wird gestoppt"
|
||||
|
||||
#. Translators: help text for qcluster management command
|
||||
#: management/commands/qcluster.py:11
|
||||
#: management/commands/qcluster.py:9
|
||||
msgid "Starts a Django Q Cluster."
|
||||
msgstr "Startet ein Django-Q-Cluster."
|
||||
|
||||
@@ -178,479 +236,253 @@ msgstr "Überwacht die Speichernutzung von Q Cluster"
|
||||
msgid "Monitors Q Cluster activity"
|
||||
msgstr "Q-Cluster aktiv überwachen"
|
||||
|
||||
#: models.py:30 models.py:469
|
||||
msgid "Task id"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:31
|
||||
msgid "32-character task identifier."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:36 models.py:235 models.py:470
|
||||
msgid "Name"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:37
|
||||
msgid "Optional human-readable name for lookup and display."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:41 models.py:240 models.py:468
|
||||
msgid "Function"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:42
|
||||
msgid "Dotted import path to the callable, e.g. myapp.tasks.job."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:47 models.py:247
|
||||
msgid "Hook"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:48
|
||||
msgid "Optional dotted path to a callback that receives the task result."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:53 models.py:253 models.py:472
|
||||
msgid "Arguments"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:54
|
||||
msgid "Pickled positional arguments (tuple)."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:59 models.py:259 models.py:473
|
||||
msgid "Keyword arguments"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:60
|
||||
msgid "Pickled keyword arguments (dict)."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:65
|
||||
msgid "Result"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:66
|
||||
msgid "Pickled return value or error payload."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:72 models.py:471
|
||||
msgid "Group"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:73
|
||||
msgid "Optional group id to aggregate related tasks."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:80 models.py:328
|
||||
#, fuzzy
|
||||
#| msgid "Clusters"
|
||||
msgid "Cluster"
|
||||
msgstr "Cluster"
|
||||
|
||||
#: models.py:81
|
||||
msgid "Name of the cluster that executed this task."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:85
|
||||
msgid "Started at"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:86
|
||||
msgid "When the worker started executing the task."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:90
|
||||
#, fuzzy
|
||||
#| msgid "Stopped"
|
||||
msgid "Stopped at"
|
||||
msgstr "Gestoppt"
|
||||
|
||||
#: models.py:91
|
||||
msgid "When the worker finished (success or failure)."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:96 monitor_terminal.py:180
|
||||
msgid "Success"
|
||||
msgstr "Erfolg"
|
||||
|
||||
#: models.py:97
|
||||
msgid "False if the task raised an exception or timed out."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:101
|
||||
msgid "Attempt count"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:102
|
||||
msgid "How many times execution was attempted."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:172
|
||||
msgid "Task"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:173
|
||||
msgid "Tasks"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:194
|
||||
#: models.py:125
|
||||
msgid "Successful task"
|
||||
msgstr "Erfolgreiche Aufgabe"
|
||||
|
||||
#: models.py:195
|
||||
#: models.py:126
|
||||
msgid "Successful tasks"
|
||||
msgstr "Erfolgreiche Aufgaben"
|
||||
|
||||
#: models.py:210
|
||||
#: models.py:141
|
||||
msgid "Failed task"
|
||||
msgstr "Fehlgeschlagene Aufgabe"
|
||||
|
||||
#: models.py:211
|
||||
#: models.py:142
|
||||
msgid "Failed tasks"
|
||||
msgstr "Fehlgeschlagene Aufgaben"
|
||||
|
||||
#: models.py:219 models.py:347
|
||||
#: models.py:150 models.py:234
|
||||
msgid "Please install croniter to enable cron expressions"
|
||||
msgstr "Bitte installieren Sie croniter, um Cron-Ausdrücke zu aktivieren"
|
||||
|
||||
#: models.py:236
|
||||
msgid "Optional label to identify this schedule in the admin."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:241
|
||||
msgid "e.g. module.tasks.function"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:248
|
||||
msgid "e.g. module.tasks.result_function"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:254
|
||||
#: models.py:170
|
||||
msgid "e.g. 1, 2, 'John'"
|
||||
msgstr "zum Beispiel 1, 2, 'John'"
|
||||
|
||||
#: models.py:260
|
||||
#: models.py:172
|
||||
msgid "e.g. x=1, y=2, name='John'"
|
||||
msgstr "zum Beispiel x=1, y=2, name='John'"
|
||||
|
||||
#: models.py:274
|
||||
#: models.py:186
|
||||
msgid "Once"
|
||||
msgstr "Einmal"
|
||||
|
||||
#: models.py:275 models.py:296
|
||||
#: models.py:187
|
||||
msgid "Minutes"
|
||||
msgstr "Minuten"
|
||||
|
||||
#: models.py:276
|
||||
#: models.py:188
|
||||
msgid "Hourly"
|
||||
msgstr "Stündlich"
|
||||
|
||||
#: models.py:277
|
||||
#: models.py:189
|
||||
msgid "Daily"
|
||||
msgstr "Täglich"
|
||||
|
||||
#: models.py:278
|
||||
#: models.py:190
|
||||
msgid "Weekly"
|
||||
msgstr "Wöchentlich"
|
||||
|
||||
#: models.py:279
|
||||
#: models.py:191
|
||||
msgid "Biweekly"
|
||||
msgstr "Zweiwöchentlich"
|
||||
|
||||
#: models.py:280
|
||||
#: models.py:192
|
||||
msgid "Monthly"
|
||||
msgstr "Monatlich"
|
||||
|
||||
#: models.py:281
|
||||
#: models.py:193
|
||||
msgid "Bimonthly"
|
||||
msgstr "Zweimonatlich"
|
||||
|
||||
#: models.py:282
|
||||
#: models.py:194
|
||||
msgid "Quarterly"
|
||||
msgstr "Vierteljährlich"
|
||||
|
||||
#: models.py:283
|
||||
#: models.py:195
|
||||
msgid "Yearly"
|
||||
msgstr "Jährlich"
|
||||
|
||||
#: models.py:284 models.py:313
|
||||
#: models.py:196
|
||||
msgid "Cron"
|
||||
msgstr "Cron"
|
||||
|
||||
#: models.py:290
|
||||
#: models.py:199
|
||||
msgid "Schedule Type"
|
||||
msgstr "Zeitplan-Typ"
|
||||
|
||||
#: models.py:291
|
||||
msgid "How often this task should be enqueued."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:297
|
||||
#: models.py:202
|
||||
msgid "Number of minutes for the Minutes type"
|
||||
msgstr "Anzahl Minuten für den Typ 'Minuten'"
|
||||
|
||||
#: models.py:300
|
||||
#: models.py:205
|
||||
msgid "Repeats"
|
||||
msgstr "Wiederholungen"
|
||||
msgstr "Wiederhohlungen"
|
||||
|
||||
#: models.py:300
|
||||
#: models.py:205
|
||||
msgid "n = n times, -1 = forever"
|
||||
msgstr "n = n mal, -1 = für immer"
|
||||
|
||||
#: models.py:303
|
||||
#: models.py:208
|
||||
msgid "Next Run"
|
||||
msgstr "Nächste Ausführung"
|
||||
|
||||
#: models.py:304
|
||||
msgid "When this schedule runs next (stored in UTC)."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:314
|
||||
#: models.py:215
|
||||
msgid "Cron expression"
|
||||
msgstr "Cron-Ausdruck"
|
||||
|
||||
#: models.py:320
|
||||
msgid "Last task id"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:321
|
||||
msgid "Id of the last task spawned from this schedule (read-only)."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:329 models.py:427
|
||||
msgid "Name of the target cluster"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:336
|
||||
msgid "Intended date kwarg"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:337
|
||||
#: models.py:224
|
||||
msgid "Name of kwarg to pass intended schedule date"
|
||||
msgstr "Name des zu passierenden Kwargs vorgesehenes Datum"
|
||||
|
||||
#: models.py:418
|
||||
#: models.py:299
|
||||
msgid "Scheduled task"
|
||||
msgstr "Geplante Aufgabe"
|
||||
|
||||
#: models.py:419
|
||||
#: models.py:300
|
||||
msgid "Scheduled tasks"
|
||||
msgstr "Geplante Aufgaben"
|
||||
|
||||
#: models.py:426
|
||||
#, fuzzy
|
||||
#| msgid "Clusters"
|
||||
msgid "Cluster key"
|
||||
msgstr "Cluster"
|
||||
|
||||
#: models.py:430
|
||||
msgid "Payload"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:431
|
||||
msgid "Signed serialized task package (do not edit manually)."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:435
|
||||
msgid "Lock until"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:436
|
||||
msgid "Prevent any cluster from pulling until"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:474
|
||||
#, fuzzy
|
||||
#| msgid "Queued task"
|
||||
msgid "Queue options"
|
||||
msgstr "Eingereihte Aufgabe"
|
||||
|
||||
#: models.py:478
|
||||
#: models.py:326
|
||||
msgid "Queued task"
|
||||
msgstr "Eingereihte Aufgabe"
|
||||
|
||||
#: models.py:479
|
||||
#: models.py:327
|
||||
msgid "Queued tasks"
|
||||
msgstr "Eingereihte Aufgaben"
|
||||
|
||||
#: monitor.py:41
|
||||
#, python-format
|
||||
msgid "%(name)s monitoring at %(id)s"
|
||||
msgstr "%(name)s beobachtet auf %(id)s"
|
||||
|
||||
#: monitor.py:61
|
||||
#, python-format
|
||||
msgid "Processed '%(info_name)s' (%(task_name)s)"
|
||||
msgstr "[%(task_name)s] - '%(info_name)s' wurde verarbeitet"
|
||||
|
||||
#: monitor.py:67
|
||||
#, python-format
|
||||
msgid "Failed '%(info_name)s' (%(task_name)s) - %(task_result)s"
|
||||
msgstr "'%(info_name)s' (%(task_name)s) ist fehlgeschlagen - %(task_result)s"
|
||||
|
||||
#: monitor.py:74
|
||||
#, python-format
|
||||
msgid "%(name)s stopped monitoring results"
|
||||
msgstr "%(name)s überwacht keine Ergebnisse mehr"
|
||||
|
||||
#: monitor_terminal.py:64 monitor_terminal.py:348
|
||||
#: monitor.py:64 monitor.py:348
|
||||
msgid "Host"
|
||||
msgstr "Host"
|
||||
|
||||
#: monitor_terminal.py:68 monitor_terminal.py:352 monitor_terminal.py:457
|
||||
#: monitor.py:68 monitor.py:352 monitor.py:459
|
||||
msgid "Id"
|
||||
msgstr "Id"
|
||||
|
||||
#: monitor_terminal.py:72
|
||||
#: monitor.py:72
|
||||
msgid "State"
|
||||
msgstr "Status"
|
||||
|
||||
#: monitor_terminal.py:76
|
||||
#: monitor.py:76
|
||||
msgid "Pool"
|
||||
msgstr "Pool"
|
||||
|
||||
#: monitor_terminal.py:80
|
||||
#: monitor.py:80
|
||||
msgid "TQ"
|
||||
msgstr "TQ"
|
||||
|
||||
#: monitor_terminal.py:84
|
||||
#: monitor.py:84
|
||||
msgid "RQ"
|
||||
msgstr "RQ"
|
||||
|
||||
#: monitor_terminal.py:88
|
||||
#: monitor.py:88
|
||||
msgid "RC"
|
||||
msgstr "RC"
|
||||
|
||||
#: monitor_terminal.py:92
|
||||
#: monitor.py:92
|
||||
msgid "Up"
|
||||
msgstr "Up"
|
||||
|
||||
#: monitor_terminal.py:172 monitor_terminal.py:286
|
||||
#: monitor.py:172 monitor.py:286
|
||||
msgid "Queued"
|
||||
msgstr "Eingereiht"
|
||||
|
||||
#: monitor_terminal.py:190 monitor_terminal.py:294
|
||||
#: monitor.py:180
|
||||
msgid "Success"
|
||||
msgstr "Erfolg"
|
||||
|
||||
#: monitor.py:190 monitor.py:294
|
||||
msgid "Failures"
|
||||
msgstr "Fehlschläge"
|
||||
|
||||
#: monitor_terminal.py:201 monitor_terminal.py:496
|
||||
#: monitor.py:201 monitor.py:498
|
||||
msgid "[Press q to quit]"
|
||||
msgstr "[Drücken Sie q zum Beenden]"
|
||||
|
||||
#: monitor_terminal.py:227
|
||||
#: monitor.py:227
|
||||
msgid "day"
|
||||
msgstr "Tag"
|
||||
|
||||
#: monitor_terminal.py:248
|
||||
#: monitor.py:248
|
||||
msgid "second"
|
||||
msgstr "Sekunde"
|
||||
|
||||
#: monitor_terminal.py:251
|
||||
#: monitor.py:251
|
||||
msgid "minute"
|
||||
msgstr "Minute"
|
||||
|
||||
#: monitor_terminal.py:254
|
||||
#: monitor.py:254
|
||||
msgid "hour"
|
||||
msgstr "Stunde"
|
||||
|
||||
#: monitor_terminal.py:263
|
||||
#: monitor.py:263
|
||||
#, python-format
|
||||
msgid "-- %(prefix)s %(version)s on %(info)s --"
|
||||
msgstr "-- %(prefix)s %(version)s auf %(info)s --"
|
||||
|
||||
#: monitor_terminal.py:273
|
||||
#: monitor.py:273
|
||||
msgid "Clusters"
|
||||
msgstr "Cluster"
|
||||
|
||||
#: monitor_terminal.py:277
|
||||
#: monitor.py:277
|
||||
msgid "Workers"
|
||||
msgstr "Arbeiter"
|
||||
|
||||
#: monitor_terminal.py:281
|
||||
#: monitor.py:281
|
||||
msgid "Restarts"
|
||||
msgstr "Neustarts"
|
||||
|
||||
#: monitor_terminal.py:290
|
||||
#: monitor.py:290
|
||||
msgid "Successes"
|
||||
msgstr "Erfolge"
|
||||
|
||||
#: monitor_terminal.py:299
|
||||
#: monitor.py:299
|
||||
msgid "Schedules"
|
||||
msgstr "Zeitpläne"
|
||||
|
||||
#: monitor_terminal.py:303
|
||||
#: monitor.py:303
|
||||
#, python-format
|
||||
msgid "Tasks/%(per)s"
|
||||
msgstr "Aufgaben/%(per)s"
|
||||
|
||||
#: monitor_terminal.py:307
|
||||
#: monitor.py:307
|
||||
msgid "Avg time"
|
||||
msgstr "Durchschnittl. Zeit"
|
||||
|
||||
#: monitor_terminal.py:357
|
||||
#: monitor.py:357
|
||||
msgid "Available (%)"
|
||||
msgstr "Verfügbar (%)"
|
||||
|
||||
#: monitor_terminal.py:363
|
||||
#: monitor.py:363
|
||||
msgid "Available (MB)"
|
||||
msgstr "Verfügbar (MB)"
|
||||
|
||||
#: monitor_terminal.py:368
|
||||
#: monitor.py:368
|
||||
msgid "Total (MB)"
|
||||
msgstr "Insgesamt (MB)"
|
||||
|
||||
#: monitor_terminal.py:373
|
||||
#: monitor.py:373
|
||||
msgid "Sentinel (MB)"
|
||||
msgstr "Sentinel (MB)"
|
||||
|
||||
#: monitor_terminal.py:379
|
||||
#: monitor.py:379
|
||||
msgid "Monitor (MB)"
|
||||
msgstr "Monitor (MB)"
|
||||
|
||||
#: monitor_terminal.py:385
|
||||
#: monitor.py:385
|
||||
msgid "Workers (MB)"
|
||||
msgstr "Arbeiter (MB)"
|
||||
|
||||
#: monitor_terminal.py:485
|
||||
#: monitor.py:487
|
||||
#, python-format
|
||||
msgid "Available lowest (): %(memory_percent)s ((at)s)"
|
||||
msgstr "Niedrigste verfügbar (): %(memory_percent)s ((at)s)"
|
||||
|
||||
#: monitor_terminal.py:507
|
||||
#: monitor.py:509
|
||||
msgid "No clusters appear to be running."
|
||||
msgstr "Es scheinen keine Cluster zu laufen."
|
||||
|
||||
#: pusher.py:40
|
||||
#, python-format
|
||||
#| msgid "%(process_name)s pushing tasks at %(id)s"
|
||||
msgid "%(name)s pushing tasks at %(id)s"
|
||||
msgstr "%(name)s veröffentlicht Aufagaben auf %(id)s"
|
||||
|
||||
#: pusher.py:67
|
||||
#, python-format
|
||||
msgid "queueing from %(list_key)s"
|
||||
msgstr "Einreihen von %(list_key)s"
|
||||
|
||||
#: pusher.py:71
|
||||
#, python-format
|
||||
msgid "%(name)s stopped pushing tasks"
|
||||
msgstr "%(name)s veröffentlicht keine Aufgaben mehr"
|
||||
|
||||
#: scheduler.py:110
|
||||
#, python-format
|
||||
msgid "%(process_name)s failed to create a task from schedule [%(schedule)s]"
|
||||
msgstr ""
|
||||
"%(process_name)s konnte keine Aufgabe von Zeitplan [%(schedule)s] erstellen"
|
||||
|
||||
#: scheduler.py:121
|
||||
#, fuzzy, python-format
|
||||
#| msgid "%(process_name)s created a task from schedule [%(schedule)s]"
|
||||
msgid ""
|
||||
"%(process_name)s created task %(task_name)s from schedule [%(schedule)s]"
|
||||
msgstr ""
|
||||
"%(process_name)s hat eine Aufgabe des Zeitplans [%(schedule)s] erstellt"
|
||||
|
||||
#: signals.py:22
|
||||
#, python-format
|
||||
msgid "malformed return hook '%(hook)s' for [%(name)s]"
|
||||
@@ -661,26 +493,6 @@ msgstr "Ungültiger Return-Hook '%(hook)s' für [%(name)s]"
|
||||
msgid "return hook %(hook)s failed on [%(name)s] because %(error)s"
|
||||
msgstr "Return-Hook %(hook)s für [%(name)s] ist gescheitert: %(error)s"
|
||||
|
||||
#: timeout.py:30 timeout.py:43
|
||||
msgid "SIGALARM is not available on your platform"
|
||||
msgstr ""
|
||||
|
||||
#: worker.py:49
|
||||
#, python-format
|
||||
msgid "%(proc_name)s ready for work at %(id)s"
|
||||
msgstr "%(proc_name)s ist bereit für Arbeit auf %(id)s"
|
||||
|
||||
#: worker.py:69
|
||||
#, fuzzy, python-format
|
||||
#| msgid "%(proc_name)s processing '%(func_name)s' (%(task_name)s)"
|
||||
msgid "%(proc_name)s processing %(task_name)s '%(func_name)s'"
|
||||
msgstr "%(proc_name)s verarbeitet '%(func_name)s' (%(task_name)s)"
|
||||
|
||||
#: worker.py:135
|
||||
#, python-format
|
||||
msgid "%(proc_name)s stopped doing work"
|
||||
msgstr "%(proc_name)s hat die Arbeit beendet"
|
||||
|
||||
#, python-format
|
||||
#~ msgid ""
|
||||
#~ "Could not process '%(func_name)s'. Check the location of the function and "
|
||||
|
||||
@@ -6,7 +6,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-04-17 21:10+0000\n"
|
||||
"POT-Creation-Date: 2023-01-26 01:38+0000\n"
|
||||
"PO-Revision-Date: 2018-08-05 18:28+0200\n"
|
||||
"Last-Translator: Thierry BOULOGNE <contact@tng-concepts.com>\n"
|
||||
"Language-Team: \n"
|
||||
@@ -17,44 +17,36 @@ msgstr ""
|
||||
"X-Generator: Poedit 2.1.1\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
|
||||
#: admin.py:30
|
||||
#: admin.py:43
|
||||
msgid "Resubmit selected tasks to queue"
|
||||
msgstr "Resoumettre les tâches sélectionnées à la file d'attente"
|
||||
|
||||
#: admin.py:64
|
||||
msgid "Time Taken"
|
||||
msgstr ""
|
||||
|
||||
#: admin.py:95
|
||||
msgid "Short result"
|
||||
msgstr ""
|
||||
|
||||
#: admin.py:142 models.py:412
|
||||
#: admin.py:107 models.py:293
|
||||
#, fuzzy
|
||||
#| msgid "Success"
|
||||
msgid "success"
|
||||
msgstr "succès"
|
||||
|
||||
#: admin.py:154 models.py:414
|
||||
#: admin.py:119 models.py:295
|
||||
msgid "last_run"
|
||||
msgstr ""
|
||||
|
||||
#: cluster.py:75
|
||||
#: cluster.py:79
|
||||
#, python-format
|
||||
msgid "Q Cluster %(name)s starting."
|
||||
msgstr "Démarrage de Q Cluster-%(name)s."
|
||||
|
||||
#: cluster.py:83
|
||||
#: cluster.py:87
|
||||
#, python-format
|
||||
msgid "Q Cluster %(name)s stopping."
|
||||
msgstr "Arrêt de Q Cluster-%(name)s."
|
||||
|
||||
#: cluster.py:86
|
||||
#: cluster.py:90
|
||||
#, python-format
|
||||
msgid "Q Cluster %(name)s has stopped."
|
||||
msgstr "Q Cluster-%(name)s a été arrêté."
|
||||
|
||||
#: cluster.py:93
|
||||
#: cluster.py:97
|
||||
#, python-format
|
||||
msgid "%(name)s got signal %(signal)s"
|
||||
msgstr "%(name)s à reçu le signal %(signal)s"
|
||||
@@ -69,7 +61,7 @@ msgstr "surveillant %(name)s réincarné après un arrêt intempestif"
|
||||
msgid "reincarnated pusher %(name)s after sudden death"
|
||||
msgstr "répartiteur %(name)s réincarné après un arrêt intempestif"
|
||||
|
||||
#: cluster.py:262
|
||||
#: cluster.py:250
|
||||
#, python-format
|
||||
msgid ""
|
||||
"reincarnated worker %(name)s after timeout while processing task "
|
||||
@@ -78,57 +70,122 @@ msgstr ""
|
||||
"processus %(name)s réincarné, délai de traitement dépassé pour la tâche "
|
||||
"%(task_name)s"
|
||||
|
||||
#: cluster.py:265
|
||||
#: cluster.py:255
|
||||
#, python-format
|
||||
msgid "reincarnated worker %(name)s after timeout"
|
||||
msgstr "processus %(name)s réincarné, délai de traitement dépassé"
|
||||
|
||||
#: cluster.py:270
|
||||
#: cluster.py:260
|
||||
#, python-format
|
||||
msgid "recycled worker %(name)s"
|
||||
msgstr "processus recyclé %(name)s"
|
||||
|
||||
#: cluster.py:273
|
||||
#: cluster.py:263
|
||||
#, python-format
|
||||
msgid "reincarnated worker %(name)s after death"
|
||||
msgstr "processus réintégré %(name)s après arrêt"
|
||||
|
||||
#: cluster.py:297
|
||||
#: cluster.py:287
|
||||
#, python-format
|
||||
msgid "%(name)s guarding cluster %(cluster_name)s"
|
||||
msgstr "%(name)s surveillance du cluster à %(cluster_name)s"
|
||||
|
||||
#: cluster.py:307
|
||||
#: cluster.py:296
|
||||
#, python-format
|
||||
msgid "Q Cluster %(cluster_name)s running."
|
||||
msgstr "Démarrage de Q Cluster-%(cluster_name)s."
|
||||
|
||||
#: cluster.py:346
|
||||
#: cluster.py:332
|
||||
#, python-format
|
||||
msgid "%(name)s stopping cluster processes"
|
||||
msgstr "%(name)s arrêt des processus du cluster"
|
||||
|
||||
#: cluster.py:371
|
||||
#: cluster.py:357
|
||||
#, python-format
|
||||
msgid "%(name)s waiting for the monitor."
|
||||
msgstr "%(name)s en attente du surveillant."
|
||||
|
||||
#: cluster.py:394
|
||||
#: cluster.py:383
|
||||
#, python-format
|
||||
msgid "%(name)s pushing tasks at %(id)s"
|
||||
msgstr "%(name)s répartit les tâches %(id)s"
|
||||
|
||||
#: cluster.py:407
|
||||
#, python-format
|
||||
msgid "queueing from %(list_key)s"
|
||||
msgstr "mise en file d'attente de %(list_key)s"
|
||||
|
||||
#: cluster.py:411
|
||||
#, python-format
|
||||
msgid "%(name)s stopped pushing tasks"
|
||||
msgstr "%(name)s a cessé de répartir les tâches"
|
||||
|
||||
#: cluster.py:426
|
||||
#, python-format
|
||||
msgid "%(name)s monitoring at %(id)s"
|
||||
msgstr "%(name)s surveille les résultats %(id)s"
|
||||
|
||||
#: cluster.py:445
|
||||
#, python-format
|
||||
msgid "Processed '%(info_name)s' (%(task_name)s)"
|
||||
msgstr "traité '%(info_name)s' (%(task_name)s)"
|
||||
|
||||
#: cluster.py:451
|
||||
#, python-format
|
||||
msgid "Failed '%(info_name)s' (%(task_name)s) - %(task_result)s"
|
||||
msgstr "Manqué '%(info_name)s' (%(task_name)s) - %(task_result)s"
|
||||
|
||||
#: cluster.py:458
|
||||
#, python-format
|
||||
msgid "%(name)s stopped monitoring results"
|
||||
msgstr "%(name)s a cessé de de surveiller les résultats"
|
||||
|
||||
#: cluster.py:474
|
||||
#, python-format
|
||||
msgid "%(proc_name)s ready for work at %(id)s"
|
||||
msgstr "%(proc_name)s prêt pour le travail à %(id)s"
|
||||
|
||||
#: cluster.py:494
|
||||
#, fuzzy, python-format
|
||||
msgid "%(proc_name)s processing %(task_name)s '%(func_name)s'"
|
||||
msgstr "%(proc_name)s exécute %(task_name)s '%(func_name)s'"
|
||||
|
||||
#: cluster.py:546
|
||||
#, python-format
|
||||
msgid "%(proc_name)s stopped doing work"
|
||||
msgstr "%(proc_name)s a cessé de travailler"
|
||||
|
||||
#: cluster.py:751
|
||||
#, python-format
|
||||
msgid "%(process_name)s failed to create a task from schedule [%(schedule)s]"
|
||||
msgstr ""
|
||||
"%(process_name)s Echec de la création d'une tâche à partir de Schedule "
|
||||
"[%(schedule)s]"
|
||||
|
||||
#: cluster.py:762
|
||||
#, python-format
|
||||
msgid ""
|
||||
"%(process_name)s created task %(task_name)s from schedule [%(schedule)s]"
|
||||
msgstr ""
|
||||
"%(process_name)s a créé la tâche %(task_name)s à partir de Schedule "
|
||||
"[%(schedule)s]"
|
||||
|
||||
#: cluster.py:808
|
||||
msgid "Skipping cpu affinity because psutil was not found."
|
||||
msgstr "L'affinité cpu ne sera pas définie car psutil n'a pas été trouvé."
|
||||
|
||||
#: cluster.py:399
|
||||
#: cluster.py:813
|
||||
msgid "Faking cpu affinity because it is not supported on this platform"
|
||||
msgstr ""
|
||||
"Simulation de l'affinité cpu parce qu'elle n'est pas supportée sur cette "
|
||||
"plateforme."
|
||||
|
||||
#: cluster.py:421
|
||||
#: cluster.py:835
|
||||
#, python-format
|
||||
msgid "%(pid)s will use cpu %(affinity)s"
|
||||
msgstr "%(pid)s utilisera le CPU %(affinity)s"
|
||||
|
||||
#: conf.py:122
|
||||
#: conf.py:90
|
||||
#, python-format
|
||||
msgid ""
|
||||
"SAVE_LIMIT_PER (%(option)s) is not a valid option. Options are: 'group', "
|
||||
@@ -138,28 +195,28 @@ msgstr ""
|
||||
"'group', 'name', 'func' et None. La valeur par défaut est None."
|
||||
|
||||
#. Translators: Cluster status descriptions
|
||||
#: conf.py:239
|
||||
#: conf.py:207
|
||||
msgid "Starting"
|
||||
msgstr "Démarrage"
|
||||
|
||||
#: conf.py:240
|
||||
#: conf.py:208
|
||||
msgid "Working"
|
||||
msgstr "Actif"
|
||||
|
||||
#: conf.py:241
|
||||
#: conf.py:209
|
||||
msgid "Idle"
|
||||
msgstr "En attente"
|
||||
|
||||
#: conf.py:242
|
||||
#: conf.py:210
|
||||
msgid "Stopped"
|
||||
msgstr "Arrêté"
|
||||
|
||||
#: conf.py:243
|
||||
#: conf.py:211
|
||||
msgid "Stopping"
|
||||
msgstr "En cours d’arrêt"
|
||||
|
||||
#. Translators: help text for qcluster management command
|
||||
#: management/commands/qcluster.py:11
|
||||
#: management/commands/qcluster.py:9
|
||||
msgid "Starts a Django Q Cluster."
|
||||
msgstr "Démarre un cluster Django Q."
|
||||
|
||||
@@ -180,485 +237,259 @@ msgstr "Surveille l'utilisation mémoire du Q cluster"
|
||||
msgid "Monitors Q Cluster activity"
|
||||
msgstr "Surveille l'activité de Q cluster"
|
||||
|
||||
#: models.py:30 models.py:469
|
||||
msgid "Task id"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:31
|
||||
msgid "32-character task identifier."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:36 models.py:235 models.py:470
|
||||
msgid "Name"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:37
|
||||
msgid "Optional human-readable name for lookup and display."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:41 models.py:240 models.py:468
|
||||
msgid "Function"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:42
|
||||
msgid "Dotted import path to the callable, e.g. myapp.tasks.job."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:47 models.py:247
|
||||
msgid "Hook"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:48
|
||||
msgid "Optional dotted path to a callback that receives the task result."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:53 models.py:253 models.py:472
|
||||
msgid "Arguments"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:54
|
||||
msgid "Pickled positional arguments (tuple)."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:59 models.py:259 models.py:473
|
||||
msgid "Keyword arguments"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:60
|
||||
msgid "Pickled keyword arguments (dict)."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:65
|
||||
msgid "Result"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:66
|
||||
msgid "Pickled return value or error payload."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:72 models.py:471
|
||||
msgid "Group"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:73
|
||||
msgid "Optional group id to aggregate related tasks."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:80 models.py:328
|
||||
#, fuzzy
|
||||
#| msgid "Clusters"
|
||||
msgid "Cluster"
|
||||
msgstr "Grappes"
|
||||
|
||||
#: models.py:81
|
||||
msgid "Name of the cluster that executed this task."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:85
|
||||
msgid "Started at"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:86
|
||||
msgid "When the worker started executing the task."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:90
|
||||
#, fuzzy
|
||||
#| msgid "Stopped"
|
||||
msgid "Stopped at"
|
||||
msgstr "Arrêté"
|
||||
|
||||
#: models.py:91
|
||||
msgid "When the worker finished (success or failure)."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:96 monitor_terminal.py:180
|
||||
msgid "Success"
|
||||
msgstr "Succès"
|
||||
|
||||
#: models.py:97
|
||||
msgid "False if the task raised an exception or timed out."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:101
|
||||
msgid "Attempt count"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:102
|
||||
msgid "How many times execution was attempted."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:172
|
||||
msgid "Task"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:173
|
||||
msgid "Tasks"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:194
|
||||
#: models.py:125
|
||||
msgid "Successful task"
|
||||
msgstr "Tâche réussie"
|
||||
|
||||
#: models.py:195
|
||||
#: models.py:126
|
||||
msgid "Successful tasks"
|
||||
msgstr "Tâches réussies"
|
||||
|
||||
#: models.py:210
|
||||
#: models.py:141
|
||||
msgid "Failed task"
|
||||
msgstr "Tâche échoué"
|
||||
|
||||
#: models.py:211
|
||||
#: models.py:142
|
||||
msgid "Failed tasks"
|
||||
msgstr "Tâches échouées"
|
||||
|
||||
#: models.py:219 models.py:347
|
||||
#: models.py:150 models.py:234
|
||||
msgid "Please install croniter to enable cron expressions"
|
||||
msgstr "Veuillez installer croniter pour activer les expressions cron."
|
||||
|
||||
#: models.py:236
|
||||
msgid "Optional label to identify this schedule in the admin."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:241
|
||||
msgid "e.g. module.tasks.function"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:248
|
||||
msgid "e.g. module.tasks.result_function"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:254
|
||||
#: models.py:170
|
||||
msgid "e.g. 1, 2, 'John'"
|
||||
msgstr "ex. 1, 2, ‘Jean’"
|
||||
|
||||
#: models.py:260
|
||||
#: models.py:172
|
||||
msgid "e.g. x=1, y=2, name='John'"
|
||||
msgstr "p. ex. x = 1, y = 2, Nom = ‘Jean’"
|
||||
|
||||
#: models.py:274
|
||||
#: models.py:186
|
||||
msgid "Once"
|
||||
msgstr "Une fois"
|
||||
|
||||
#: models.py:275 models.py:296
|
||||
#: models.py:187
|
||||
msgid "Minutes"
|
||||
msgstr "Minutes"
|
||||
|
||||
#: models.py:276
|
||||
#: models.py:188
|
||||
msgid "Hourly"
|
||||
msgstr "Toutes les heures"
|
||||
|
||||
#: models.py:277
|
||||
#: models.py:189
|
||||
msgid "Daily"
|
||||
msgstr "Quotidien"
|
||||
|
||||
#: models.py:278
|
||||
#: models.py:190
|
||||
msgid "Weekly"
|
||||
msgstr "Hebdomadaire"
|
||||
|
||||
#: models.py:279
|
||||
#: models.py:191
|
||||
#, fuzzy
|
||||
#| msgid "Weekly"
|
||||
msgid "Biweekly"
|
||||
msgstr "Bihebdomadaire"
|
||||
|
||||
#: models.py:280
|
||||
#: models.py:192
|
||||
msgid "Monthly"
|
||||
msgstr "Mensuel"
|
||||
|
||||
#: models.py:281
|
||||
#: models.py:193
|
||||
#, fuzzy
|
||||
#| msgid "Monthly"
|
||||
msgid "Bimonthly"
|
||||
msgstr "Bimestriel"
|
||||
|
||||
#: models.py:282
|
||||
#: models.py:194
|
||||
msgid "Quarterly"
|
||||
msgstr "Trimestriel"
|
||||
msgstr "Tous les quart-d’heure"
|
||||
|
||||
#: models.py:283
|
||||
#: models.py:195
|
||||
msgid "Yearly"
|
||||
msgstr "Annuel"
|
||||
|
||||
#: models.py:284 models.py:313
|
||||
#: models.py:196
|
||||
msgid "Cron"
|
||||
msgstr "Cron"
|
||||
|
||||
#: models.py:290
|
||||
#: models.py:199
|
||||
msgid "Schedule Type"
|
||||
msgstr "Type de plannification"
|
||||
|
||||
#: models.py:291
|
||||
msgid "How often this task should be enqueued."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:297
|
||||
#: models.py:202
|
||||
msgid "Number of minutes for the Minutes type"
|
||||
msgstr "Nombre de minutes pour le type de minutes"
|
||||
|
||||
#: models.py:300
|
||||
#: models.py:205
|
||||
msgid "Repeats"
|
||||
msgstr "Répéter"
|
||||
|
||||
#: models.py:300
|
||||
#: models.py:205
|
||||
msgid "n = n times, -1 = forever"
|
||||
msgstr "n = n fois,-1 = Toujours"
|
||||
|
||||
#: models.py:303
|
||||
#: models.py:208
|
||||
msgid "Next Run"
|
||||
msgstr "Prochaine exécution"
|
||||
|
||||
#: models.py:304
|
||||
msgid "When this schedule runs next (stored in UTC)."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:314
|
||||
#: models.py:215
|
||||
msgid "Cron expression"
|
||||
msgstr "Expression Cron"
|
||||
|
||||
#: models.py:320
|
||||
msgid "Last task id"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:321
|
||||
msgid "Id of the last task spawned from this schedule (read-only)."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:329 models.py:427
|
||||
msgid "Name of the target cluster"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:336
|
||||
msgid "Intended date kwarg"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:337
|
||||
#: models.py:224
|
||||
msgid "Name of kwarg to pass intended schedule date"
|
||||
msgstr "Nom du kwarg à passer Date prévue de l'horaire"
|
||||
|
||||
#: models.py:418
|
||||
#: models.py:299
|
||||
msgid "Scheduled task"
|
||||
msgstr "Tâche planifiée"
|
||||
|
||||
#: models.py:419
|
||||
#: models.py:300
|
||||
msgid "Scheduled tasks"
|
||||
msgstr "Tâches planifiées"
|
||||
|
||||
#: models.py:426
|
||||
#, fuzzy
|
||||
#| msgid "Clusters"
|
||||
msgid "Cluster key"
|
||||
msgstr "Grappes"
|
||||
|
||||
#: models.py:430
|
||||
msgid "Payload"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:431
|
||||
msgid "Signed serialized task package (do not edit manually)."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:435
|
||||
msgid "Lock until"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:436
|
||||
msgid "Prevent any cluster from pulling until"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:474
|
||||
#, fuzzy
|
||||
#| msgid "Queued task"
|
||||
msgid "Queue options"
|
||||
msgstr "Tâche en file d'attente"
|
||||
|
||||
#: models.py:478
|
||||
#: models.py:326
|
||||
msgid "Queued task"
|
||||
msgstr "Tâche en file d'attente"
|
||||
|
||||
#: models.py:479
|
||||
#: models.py:327
|
||||
msgid "Queued tasks"
|
||||
msgstr "Tâches en file d'attente"
|
||||
|
||||
#: monitor.py:41
|
||||
#, python-format
|
||||
msgid "%(name)s monitoring at %(id)s"
|
||||
msgstr "%(name)s surveille les résultats %(id)s"
|
||||
|
||||
#: monitor.py:61
|
||||
#, python-format
|
||||
msgid "Processed '%(info_name)s' (%(task_name)s)"
|
||||
msgstr "traité '%(info_name)s' (%(task_name)s)"
|
||||
|
||||
#: monitor.py:67
|
||||
#, python-format
|
||||
msgid "Failed '%(info_name)s' (%(task_name)s) - %(task_result)s"
|
||||
msgstr "Manqué '%(info_name)s' (%(task_name)s) - %(task_result)s"
|
||||
|
||||
#: monitor.py:74
|
||||
#, python-format
|
||||
msgid "%(name)s stopped monitoring results"
|
||||
msgstr "%(name)s a cessé de de surveiller les résultats"
|
||||
|
||||
#: monitor_terminal.py:64 monitor_terminal.py:348
|
||||
#: monitor.py:64 monitor.py:348
|
||||
msgid "Host"
|
||||
msgstr "Hôte"
|
||||
|
||||
#: monitor_terminal.py:68 monitor_terminal.py:352 monitor_terminal.py:457
|
||||
#: monitor.py:68 monitor.py:352 monitor.py:459
|
||||
msgid "Id"
|
||||
msgstr "Id"
|
||||
|
||||
#: monitor_terminal.py:72
|
||||
#: monitor.py:72
|
||||
msgid "State"
|
||||
msgstr "Statut"
|
||||
|
||||
#: monitor_terminal.py:76
|
||||
#: monitor.py:76
|
||||
msgid "Pool"
|
||||
msgstr "Piscine"
|
||||
|
||||
#: monitor_terminal.py:80
|
||||
#: monitor.py:80
|
||||
msgid "TQ"
|
||||
msgstr "TQ"
|
||||
|
||||
#: monitor_terminal.py:84
|
||||
#: monitor.py:84
|
||||
msgid "RQ"
|
||||
msgstr "RQ"
|
||||
|
||||
#: monitor_terminal.py:88
|
||||
#: monitor.py:88
|
||||
msgid "RC"
|
||||
msgstr "RC"
|
||||
|
||||
#: monitor_terminal.py:92
|
||||
#: monitor.py:92
|
||||
msgid "Up"
|
||||
msgstr "Haut"
|
||||
|
||||
#: monitor_terminal.py:172 monitor_terminal.py:286
|
||||
#: monitor.py:172 monitor.py:286
|
||||
msgid "Queued"
|
||||
msgstr "En file d'attente"
|
||||
|
||||
#: monitor_terminal.py:190 monitor_terminal.py:294
|
||||
#: monitor.py:180
|
||||
msgid "Success"
|
||||
msgstr "Succès"
|
||||
|
||||
#: monitor.py:190 monitor.py:294
|
||||
msgid "Failures"
|
||||
msgstr "Défaillances"
|
||||
|
||||
#: monitor_terminal.py:201 monitor_terminal.py:496
|
||||
#: monitor.py:201 monitor.py:498
|
||||
msgid "[Press q to quit]"
|
||||
msgstr "[appuyez sur q pour quitter]"
|
||||
|
||||
#: monitor_terminal.py:227
|
||||
#: monitor.py:227
|
||||
msgid "day"
|
||||
msgstr "jour"
|
||||
|
||||
#: monitor_terminal.py:248
|
||||
#: monitor.py:248
|
||||
msgid "second"
|
||||
msgstr "seconde"
|
||||
|
||||
#: monitor_terminal.py:251
|
||||
#: monitor.py:251
|
||||
msgid "minute"
|
||||
msgstr "minute"
|
||||
|
||||
#: monitor_terminal.py:254
|
||||
#: monitor.py:254
|
||||
msgid "hour"
|
||||
msgstr "heure"
|
||||
|
||||
#: monitor_terminal.py:263
|
||||
#: monitor.py:263
|
||||
#, python-format
|
||||
msgid "-- %(prefix)s %(version)s on %(info)s --"
|
||||
msgstr "--%(prefix)s %(version)s sur %(info)s --"
|
||||
|
||||
#: monitor_terminal.py:273
|
||||
#: monitor.py:273
|
||||
msgid "Clusters"
|
||||
msgstr "Grappes"
|
||||
|
||||
#: monitor_terminal.py:277
|
||||
#: monitor.py:277
|
||||
msgid "Workers"
|
||||
msgstr "Processus"
|
||||
|
||||
#: monitor_terminal.py:281
|
||||
#: monitor.py:281
|
||||
msgid "Restarts"
|
||||
msgstr "Redémarrages"
|
||||
|
||||
#: monitor_terminal.py:290
|
||||
#: monitor.py:290
|
||||
msgid "Successes"
|
||||
msgstr "Succès"
|
||||
|
||||
#: monitor_terminal.py:299
|
||||
#: monitor.py:299
|
||||
msgid "Schedules"
|
||||
msgstr "Planifications"
|
||||
|
||||
#: monitor_terminal.py:303
|
||||
#: monitor.py:303
|
||||
#, python-format
|
||||
msgid "Tasks/%(per)s"
|
||||
msgstr "Tâches/%(per)s"
|
||||
|
||||
#: monitor_terminal.py:307
|
||||
#: monitor.py:307
|
||||
msgid "Avg time"
|
||||
msgstr "Temps Moyen"
|
||||
|
||||
#: monitor_terminal.py:357
|
||||
#: monitor.py:357
|
||||
msgid "Available (%)"
|
||||
msgstr ""
|
||||
|
||||
#: monitor_terminal.py:363
|
||||
#: monitor.py:363
|
||||
msgid "Available (MB)"
|
||||
msgstr "Disponible sur (MB)"
|
||||
|
||||
#: monitor_terminal.py:368
|
||||
#: monitor.py:368
|
||||
msgid "Total (MB)"
|
||||
msgstr "Total (MB)"
|
||||
|
||||
#: monitor_terminal.py:373
|
||||
#: monitor.py:373
|
||||
msgid "Sentinel (MB)"
|
||||
msgstr "Sentinel (MB)"
|
||||
|
||||
#: monitor_terminal.py:379
|
||||
#: monitor.py:379
|
||||
msgid "Monitor (MB)"
|
||||
msgstr "Monitor (MB)"
|
||||
|
||||
#: monitor_terminal.py:385
|
||||
#: monitor.py:385
|
||||
#, fuzzy
|
||||
#| msgid "Workers"
|
||||
msgid "Workers (MB)"
|
||||
msgstr "Processus (MB)"
|
||||
|
||||
#: monitor_terminal.py:485
|
||||
#: monitor.py:487
|
||||
#, python-format
|
||||
msgid "Available lowest (): %(memory_percent)s ((at)s)"
|
||||
msgstr "Disponible le plus bas () : %(memory_percent)s ((at)s)"
|
||||
|
||||
#: monitor_terminal.py:507
|
||||
#: monitor.py:509
|
||||
msgid "No clusters appear to be running."
|
||||
msgstr "Aucun cluster ne semble être en cours d'exécution."
|
||||
|
||||
#: pusher.py:40
|
||||
#, python-format
|
||||
msgid "%(name)s pushing tasks at %(id)s"
|
||||
msgstr "%(name)s répartit les tâches %(id)s"
|
||||
|
||||
#: pusher.py:67
|
||||
#, python-format
|
||||
msgid "queueing from %(list_key)s"
|
||||
msgstr "mise en file d'attente de %(list_key)s"
|
||||
|
||||
#: pusher.py:71
|
||||
#, python-format
|
||||
msgid "%(name)s stopped pushing tasks"
|
||||
msgstr "%(name)s a cessé de répartir les tâches"
|
||||
|
||||
#: scheduler.py:110
|
||||
#, python-format
|
||||
msgid "%(process_name)s failed to create a task from schedule [%(schedule)s]"
|
||||
msgstr ""
|
||||
"%(process_name)s Echec de la création d'une tâche à partir de Schedule "
|
||||
"[%(schedule)s]"
|
||||
|
||||
#: scheduler.py:121
|
||||
#, python-format
|
||||
msgid ""
|
||||
"%(process_name)s created task %(task_name)s from schedule [%(schedule)s]"
|
||||
msgstr ""
|
||||
"%(process_name)s a créé la tâche %(task_name)s à partir de Schedule "
|
||||
"[%(schedule)s]"
|
||||
|
||||
#: signals.py:22
|
||||
#, python-format
|
||||
msgid "malformed return hook '%(hook)s' for [%(name)s]"
|
||||
@@ -669,25 +500,6 @@ msgstr "hook de retour '%(hook)s' mal formé pour [%(name)s]"
|
||||
msgid "return hook %(hook)s failed on [%(name)s] because %(error)s"
|
||||
msgstr "hook de retour %(hook)s a échoué sur [%(name)s] à cause de %(error)s"
|
||||
|
||||
#: timeout.py:30 timeout.py:43
|
||||
msgid "SIGALARM is not available on your platform"
|
||||
msgstr ""
|
||||
|
||||
#: worker.py:49
|
||||
#, python-format
|
||||
msgid "%(proc_name)s ready for work at %(id)s"
|
||||
msgstr "%(proc_name)s prêt pour le travail à %(id)s"
|
||||
|
||||
#: worker.py:69
|
||||
#, fuzzy, python-format
|
||||
msgid "%(proc_name)s processing %(task_name)s '%(func_name)s'"
|
||||
msgstr "%(proc_name)s exécute %(task_name)s '%(func_name)s'"
|
||||
|
||||
#: worker.py:135
|
||||
#, python-format
|
||||
msgid "%(proc_name)s stopped doing work"
|
||||
msgstr "%(proc_name)s a cessé de travailler"
|
||||
|
||||
#, python-format
|
||||
#~ msgid ""
|
||||
#~ "Could not process '%(func_name)s'. Check the location of the function and "
|
||||
|
||||
@@ -1,676 +0,0 @@
|
||||
# Russian translation for django-q.
|
||||
# Copyright (C) Django Q authors
|
||||
# This file is distributed under the same license as the django-q package.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: django-q\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-04-17 21:07+0000\n"
|
||||
"PO-Revision-Date: 2026-04-04 12:00+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Russian\n"
|
||||
"Language: ru\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
|
||||
"n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || "
|
||||
"(n%100>=11 && n%100<=14)? 2 : 3);\n"
|
||||
|
||||
#: django_q/admin.py:30
|
||||
msgid "Resubmit selected tasks to queue"
|
||||
msgstr "Повторно отправить выбранные задачи в очередь"
|
||||
|
||||
#: django_q/admin.py:64
|
||||
msgid "Time Taken"
|
||||
msgstr "Время выполнения"
|
||||
|
||||
#: django_q/admin.py:95
|
||||
msgid "Short result"
|
||||
msgstr "Краткий результат"
|
||||
|
||||
#: django_q/admin.py:142 django_q/models.py:412
|
||||
msgid "success"
|
||||
msgstr "успех"
|
||||
|
||||
#: django_q/admin.py:154 django_q/models.py:414
|
||||
msgid "last_run"
|
||||
msgstr "последний запуск"
|
||||
|
||||
#: django_q/cluster.py:75
|
||||
#, python-format
|
||||
msgid "Q Cluster %(name)s starting."
|
||||
msgstr "Запуск кластера Q %(name)s."
|
||||
|
||||
#: django_q/cluster.py:83
|
||||
#, python-format
|
||||
msgid "Q Cluster %(name)s stopping."
|
||||
msgstr "Остановка кластера Q %(name)s."
|
||||
|
||||
#: django_q/cluster.py:86
|
||||
#, python-format
|
||||
msgid "Q Cluster %(name)s has stopped."
|
||||
msgstr "Кластер Q %(name)s остановлен."
|
||||
|
||||
#: django_q/cluster.py:93
|
||||
#, python-format
|
||||
msgid "%(name)s got signal %(signal)s"
|
||||
msgstr "%(name)s получил сигнал %(signal)s"
|
||||
|
||||
#: django_q/cluster.py:224
|
||||
#, python-format
|
||||
msgid "reincarnated monitor %(name)s after sudden death"
|
||||
msgstr "монитор %(name)s перезапущен после аварийного завершения"
|
||||
|
||||
#: django_q/cluster.py:230
|
||||
#, python-format
|
||||
msgid "reincarnated pusher %(name)s after sudden death"
|
||||
msgstr "пушер %(name)s перезапущен после аварийного завершения"
|
||||
|
||||
#: django_q/cluster.py:262
|
||||
#, python-format
|
||||
msgid ""
|
||||
"reincarnated worker %(name)s after timeout while processing task "
|
||||
"%(task_name)s"
|
||||
msgstr ""
|
||||
"воркер %(name)s перезапущен по таймауту при выполнении задачи %(task_name)s"
|
||||
|
||||
#: django_q/cluster.py:265
|
||||
#, python-format
|
||||
msgid "reincarnated worker %(name)s after timeout"
|
||||
msgstr "воркер %(name)s перезапущен по таймауту"
|
||||
|
||||
#: django_q/cluster.py:270
|
||||
#, python-format
|
||||
msgid "recycled worker %(name)s"
|
||||
msgstr "воркер %(name)s перезапущен (recycle)"
|
||||
|
||||
#: django_q/cluster.py:273
|
||||
#, python-format
|
||||
msgid "reincarnated worker %(name)s after death"
|
||||
msgstr "воркер %(name)s перезапущен после сбоя"
|
||||
|
||||
#: django_q/cluster.py:297
|
||||
#, python-format
|
||||
msgid "%(name)s guarding cluster %(cluster_name)s"
|
||||
msgstr "%(name)s обслуживает кластер %(cluster_name)s"
|
||||
|
||||
#: django_q/cluster.py:307
|
||||
#, python-format
|
||||
msgid "Q Cluster %(cluster_name)s running."
|
||||
msgstr "Кластер Q %(cluster_name)s работает."
|
||||
|
||||
#: django_q/cluster.py:346
|
||||
#, python-format
|
||||
msgid "%(name)s stopping cluster processes"
|
||||
msgstr "%(name)s останавливает процессы кластера"
|
||||
|
||||
#: django_q/cluster.py:371
|
||||
#, python-format
|
||||
msgid "%(name)s waiting for the monitor."
|
||||
msgstr "%(name)s ожидает монитор."
|
||||
|
||||
#: django_q/cluster.py:394
|
||||
msgid "Skipping cpu affinity because psutil was not found."
|
||||
msgstr "Привязка к CPU пропущена: не найден psutil."
|
||||
|
||||
#: django_q/cluster.py:399
|
||||
msgid "Faking cpu affinity because it is not supported on this platform"
|
||||
msgstr "Имитация привязки к CPU: на этой платформе не поддерживается"
|
||||
|
||||
#: django_q/cluster.py:421
|
||||
#, python-format
|
||||
msgid "%(pid)s will use cpu %(affinity)s"
|
||||
msgstr "%(pid)s будет использовать CPU %(affinity)s"
|
||||
|
||||
#: django_q/conf.py:122
|
||||
#, python-format
|
||||
msgid ""
|
||||
"SAVE_LIMIT_PER (%(option)s) is not a valid option. Options are: 'group', "
|
||||
"'name', 'func' and None. Default is None."
|
||||
msgstr ""
|
||||
"SAVE_LIMIT_PER (%(option)s) — недопустимое значение. Допустимо: «group», "
|
||||
"«name», «func» и None. По умолчанию None."
|
||||
|
||||
#. Translators: Cluster status descriptions
|
||||
#: django_q/conf.py:239
|
||||
msgid "Starting"
|
||||
msgstr "Запуск"
|
||||
|
||||
#: django_q/conf.py:240
|
||||
msgid "Working"
|
||||
msgstr "Работает"
|
||||
|
||||
#: django_q/conf.py:241
|
||||
msgid "Idle"
|
||||
msgstr "Простой"
|
||||
|
||||
#: django_q/conf.py:242
|
||||
msgid "Stopped"
|
||||
msgstr "Остановлен"
|
||||
|
||||
#: django_q/conf.py:243
|
||||
msgid "Stopping"
|
||||
msgstr "Останавливается"
|
||||
|
||||
#. Translators: help text for qcluster management command
|
||||
#: django_q/management/commands/qcluster.py:11
|
||||
msgid "Starts a Django Q Cluster."
|
||||
msgstr "Запускает кластер Django Q."
|
||||
|
||||
#. Translators: help text for qinfo management command
|
||||
#: django_q/management/commands/qinfo.py:11
|
||||
msgid "General information over all clusters."
|
||||
msgstr "Общая информация по всем кластерам."
|
||||
|
||||
#. Translators: help text for qmemory management command
|
||||
#: django_q/management/commands/qmemory.py:9
|
||||
msgid "Monitors Q Cluster memory usage"
|
||||
msgstr "Мониторинг использования памяти кластером Q"
|
||||
|
||||
#. Translators: help text for qmonitor management command
|
||||
#: django_q/management/commands/qmonitor.py:9
|
||||
msgid "Monitors Q Cluster activity"
|
||||
msgstr "Мониторинг активности кластера Q"
|
||||
|
||||
#: django_q/models.py:30 django_q/models.py:469
|
||||
msgid "Task id"
|
||||
msgstr "Идентификатор задачи"
|
||||
|
||||
#: django_q/models.py:31
|
||||
msgid "32-character task identifier."
|
||||
msgstr "32-символьный идентификатор задачи."
|
||||
|
||||
#: django_q/models.py:36 django_q/models.py:235 django_q/models.py:470
|
||||
msgid "Name"
|
||||
msgstr "Имя"
|
||||
|
||||
#: django_q/models.py:37
|
||||
msgid "Optional human-readable name for lookup and display."
|
||||
msgstr "Необязательное имя для поиска и отображения."
|
||||
|
||||
#: django_q/models.py:41 django_q/models.py:240 django_q/models.py:468
|
||||
msgid "Function"
|
||||
msgstr "Функция"
|
||||
|
||||
#: django_q/models.py:42
|
||||
msgid "Dotted import path to the callable, e.g. myapp.tasks.job."
|
||||
msgstr "Путь импорта к вызываемому объекту, напр. myapp.tasks.job."
|
||||
|
||||
#: django_q/models.py:47 django_q/models.py:247
|
||||
msgid "Hook"
|
||||
msgstr "Обработчик"
|
||||
|
||||
#: django_q/models.py:48
|
||||
msgid "Optional dotted path to a callback that receives the task result."
|
||||
msgstr "Необязательный путь к функции, получающей результат задачи."
|
||||
|
||||
#: django_q/models.py:53 django_q/models.py:253 django_q/models.py:472
|
||||
msgid "Arguments"
|
||||
msgstr "Аргументы"
|
||||
|
||||
#: django_q/models.py:54
|
||||
msgid "Pickled positional arguments (tuple)."
|
||||
msgstr "Сериализованные позиционные аргументы (tuple)."
|
||||
|
||||
#: django_q/models.py:59 django_q/models.py:259 django_q/models.py:473
|
||||
msgid "Keyword arguments"
|
||||
msgstr "Именованные аргументы"
|
||||
|
||||
#: django_q/models.py:60
|
||||
msgid "Pickled keyword arguments (dict)."
|
||||
msgstr "Сериализованные именованные аргументы (dict)."
|
||||
|
||||
#: django_q/models.py:65
|
||||
msgid "Result"
|
||||
msgstr "Результат"
|
||||
|
||||
#: django_q/models.py:66
|
||||
msgid "Pickled return value or error payload."
|
||||
msgstr "Сериализованный результат или данные об ошибке."
|
||||
|
||||
#: django_q/models.py:72 django_q/models.py:471
|
||||
msgid "Group"
|
||||
msgstr "Группа"
|
||||
|
||||
#: django_q/models.py:73
|
||||
msgid "Optional group id to aggregate related tasks."
|
||||
msgstr "Необязательный идентификатор группы связанных задач."
|
||||
|
||||
#: django_q/models.py:80 django_q/models.py:328
|
||||
msgid "Cluster"
|
||||
msgstr "Кластер"
|
||||
|
||||
#: django_q/models.py:81
|
||||
msgid "Name of the cluster that executed this task."
|
||||
msgstr "Имя кластера, выполнившего эту задачу."
|
||||
|
||||
#: django_q/models.py:85
|
||||
msgid "Started at"
|
||||
msgstr "Начало выполнения"
|
||||
|
||||
#: django_q/models.py:86
|
||||
msgid "When the worker started executing the task."
|
||||
msgstr "Когда воркер начал выполнение задачи."
|
||||
|
||||
#: django_q/models.py:90
|
||||
msgid "Stopped at"
|
||||
msgstr "Окончание выполнения"
|
||||
|
||||
#: django_q/models.py:91
|
||||
msgid "When the worker finished (success or failure)."
|
||||
msgstr "Когда воркер завершил выполнение (успех или ошибка)."
|
||||
|
||||
#: django_q/models.py:96 django_q/monitor_terminal.py:180
|
||||
msgid "Success"
|
||||
msgstr "Успех"
|
||||
|
||||
#: django_q/models.py:97
|
||||
msgid "False if the task raised an exception or timed out."
|
||||
msgstr "Ложь, если задача завершилась исключением или таймаутом."
|
||||
|
||||
#: django_q/models.py:101
|
||||
msgid "Attempt count"
|
||||
msgstr "Число попыток"
|
||||
|
||||
#: django_q/models.py:102
|
||||
msgid "How many times execution was attempted."
|
||||
msgstr "Сколько раз выполнялась попытка запуска."
|
||||
|
||||
#: django_q/models.py:172
|
||||
msgid "Task"
|
||||
msgstr "Задача"
|
||||
|
||||
#: django_q/models.py:173
|
||||
msgid "Tasks"
|
||||
msgstr "Задачи"
|
||||
|
||||
#: django_q/models.py:194
|
||||
msgid "Successful task"
|
||||
msgstr "Успешная задача"
|
||||
|
||||
#: django_q/models.py:195
|
||||
msgid "Successful tasks"
|
||||
msgstr "Успешные задачи"
|
||||
|
||||
#: django_q/models.py:210
|
||||
msgid "Failed task"
|
||||
msgstr "Неудачная задача"
|
||||
|
||||
#: django_q/models.py:211
|
||||
msgid "Failed tasks"
|
||||
msgstr "Неудачные задачи"
|
||||
|
||||
#: django_q/models.py:219 django_q/models.py:347
|
||||
msgid "Please install croniter to enable cron expressions"
|
||||
msgstr "Установите croniter для поддержки cron-выражений"
|
||||
|
||||
#: django_q/models.py:236
|
||||
msgid "Optional label to identify this schedule in the admin."
|
||||
msgstr "Необязательная подпись для расписания в админке."
|
||||
|
||||
#: django_q/models.py:241
|
||||
msgid "e.g. module.tasks.function"
|
||||
msgstr "например: module.tasks.function"
|
||||
|
||||
#: django_q/models.py:248
|
||||
msgid "e.g. module.tasks.result_function"
|
||||
msgstr "например: module.tasks.result_function "
|
||||
|
||||
#: django_q/models.py:254
|
||||
msgid "e.g. 1, 2, 'John'"
|
||||
msgstr "например: 1, 2, 'Алимхан'"
|
||||
|
||||
#: django_q/models.py:260
|
||||
msgid "e.g. x=1, y=2, name='John'"
|
||||
msgstr "например: x=1, y=2, name='Алимхан'"
|
||||
|
||||
#: django_q/models.py:274
|
||||
msgid "Once"
|
||||
msgstr "Один раз"
|
||||
|
||||
#: django_q/models.py:275 django_q/models.py:296
|
||||
msgid "Minutes"
|
||||
msgstr "Минуты"
|
||||
|
||||
#: django_q/models.py:276
|
||||
msgid "Hourly"
|
||||
msgstr "Ежечасно"
|
||||
|
||||
#: django_q/models.py:277
|
||||
msgid "Daily"
|
||||
msgstr "Ежедневно"
|
||||
|
||||
#: django_q/models.py:278
|
||||
msgid "Weekly"
|
||||
msgstr "Еженедельно"
|
||||
|
||||
#: django_q/models.py:279
|
||||
msgid "Biweekly"
|
||||
msgstr "Раз в две недели"
|
||||
|
||||
#: django_q/models.py:280
|
||||
msgid "Monthly"
|
||||
msgstr "Ежемесячно"
|
||||
|
||||
#: django_q/models.py:281
|
||||
msgid "Bimonthly"
|
||||
msgstr "Раз в два месяца"
|
||||
|
||||
#: django_q/models.py:282
|
||||
msgid "Quarterly"
|
||||
msgstr "Ежеквартально"
|
||||
|
||||
#: django_q/models.py:283
|
||||
msgid "Yearly"
|
||||
msgstr "Ежегодно"
|
||||
|
||||
#: django_q/models.py:284 django_q/models.py:313
|
||||
msgid "Cron"
|
||||
msgstr "Cron"
|
||||
|
||||
#: django_q/models.py:290
|
||||
msgid "Schedule Type"
|
||||
msgstr "Тип расписания"
|
||||
|
||||
#: django_q/models.py:291
|
||||
msgid "How often this task should be enqueued."
|
||||
msgstr "Как часто ставить эту задачу в очередь."
|
||||
|
||||
#: django_q/models.py:297
|
||||
msgid "Number of minutes for the Minutes type"
|
||||
msgstr "Число минут для типа «Минуты»"
|
||||
|
||||
#: django_q/models.py:300
|
||||
msgid "Repeats"
|
||||
msgstr "Повторы"
|
||||
|
||||
#: django_q/models.py:300
|
||||
msgid "n = n times, -1 = forever"
|
||||
msgstr "n — число повторов, -1 — бесконечно"
|
||||
|
||||
#: django_q/models.py:303
|
||||
msgid "Next Run"
|
||||
msgstr "Следующий запуск"
|
||||
|
||||
#: django_q/models.py:304
|
||||
msgid "When this schedule runs next (stored in UTC)."
|
||||
msgstr "Когда следующий запуск по расписанию (в UTC)."
|
||||
|
||||
#: django_q/models.py:314
|
||||
msgid "Cron expression"
|
||||
msgstr "Cron-выражение"
|
||||
|
||||
#: django_q/models.py:320
|
||||
msgid "Last task id"
|
||||
msgstr "ID последней задачи"
|
||||
|
||||
#: django_q/models.py:321
|
||||
msgid "Id of the last task spawned from this schedule (read-only)."
|
||||
msgstr "Идентификатор последней задачи по этому расписанию (read-only)."
|
||||
|
||||
#: django_q/models.py:329 django_q/models.py:427
|
||||
msgid "Name of the target cluster"
|
||||
msgstr "Имя целевого кластера"
|
||||
|
||||
#: django_q/models.py:336
|
||||
msgid "Intended date kwarg"
|
||||
msgstr "Имя аргумента для даты запуска"
|
||||
|
||||
#: django_q/models.py:337
|
||||
msgid "Name of kwarg to pass intended schedule date"
|
||||
msgstr "Имя kwargs для передачи запланированной даты"
|
||||
|
||||
#: django_q/models.py:418
|
||||
msgid "Scheduled task"
|
||||
msgstr "Запланированная задача"
|
||||
|
||||
#: django_q/models.py:419
|
||||
msgid "Scheduled tasks"
|
||||
msgstr "Запланированные задачи"
|
||||
|
||||
#: django_q/models.py:426
|
||||
msgid "Cluster key"
|
||||
msgstr "Ключ кластера"
|
||||
|
||||
#: django_q/models.py:430
|
||||
msgid "Payload"
|
||||
msgstr "Данные задачи"
|
||||
|
||||
#: django_q/models.py:431
|
||||
msgid "Signed serialized task package (do not edit manually)."
|
||||
msgstr "Подписанный сериализованный пакет задачи (не редактировать вручную)."
|
||||
|
||||
#: django_q/models.py:435
|
||||
msgid "Lock until"
|
||||
msgstr "Блокировка до"
|
||||
|
||||
#: django_q/models.py:436
|
||||
msgid "Prevent any cluster from pulling until"
|
||||
msgstr "Не забирать задачи кластерами до"
|
||||
|
||||
#: django_q/models.py:474
|
||||
msgid "Queue options"
|
||||
msgstr "Дополнительные параметры очереди"
|
||||
|
||||
#: django_q/models.py:478
|
||||
msgid "Queued task"
|
||||
msgstr "Задача в очереди"
|
||||
|
||||
#: django_q/models.py:479
|
||||
msgid "Queued tasks"
|
||||
msgstr "Задачи в очереди"
|
||||
|
||||
#: django_q/monitor.py:41
|
||||
#, python-format
|
||||
msgid "%(name)s monitoring at %(id)s"
|
||||
msgstr "%(name)s мониторинг на %(id)s"
|
||||
|
||||
#: django_q/monitor.py:61
|
||||
#, python-format
|
||||
msgid "Processed '%(info_name)s' (%(task_name)s)"
|
||||
msgstr "Обработано «%(info_name)s» (%(task_name)s)"
|
||||
|
||||
#: django_q/monitor.py:67
|
||||
#, python-format
|
||||
msgid "Failed '%(info_name)s' (%(task_name)s) - %(task_result)s"
|
||||
msgstr "Ошибка «%(info_name)s» (%(task_name)s) — %(task_result)s"
|
||||
|
||||
#: django_q/monitor.py:74
|
||||
#, python-format
|
||||
msgid "%(name)s stopped monitoring results"
|
||||
msgstr "%(name)s прекратил отслеживание результатов"
|
||||
|
||||
#: django_q/monitor_terminal.py:64 django_q/monitor_terminal.py:348
|
||||
msgid "Host"
|
||||
msgstr "Хост"
|
||||
|
||||
#: django_q/monitor_terminal.py:68 django_q/monitor_terminal.py:352
|
||||
#: django_q/monitor_terminal.py:457
|
||||
msgid "Id"
|
||||
msgstr "Id"
|
||||
|
||||
#: django_q/monitor_terminal.py:72
|
||||
msgid "State"
|
||||
msgstr "Состояние"
|
||||
|
||||
#: django_q/monitor_terminal.py:76
|
||||
msgid "Pool"
|
||||
msgstr "Пул"
|
||||
|
||||
#: django_q/monitor_terminal.py:80
|
||||
msgid "TQ"
|
||||
msgstr "TQ"
|
||||
|
||||
#: django_q/monitor_terminal.py:84
|
||||
msgid "RQ"
|
||||
msgstr "RQ"
|
||||
|
||||
#: django_q/monitor_terminal.py:88
|
||||
msgid "RC"
|
||||
msgstr "RC"
|
||||
|
||||
#: django_q/monitor_terminal.py:92
|
||||
msgid "Up"
|
||||
msgstr "Аптайм"
|
||||
|
||||
#: django_q/monitor_terminal.py:172 django_q/monitor_terminal.py:286
|
||||
msgid "Queued"
|
||||
msgstr "В очереди"
|
||||
|
||||
#: django_q/monitor_terminal.py:190 django_q/monitor_terminal.py:294
|
||||
msgid "Failures"
|
||||
msgstr "Сбои"
|
||||
|
||||
#: django_q/monitor_terminal.py:201 django_q/monitor_terminal.py:496
|
||||
msgid "[Press q to quit]"
|
||||
msgstr "[Нажмите q для выхода]"
|
||||
|
||||
#: django_q/monitor_terminal.py:227
|
||||
msgid "day"
|
||||
msgstr "день"
|
||||
|
||||
#: django_q/monitor_terminal.py:248
|
||||
msgid "second"
|
||||
msgstr "секунда"
|
||||
|
||||
#: django_q/monitor_terminal.py:251
|
||||
msgid "minute"
|
||||
msgstr "минута"
|
||||
|
||||
#: django_q/monitor_terminal.py:254
|
||||
msgid "hour"
|
||||
msgstr "час"
|
||||
|
||||
#: django_q/monitor_terminal.py:263
|
||||
#, python-format
|
||||
msgid "-- %(prefix)s %(version)s on %(info)s --"
|
||||
msgstr "-- %(prefix)s %(version)s на %(info)s --"
|
||||
|
||||
#: django_q/monitor_terminal.py:273
|
||||
msgid "Clusters"
|
||||
msgstr "Кластеры"
|
||||
|
||||
#: django_q/monitor_terminal.py:277
|
||||
msgid "Workers"
|
||||
msgstr "Воркеры"
|
||||
|
||||
#: django_q/monitor_terminal.py:281
|
||||
msgid "Restarts"
|
||||
msgstr "Перезапуски"
|
||||
|
||||
#: django_q/monitor_terminal.py:290
|
||||
msgid "Successes"
|
||||
msgstr "Успехи"
|
||||
|
||||
#: django_q/monitor_terminal.py:299
|
||||
msgid "Schedules"
|
||||
msgstr "Расписания"
|
||||
|
||||
#: django_q/monitor_terminal.py:303
|
||||
#, python-format
|
||||
msgid "Tasks/%(per)s"
|
||||
msgstr "Задач/%(per)s"
|
||||
|
||||
#: django_q/monitor_terminal.py:307
|
||||
msgid "Avg time"
|
||||
msgstr "Среднее время"
|
||||
|
||||
#: django_q/monitor_terminal.py:357
|
||||
msgid "Available (%)"
|
||||
msgstr "Доступно (%)"
|
||||
|
||||
#: django_q/monitor_terminal.py:363
|
||||
msgid "Available (MB)"
|
||||
msgstr "Доступно (МБ)"
|
||||
|
||||
#: django_q/monitor_terminal.py:368
|
||||
msgid "Total (MB)"
|
||||
msgstr "Всего (МБ)"
|
||||
|
||||
#: django_q/monitor_terminal.py:373
|
||||
msgid "Sentinel (MB)"
|
||||
msgstr "Sentinel (МБ)"
|
||||
|
||||
#: django_q/monitor_terminal.py:379
|
||||
msgid "Monitor (MB)"
|
||||
msgstr "Монитор (МБ)"
|
||||
|
||||
#: django_q/monitor_terminal.py:385
|
||||
msgid "Workers (MB)"
|
||||
msgstr "Воркеры (МБ)"
|
||||
|
||||
#: django_q/monitor_terminal.py:485
|
||||
#, python-format
|
||||
msgid "Available lowest (): %(memory_percent)s ((at)s)"
|
||||
msgstr "Минимум доступно (): %(memory_percent)s ((at)s)"
|
||||
|
||||
#: django_q/monitor_terminal.py:507
|
||||
msgid "No clusters appear to be running."
|
||||
msgstr "Похоже, ни один кластер не запущен."
|
||||
|
||||
#: django_q/pusher.py:40
|
||||
#, python-format
|
||||
msgid "%(name)s pushing tasks at %(id)s"
|
||||
msgstr "%(name)s отправляет задачи на %(id)s"
|
||||
|
||||
#: django_q/pusher.py:67
|
||||
#, python-format
|
||||
msgid "queueing from %(list_key)s"
|
||||
msgstr "очередь из %(list_key)s"
|
||||
|
||||
#: django_q/pusher.py:71
|
||||
#, python-format
|
||||
msgid "%(name)s stopped pushing tasks"
|
||||
msgstr "%(name)s прекратил отправку задач"
|
||||
|
||||
#: django_q/scheduler.py:110
|
||||
#, python-format
|
||||
msgid "%(process_name)s failed to create a task from schedule [%(schedule)s]"
|
||||
msgstr ""
|
||||
"%(process_name)s не удалось создать задачу по расписанию [%(schedule)s]"
|
||||
|
||||
#: django_q/scheduler.py:121
|
||||
#, python-format
|
||||
msgid ""
|
||||
"%(process_name)s created task %(task_name)s from schedule [%(schedule)s]"
|
||||
msgstr ""
|
||||
"%(process_name)s создал задачу %(task_name)s по расписанию [%(schedule)s]"
|
||||
|
||||
#: django_q/signals.py:22
|
||||
#, python-format
|
||||
msgid "malformed return hook '%(hook)s' for [%(name)s]"
|
||||
msgstr "некорректный return hook «%(hook)s» для [%(name)s]"
|
||||
|
||||
#: django_q/signals.py:30
|
||||
#, python-format
|
||||
msgid "return hook %(hook)s failed on [%(name)s] because %(error)s"
|
||||
msgstr "return hook %(hook)s для [%(name)s] завершился ошибкой: %(error)s"
|
||||
|
||||
#: django_q/timeout.py:30 django_q/timeout.py:43
|
||||
msgid "SIGALARM is not available on your platform"
|
||||
msgstr "SIGALRM недоступен на вашей платформе"
|
||||
|
||||
#: django_q/worker.py:49
|
||||
#, python-format
|
||||
msgid "%(proc_name)s ready for work at %(id)s"
|
||||
msgstr "%(proc_name)s готов к работе на %(id)s"
|
||||
|
||||
#: django_q/worker.py:69
|
||||
#, python-format
|
||||
msgid "%(proc_name)s processing %(task_name)s '%(func_name)s'"
|
||||
msgstr "%(proc_name)s выполняет %(task_name)s «%(func_name)s»"
|
||||
|
||||
#: django_q/worker.py:135
|
||||
#, python-format
|
||||
msgid "%(proc_name)s stopped doing work"
|
||||
msgstr "%(proc_name)s прекратил работу"
|
||||
|
||||
#, python-format
|
||||
#~ msgid ""
|
||||
#~ "Could not process '%(func_name)s'. Check the location of the function and "
|
||||
#~ "the args/kwargs."
|
||||
#~ msgstr ""
|
||||
#~ "Не удалось обработать «%(func_name)s». Проверьте путь к функции и args/"
|
||||
#~ "kwargs."
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-04-17 21:10+0000\n"
|
||||
"POT-Creation-Date: 2023-01-26 01:38+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: Ethem Güner <ethemguener@gmail.com>\n"
|
||||
"Language-Team: \n"
|
||||
@@ -18,44 +18,36 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
|
||||
#: admin.py:30
|
||||
#: admin.py:43
|
||||
msgid "Resubmit selected tasks to queue"
|
||||
msgstr "Seçili işleri kuyruğa tekrar gönder"
|
||||
|
||||
#: admin.py:64
|
||||
msgid "Time Taken"
|
||||
msgstr ""
|
||||
|
||||
#: admin.py:95
|
||||
msgid "Short result"
|
||||
msgstr ""
|
||||
|
||||
#: admin.py:142 models.py:412
|
||||
#: admin.py:107 models.py:293
|
||||
#, fuzzy
|
||||
#| msgid "Success"
|
||||
msgid "success"
|
||||
msgstr "başarılı olanlar"
|
||||
|
||||
#: admin.py:154 models.py:414
|
||||
#: admin.py:119 models.py:295
|
||||
msgid "last_run"
|
||||
msgstr ""
|
||||
|
||||
#: cluster.py:75
|
||||
#: cluster.py:79
|
||||
#, python-format
|
||||
msgid "Q Cluster %(name)s starting."
|
||||
msgstr "Q Cluster %(name)s başlatılıyor."
|
||||
|
||||
#: cluster.py:83
|
||||
#: cluster.py:87
|
||||
#, python-format
|
||||
msgid "Q Cluster %(name)s stopping."
|
||||
msgstr "Q Cluster %(name)s durduruluyor."
|
||||
|
||||
#: cluster.py:86
|
||||
#: cluster.py:90
|
||||
#, python-format
|
||||
msgid "Q Cluster %(name)s has stopped."
|
||||
msgstr "Q Cluster %(name)s durduruldu."
|
||||
|
||||
#: cluster.py:93
|
||||
#: cluster.py:97
|
||||
#, python-format
|
||||
msgid "%(name)s got signal %(signal)s"
|
||||
msgstr "%(name)s, %(signal)s pid'inde izleniyor/monitoring yapılıyor."
|
||||
@@ -70,7 +62,7 @@ msgstr "Monitor %(name)s ani ölüm sonrası tekrar dirildi"
|
||||
msgid "reincarnated pusher %(name)s after sudden death"
|
||||
msgstr "Pusher %(name)s ani ölüm sonrası tekrar dirildi"
|
||||
|
||||
#: cluster.py:262
|
||||
#: cluster.py:250
|
||||
#, fuzzy, python-format
|
||||
#| msgid "reincarnated worker %(name)s after timeout"
|
||||
msgid ""
|
||||
@@ -78,55 +70,119 @@ msgid ""
|
||||
"%(task_name)s"
|
||||
msgstr "Worker %(name)s zaman aşımı sonrası tekrar dirildi"
|
||||
|
||||
#: cluster.py:265
|
||||
#: cluster.py:255
|
||||
#, python-format
|
||||
msgid "reincarnated worker %(name)s after timeout"
|
||||
msgstr "Worker %(name)s zaman aşımı sonrası tekrar dirildi"
|
||||
|
||||
#: cluster.py:270
|
||||
#: cluster.py:260
|
||||
#, python-format
|
||||
msgid "recycled worker %(name)s"
|
||||
msgstr "Worker %(name)s geri döndürüldü"
|
||||
|
||||
#: cluster.py:273
|
||||
#: cluster.py:263
|
||||
#, python-format
|
||||
msgid "reincarnated worker %(name)s after death"
|
||||
msgstr "Worker %(name)s ani ölüm sonrası tekrar dirildi"
|
||||
|
||||
#: cluster.py:297
|
||||
#: cluster.py:287
|
||||
#, python-format
|
||||
msgid "%(name)s guarding cluster %(cluster_name)s"
|
||||
msgstr "%(name)s, %(cluster_name)s cluster'ını koruyor"
|
||||
|
||||
#: cluster.py:307
|
||||
#: cluster.py:296
|
||||
#, python-format
|
||||
msgid "Q Cluster %(cluster_name)s running."
|
||||
msgstr "Q Cluster %(cluster_name)s başlatılıyor."
|
||||
|
||||
#: cluster.py:346
|
||||
#: cluster.py:332
|
||||
#, python-format
|
||||
msgid "%(name)s stopping cluster processes"
|
||||
msgstr "Cluster %(name)s işlemleri durduruluyor."
|
||||
|
||||
#: cluster.py:371
|
||||
#: cluster.py:357
|
||||
#, python-format
|
||||
msgid "%(name)s waiting for the monitor."
|
||||
msgstr "%(name)s monitor için bekliyor."
|
||||
|
||||
#: cluster.py:394
|
||||
#: cluster.py:383
|
||||
#, fuzzy, python-format
|
||||
#| msgid "%(process_name)s pushing tasks at %(id)s"
|
||||
msgid "%(name)s pushing tasks at %(id)s"
|
||||
msgstr "%(process_name)s, işleri %(id)s pid'ine gönderiyor."
|
||||
|
||||
#: cluster.py:407
|
||||
#, python-format
|
||||
msgid "queueing from %(list_key)s"
|
||||
msgstr ""
|
||||
|
||||
#: cluster.py:411
|
||||
#, python-format
|
||||
msgid "%(name)s stopped pushing tasks"
|
||||
msgstr "%(name)s işleri göndermeyi durdurdu"
|
||||
|
||||
#: cluster.py:426
|
||||
#, python-format
|
||||
msgid "%(name)s monitoring at %(id)s"
|
||||
msgstr "%(name)s, %(id)s pid'inde izleniyor/monitoring yapılıyor."
|
||||
|
||||
#: cluster.py:445
|
||||
#, python-format
|
||||
msgid "Processed '%(info_name)s' (%(task_name)s)"
|
||||
msgstr "[%(task_name)s] - '%(info_name)s işlendi."
|
||||
|
||||
#: cluster.py:451
|
||||
#, python-format
|
||||
msgid "Failed '%(info_name)s' (%(task_name)s) - %(task_result)s"
|
||||
msgstr "[%(task_name)s] - '%(info_name)s' - %(task_result)s başarısız oldu"
|
||||
|
||||
#: cluster.py:458
|
||||
#, python-format
|
||||
msgid "%(name)s stopped monitoring results"
|
||||
msgstr "%(name)s sonuçları göstermeyi bıraktı"
|
||||
|
||||
#: cluster.py:474
|
||||
#, python-format
|
||||
msgid "%(proc_name)s ready for work at %(id)s"
|
||||
msgstr "%(proc_name)s, %(id)s pid'inde çalışmaya hazır"
|
||||
|
||||
#: cluster.py:494
|
||||
#, fuzzy, python-format
|
||||
#| msgid "%(proc_name)s processing '%(func_name)s' (%(task_name)s)"
|
||||
msgid "%(proc_name)s processing %(task_name)s '%(func_name)s'"
|
||||
msgstr "%(proc_name)s, '%(func_name)s' [%(task_name)s] işlerini işiyor"
|
||||
|
||||
#: cluster.py:546
|
||||
#, python-format
|
||||
msgid "%(proc_name)s stopped doing work"
|
||||
msgstr "%(proc_name)s çalışmayı bıraktı"
|
||||
|
||||
#: cluster.py:751
|
||||
#, python-format
|
||||
msgid "%(process_name)s failed to create a task from schedule [%(schedule)s]"
|
||||
msgstr "%(process_name)s programdan bir görev oluşturamadı [%(schedule)s]"
|
||||
|
||||
#: cluster.py:762
|
||||
#, fuzzy, python-format
|
||||
#| msgid "%(process_name)s created a task from schedule [%(schedule)s]"
|
||||
msgid ""
|
||||
"%(process_name)s created task %(task_name)s from schedule [%(schedule)s]"
|
||||
msgstr "%(process_name)s programdan bir görev oluşturamadı [%(schedule)s]"
|
||||
|
||||
#: cluster.py:808
|
||||
msgid "Skipping cpu affinity because psutil was not found."
|
||||
msgstr "Psutil bulunamadığı için cpu benzeşimi atlanıyor."
|
||||
|
||||
#: cluster.py:399
|
||||
#: cluster.py:813
|
||||
msgid "Faking cpu affinity because it is not supported on this platform"
|
||||
msgstr "Bu platformda desteklenmediği için sahte cpu benzeşimi"
|
||||
|
||||
#: cluster.py:421
|
||||
#: cluster.py:835
|
||||
#, python-format
|
||||
msgid "%(pid)s will use cpu %(affinity)s"
|
||||
msgstr "%(pid)s cpu %(affinity)s kullanacaktır"
|
||||
|
||||
#: conf.py:122
|
||||
#: conf.py:90
|
||||
#, python-format
|
||||
msgid ""
|
||||
"SAVE_LIMIT_PER (%(option)s) is not a valid option. Options are: 'group', "
|
||||
@@ -136,28 +192,28 @@ msgstr ""
|
||||
"'group', 'name', 'func' ve None. Varsayılan değer None'dır."
|
||||
|
||||
#. Translators: Cluster status descriptions
|
||||
#: conf.py:239
|
||||
#: conf.py:207
|
||||
msgid "Starting"
|
||||
msgstr "Başlıyor"
|
||||
|
||||
#: conf.py:240
|
||||
#: conf.py:208
|
||||
msgid "Working"
|
||||
msgstr "Çalışıyor"
|
||||
|
||||
#: conf.py:241
|
||||
#: conf.py:209
|
||||
msgid "Idle"
|
||||
msgstr "Boşta"
|
||||
|
||||
#: conf.py:242
|
||||
#: conf.py:210
|
||||
msgid "Stopped"
|
||||
msgstr "Durdu"
|
||||
|
||||
#: conf.py:243
|
||||
#: conf.py:211
|
||||
msgid "Stopping"
|
||||
msgstr "Durduruluyor"
|
||||
|
||||
#. Translators: help text for qcluster management command
|
||||
#: management/commands/qcluster.py:11
|
||||
#: management/commands/qcluster.py:9
|
||||
msgid "Starts a Django Q Cluster."
|
||||
msgstr "Bir Django Q Cluster çalıştırır."
|
||||
|
||||
@@ -176,475 +232,257 @@ msgstr "Q Cluster'ın bellek kullanımını izler"
|
||||
msgid "Monitors Q Cluster activity"
|
||||
msgstr "Q Cluster'ın aktivitelerini izler"
|
||||
|
||||
#: models.py:30 models.py:469
|
||||
msgid "Task id"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:31
|
||||
msgid "32-character task identifier."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:36 models.py:235 models.py:470
|
||||
msgid "Name"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:37
|
||||
msgid "Optional human-readable name for lookup and display."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:41 models.py:240 models.py:468
|
||||
msgid "Function"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:42
|
||||
msgid "Dotted import path to the callable, e.g. myapp.tasks.job."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:47 models.py:247
|
||||
msgid "Hook"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:48
|
||||
msgid "Optional dotted path to a callback that receives the task result."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:53 models.py:253 models.py:472
|
||||
msgid "Arguments"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:54
|
||||
msgid "Pickled positional arguments (tuple)."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:59 models.py:259 models.py:473
|
||||
msgid "Keyword arguments"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:60
|
||||
msgid "Pickled keyword arguments (dict)."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:65
|
||||
msgid "Result"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:66
|
||||
msgid "Pickled return value or error payload."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:72 models.py:471
|
||||
msgid "Group"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:73
|
||||
msgid "Optional group id to aggregate related tasks."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:80 models.py:328
|
||||
msgid "Cluster"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:81
|
||||
msgid "Name of the cluster that executed this task."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:85
|
||||
msgid "Started at"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:86
|
||||
msgid "When the worker started executing the task."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:90
|
||||
#, fuzzy
|
||||
#| msgid "Stopped"
|
||||
msgid "Stopped at"
|
||||
msgstr "Durdu"
|
||||
|
||||
#: models.py:91
|
||||
msgid "When the worker finished (success or failure)."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:96 monitor_terminal.py:180
|
||||
msgid "Success"
|
||||
msgstr "Başarılı olanlar"
|
||||
|
||||
#: models.py:97
|
||||
msgid "False if the task raised an exception or timed out."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:101
|
||||
msgid "Attempt count"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:102
|
||||
msgid "How many times execution was attempted."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:172
|
||||
msgid "Task"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:173
|
||||
msgid "Tasks"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:194
|
||||
#: models.py:125
|
||||
msgid "Successful task"
|
||||
msgstr "Başarılı iş"
|
||||
|
||||
#: models.py:195
|
||||
#: models.py:126
|
||||
msgid "Successful tasks"
|
||||
msgstr "Başarılı işler"
|
||||
|
||||
#: models.py:210
|
||||
#: models.py:141
|
||||
msgid "Failed task"
|
||||
msgstr "Başarısız iş"
|
||||
|
||||
#: models.py:211
|
||||
#: models.py:142
|
||||
msgid "Failed tasks"
|
||||
msgstr "Başarısız işler"
|
||||
|
||||
#: models.py:219 models.py:347
|
||||
#: models.py:150 models.py:234
|
||||
msgid "Please install croniter to enable cron expressions"
|
||||
msgstr "Cron expressions'ları açmak için croniter yükleyin"
|
||||
|
||||
#: models.py:236
|
||||
msgid "Optional label to identify this schedule in the admin."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:241
|
||||
msgid "e.g. module.tasks.function"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:248
|
||||
msgid "e.g. module.tasks.result_function"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:254
|
||||
#: models.py:170
|
||||
msgid "e.g. 1, 2, 'John'"
|
||||
msgstr "Örneğin: 1, 2, 'Melih'"
|
||||
|
||||
#: models.py:260
|
||||
#: models.py:172
|
||||
msgid "e.g. x=1, y=2, name='John'"
|
||||
msgstr "Örneğin: x=1, y=2, name='Melih'"
|
||||
|
||||
#: models.py:274
|
||||
#: models.py:186
|
||||
msgid "Once"
|
||||
msgstr "Bir kere"
|
||||
|
||||
#: models.py:275 models.py:296
|
||||
#: models.py:187
|
||||
msgid "Minutes"
|
||||
msgstr "Dakika"
|
||||
|
||||
#: models.py:276
|
||||
#: models.py:188
|
||||
msgid "Hourly"
|
||||
msgstr "Saatlik"
|
||||
|
||||
#: models.py:277
|
||||
#: models.py:189
|
||||
msgid "Daily"
|
||||
msgstr "Günlük"
|
||||
|
||||
#: models.py:278
|
||||
#: models.py:190
|
||||
msgid "Weekly"
|
||||
msgstr "Haftalık"
|
||||
|
||||
#: models.py:279
|
||||
#: models.py:191
|
||||
#, fuzzy
|
||||
#| msgid "Weekly"
|
||||
msgid "Biweekly"
|
||||
msgstr "İki haftada bir"
|
||||
|
||||
#: models.py:280
|
||||
#: models.py:192
|
||||
msgid "Monthly"
|
||||
msgstr "Aylık"
|
||||
|
||||
#: models.py:281
|
||||
#: models.py:193
|
||||
#, fuzzy
|
||||
#| msgid "Monthly"
|
||||
msgid "Bimonthly"
|
||||
msgstr "İki ayda bir"
|
||||
|
||||
#: models.py:282
|
||||
#: models.py:194
|
||||
msgid "Quarterly"
|
||||
msgstr "Bir Çeyrek (3 Ay)"
|
||||
|
||||
#: models.py:283
|
||||
#: models.py:195
|
||||
msgid "Yearly"
|
||||
msgstr "Yıllık"
|
||||
|
||||
#: models.py:284 models.py:313
|
||||
#: models.py:196
|
||||
msgid "Cron"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:290
|
||||
#: models.py:199
|
||||
msgid "Schedule Type"
|
||||
msgstr "Zamanlama Tipi"
|
||||
|
||||
#: models.py:291
|
||||
msgid "How often this task should be enqueued."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:297
|
||||
#: models.py:202
|
||||
msgid "Number of minutes for the Minutes type"
|
||||
msgstr "Dakika tipine göre dakika sayısı"
|
||||
|
||||
#: models.py:300
|
||||
#: models.py:205
|
||||
msgid "Repeats"
|
||||
msgstr "Tekrar eder"
|
||||
|
||||
#: models.py:300
|
||||
#: models.py:205
|
||||
msgid "n = n times, -1 = forever"
|
||||
msgstr "n = n kere, -1 = sonsuza kadar"
|
||||
|
||||
#: models.py:303
|
||||
#: models.py:208
|
||||
msgid "Next Run"
|
||||
msgstr "Bir dahaki çalışma tarihi"
|
||||
|
||||
#: models.py:304
|
||||
msgid "When this schedule runs next (stored in UTC)."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:314
|
||||
#: models.py:215
|
||||
msgid "Cron expression"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:320
|
||||
msgid "Last task id"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:321
|
||||
msgid "Id of the last task spawned from this schedule (read-only)."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:329 models.py:427
|
||||
msgid "Name of the target cluster"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:336
|
||||
msgid "Intended date kwarg"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:337
|
||||
#: models.py:224
|
||||
msgid "Name of kwarg to pass intended schedule date"
|
||||
msgstr "Geçilecek kwarg'ın adı öngörülen program tarihi"
|
||||
|
||||
#: models.py:418
|
||||
#: models.py:299
|
||||
msgid "Scheduled task"
|
||||
msgstr "Zamanlanmış iş"
|
||||
|
||||
#: models.py:419
|
||||
#: models.py:300
|
||||
msgid "Scheduled tasks"
|
||||
msgstr "Zamanlanmış işler"
|
||||
|
||||
#: models.py:426
|
||||
msgid "Cluster key"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:430
|
||||
msgid "Payload"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:431
|
||||
msgid "Signed serialized task package (do not edit manually)."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:435
|
||||
msgid "Lock until"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:436
|
||||
msgid "Prevent any cluster from pulling until"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:474
|
||||
#, fuzzy
|
||||
#| msgid "Queued task"
|
||||
msgid "Queue options"
|
||||
msgstr "Sıraya alınmış iş"
|
||||
|
||||
#: models.py:478
|
||||
#: models.py:326
|
||||
msgid "Queued task"
|
||||
msgstr "Sıraya alınmış iş"
|
||||
|
||||
#: models.py:479
|
||||
#: models.py:327
|
||||
msgid "Queued tasks"
|
||||
msgstr "Sıraya alınmış işler"
|
||||
|
||||
#: monitor.py:41
|
||||
#, python-format
|
||||
msgid "%(name)s monitoring at %(id)s"
|
||||
msgstr "%(name)s, %(id)s pid'inde izleniyor/monitoring yapılıyor."
|
||||
|
||||
#: monitor.py:61
|
||||
#, python-format
|
||||
msgid "Processed '%(info_name)s' (%(task_name)s)"
|
||||
msgstr "[%(task_name)s] - '%(info_name)s işlendi."
|
||||
|
||||
#: monitor.py:67
|
||||
#, python-format
|
||||
msgid "Failed '%(info_name)s' (%(task_name)s) - %(task_result)s"
|
||||
msgstr "[%(task_name)s] - '%(info_name)s' - %(task_result)s başarısız oldu"
|
||||
|
||||
#: monitor.py:74
|
||||
#, python-format
|
||||
msgid "%(name)s stopped monitoring results"
|
||||
msgstr "%(name)s sonuçları göstermeyi bıraktı"
|
||||
|
||||
#: monitor_terminal.py:64 monitor_terminal.py:348
|
||||
#: monitor.py:64 monitor.py:348
|
||||
msgid "Host"
|
||||
msgstr ""
|
||||
|
||||
#: monitor_terminal.py:68 monitor_terminal.py:352 monitor_terminal.py:457
|
||||
#: monitor.py:68 monitor.py:352 monitor.py:459
|
||||
msgid "Id"
|
||||
msgstr ""
|
||||
|
||||
#: monitor_terminal.py:72
|
||||
#: monitor.py:72
|
||||
msgid "State"
|
||||
msgstr "Durum"
|
||||
|
||||
#: monitor_terminal.py:76
|
||||
#: monitor.py:76
|
||||
msgid "Pool"
|
||||
msgstr "Havuz"
|
||||
|
||||
#: monitor_terminal.py:80
|
||||
#: monitor.py:80
|
||||
msgid "TQ"
|
||||
msgstr ""
|
||||
|
||||
#: monitor_terminal.py:84
|
||||
#: monitor.py:84
|
||||
msgid "RQ"
|
||||
msgstr ""
|
||||
|
||||
#: monitor_terminal.py:88
|
||||
#: monitor.py:88
|
||||
msgid "RC"
|
||||
msgstr ""
|
||||
|
||||
#: monitor_terminal.py:92
|
||||
#: monitor.py:92
|
||||
msgid "Up"
|
||||
msgstr ""
|
||||
|
||||
#: monitor_terminal.py:172 monitor_terminal.py:286
|
||||
#: monitor.py:172 monitor.py:286
|
||||
msgid "Queued"
|
||||
msgstr "Sıraya alınmış"
|
||||
|
||||
#: monitor_terminal.py:190 monitor_terminal.py:294
|
||||
#: monitor.py:180
|
||||
msgid "Success"
|
||||
msgstr "Başarılı olanlar"
|
||||
|
||||
#: monitor.py:190 monitor.py:294
|
||||
msgid "Failures"
|
||||
msgstr "Başarısız olanlar"
|
||||
|
||||
#: monitor_terminal.py:201 monitor_terminal.py:496
|
||||
#: monitor.py:201 monitor.py:498
|
||||
msgid "[Press q to quit]"
|
||||
msgstr "[Çıkmak için q'ya basın]"
|
||||
|
||||
#: monitor_terminal.py:227
|
||||
#: monitor.py:227
|
||||
msgid "day"
|
||||
msgstr "gün"
|
||||
|
||||
#: monitor_terminal.py:248
|
||||
#: monitor.py:248
|
||||
msgid "second"
|
||||
msgstr "saniye"
|
||||
|
||||
#: monitor_terminal.py:251
|
||||
#: monitor.py:251
|
||||
msgid "minute"
|
||||
msgstr "dakika"
|
||||
|
||||
#: monitor_terminal.py:254
|
||||
#: monitor.py:254
|
||||
msgid "hour"
|
||||
msgstr "saat"
|
||||
|
||||
#: monitor_terminal.py:263
|
||||
#: monitor.py:263
|
||||
#, python-format
|
||||
msgid "-- %(prefix)s %(version)s on %(info)s --"
|
||||
msgstr "-- %(prefix)s %(version)s üzerinde %(info)s --"
|
||||
|
||||
#: monitor_terminal.py:273
|
||||
#: monitor.py:273
|
||||
msgid "Clusters"
|
||||
msgstr ""
|
||||
|
||||
#: monitor_terminal.py:277
|
||||
#: monitor.py:277
|
||||
msgid "Workers"
|
||||
msgstr ""
|
||||
|
||||
#: monitor_terminal.py:281
|
||||
#: monitor.py:281
|
||||
msgid "Restarts"
|
||||
msgstr "Yeniden çalıştırmalar"
|
||||
|
||||
#: monitor_terminal.py:290
|
||||
#: monitor.py:290
|
||||
msgid "Successes"
|
||||
msgstr "Başarılı olanlar"
|
||||
|
||||
#: monitor_terminal.py:299
|
||||
#: monitor.py:299
|
||||
msgid "Schedules"
|
||||
msgstr "Zamanlanmışlar"
|
||||
|
||||
#: monitor_terminal.py:303
|
||||
#: monitor.py:303
|
||||
#, python-format
|
||||
msgid "Tasks/%(per)s"
|
||||
msgstr "İş/%(per)s"
|
||||
|
||||
#: monitor_terminal.py:307
|
||||
#: monitor.py:307
|
||||
msgid "Avg time"
|
||||
msgstr "Ortalama süre"
|
||||
|
||||
#: monitor_terminal.py:357
|
||||
#: monitor.py:357
|
||||
msgid "Available (%)"
|
||||
msgstr "Müsait (%) "
|
||||
|
||||
#: monitor_terminal.py:363
|
||||
#: monitor.py:363
|
||||
msgid "Available (MB)"
|
||||
msgstr "Müsait (MB)"
|
||||
|
||||
#: monitor_terminal.py:368
|
||||
#: monitor.py:368
|
||||
msgid "Total (MB)"
|
||||
msgstr "Toplam (MB)"
|
||||
|
||||
#: monitor_terminal.py:373
|
||||
#: monitor.py:373
|
||||
msgid "Sentinel (MB)"
|
||||
msgstr ""
|
||||
|
||||
#: monitor_terminal.py:379
|
||||
#: monitor.py:379
|
||||
msgid "Monitor (MB)"
|
||||
msgstr "İzleme (MB)"
|
||||
|
||||
#: monitor_terminal.py:385
|
||||
#: monitor.py:385
|
||||
msgid "Workers (MB)"
|
||||
msgstr ""
|
||||
|
||||
#: monitor_terminal.py:485
|
||||
#: monitor.py:487
|
||||
#, python-format
|
||||
msgid "Available lowest (): %(memory_percent)s ((at)s)"
|
||||
msgstr "Mevcut en düşük (): %(memory_percent)s ((at)s)"
|
||||
|
||||
#: monitor_terminal.py:507
|
||||
#: monitor.py:509
|
||||
msgid "No clusters appear to be running."
|
||||
msgstr "Hiçbir küme çalışıyor görünmüyor."
|
||||
|
||||
#: pusher.py:40
|
||||
#, python-format
|
||||
msgid "%(name)s pushing tasks at %(id)s"
|
||||
msgstr "%(name)s, işleri %(id)s pid'ine gönderiyor."
|
||||
|
||||
#: pusher.py:67
|
||||
#, python-format
|
||||
msgid "queueing from %(list_key)s"
|
||||
msgstr "%(list_key)s kuyruğa alınıyor"
|
||||
|
||||
#: pusher.py:71
|
||||
#, python-format
|
||||
msgid "%(name)s stopped pushing tasks"
|
||||
msgstr "%(name)s işleri göndermeyi durdurdu"
|
||||
|
||||
#: scheduler.py:110
|
||||
#, python-format
|
||||
msgid "%(process_name)s failed to create a task from schedule [%(schedule)s]"
|
||||
msgstr "%(process_name)s programdan bir görev oluşturamadı [%(schedule)s]"
|
||||
|
||||
#: scheduler.py:121
|
||||
#, python-format
|
||||
msgid ""
|
||||
"%(process_name)s created task %(task_name)s from schedule [%(schedule)s]"
|
||||
msgstr "%(process_name)s programdan %(task_name)s görevini oluşturdu [%(schedule)s]"
|
||||
|
||||
#: signals.py:22
|
||||
#, python-format
|
||||
msgid "malformed return hook '%(hook)s' for [%(name)s]"
|
||||
@@ -655,26 +493,6 @@ msgstr ""
|
||||
msgid "return hook %(hook)s failed on [%(name)s] because %(error)s"
|
||||
msgstr ""
|
||||
|
||||
#: timeout.py:30 timeout.py:43
|
||||
msgid "SIGALARM is not available on your platform"
|
||||
msgstr ""
|
||||
|
||||
#: worker.py:49
|
||||
#, python-format
|
||||
msgid "%(proc_name)s ready for work at %(id)s"
|
||||
msgstr "%(proc_name)s, %(id)s pid'inde çalışmaya hazır"
|
||||
|
||||
#: worker.py:69
|
||||
#, fuzzy, python-format
|
||||
#| msgid "%(proc_name)s processing '%(func_name)s' (%(task_name)s)"
|
||||
msgid "%(proc_name)s processing %(task_name)s '%(func_name)s'"
|
||||
msgstr "%(proc_name)s, '%(func_name)s' [%(task_name)s] işlerini işiyor"
|
||||
|
||||
#: worker.py:135
|
||||
#, python-format
|
||||
msgid "%(proc_name)s stopped doing work"
|
||||
msgstr "%(proc_name)s çalışmayı bıraktı"
|
||||
|
||||
#, python-format
|
||||
#~ msgid ""
|
||||
#~ "Could not process '%(func_name)s'. Check the location of the function and "
|
||||
|
||||
@@ -1,682 +0,0 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: django-q\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-04-17 21:10+0000\n"
|
||||
"PO-Revision-Date: 2026-01-14 16:36+0800\n"
|
||||
"Last-Translator: Yibo Li <lybcyd@outlook.com>\n"
|
||||
"Language-Team: Simplified Chinese\n"
|
||||
"Language: zh_Hans\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: \n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
|
||||
#: admin.py:30
|
||||
msgid "Resubmit selected tasks to queue"
|
||||
msgstr "将选定的任务重新提交到队列"
|
||||
|
||||
#: admin.py:64
|
||||
msgid "Time Taken"
|
||||
msgstr ""
|
||||
|
||||
#: admin.py:95
|
||||
msgid "Short result"
|
||||
msgstr ""
|
||||
|
||||
#: admin.py:142 models.py:412
|
||||
#, fuzzy
|
||||
#| msgid "Success"
|
||||
msgid "success"
|
||||
msgstr "成功"
|
||||
|
||||
#: admin.py:154 models.py:414
|
||||
msgid "last_run"
|
||||
msgstr "最后运行"
|
||||
|
||||
#: cluster.py:75
|
||||
#, python-format
|
||||
msgid "Q Cluster %(name)s starting."
|
||||
msgstr "Q 集群 %(name)s 正在启动。"
|
||||
|
||||
#: cluster.py:83
|
||||
#, fuzzy, python-format
|
||||
#| msgid "Q Cluster-{} stopping."
|
||||
msgid "Q Cluster %(name)s stopping."
|
||||
msgstr "Q 集群 %(name)s 正在停止。"
|
||||
|
||||
#: cluster.py:86
|
||||
#, python-format
|
||||
msgid "Q Cluster %(name)s has stopped."
|
||||
msgstr "Q 集群 %(name)s 已停止。"
|
||||
|
||||
#: cluster.py:93
|
||||
#, python-format
|
||||
msgid "%(name)s got signal %(signal)s"
|
||||
msgstr "%(name)s 收到信号 %(signal)s"
|
||||
|
||||
#: cluster.py:224
|
||||
#, python-format
|
||||
msgid "reincarnated monitor %(name)s after sudden death"
|
||||
msgstr "意外宕机后monitor %(name)s 已重生"
|
||||
|
||||
#: cluster.py:230
|
||||
#, python-format
|
||||
msgid "reincarnated pusher %(name)s after sudden death"
|
||||
msgstr "意外宕机后pusher %(name)s 已重生"
|
||||
|
||||
#: cluster.py:262
|
||||
#, fuzzy, python-format
|
||||
#| msgid "reincarnated worker %(name)s after timeout"
|
||||
msgid ""
|
||||
"reincarnated worker %(name)s after timeout while processing task "
|
||||
"%(task_name)s"
|
||||
msgstr "处理任务 %(task_name)s 时超时,worker %(name)s 已重生"
|
||||
|
||||
#: cluster.py:265
|
||||
#, python-format
|
||||
msgid "reincarnated worker %(name)s after timeout"
|
||||
msgstr "超时后worker %(name)s 已重生"
|
||||
|
||||
#: cluster.py:270
|
||||
#, python-format
|
||||
msgid "recycled worker %(name)s"
|
||||
msgstr "worker %(name)s 已回收"
|
||||
|
||||
#: cluster.py:273
|
||||
#, python-format
|
||||
msgid "reincarnated worker %(name)s after death"
|
||||
msgstr "意外宕机后worker %(name)s 已重生"
|
||||
|
||||
#: cluster.py:297
|
||||
#, python-format
|
||||
msgid "%(name)s guarding cluster %(cluster_name)s"
|
||||
msgstr "%(name)s 正在保护Cluster %(cluster_name)s"
|
||||
|
||||
#: cluster.py:307
|
||||
#, python-format
|
||||
msgid "Q Cluster %(cluster_name)s running."
|
||||
msgstr "Q 集群 %(cluster_name)s 正在运行。"
|
||||
|
||||
#: cluster.py:346
|
||||
#, python-format
|
||||
msgid "%(name)s stopping cluster processes"
|
||||
msgstr "%(name)s 正在停止集群进程"
|
||||
|
||||
#: cluster.py:371
|
||||
#, python-format
|
||||
msgid "%(name)s waiting for the monitor."
|
||||
msgstr "%(name)s 正在等待监视器。"
|
||||
|
||||
#: cluster.py:394
|
||||
msgid "Skipping cpu affinity because psutil was not found."
|
||||
msgstr "跳过 CPU 亲和性,因为未找到 psutil。"
|
||||
|
||||
#: cluster.py:399
|
||||
msgid "Faking cpu affinity because it is not supported on this platform"
|
||||
msgstr "伪造 CPU 亲和性,因为此平台不支持此功能"
|
||||
|
||||
#: cluster.py:421
|
||||
#, python-format
|
||||
msgid "%(pid)s will use cpu %(affinity)s"
|
||||
msgstr "%(pid)s 将使用 CPU %(affinity)s"
|
||||
|
||||
#: conf.py:122
|
||||
#, python-format
|
||||
msgid ""
|
||||
"SAVE_LIMIT_PER (%(option)s) is not a valid option. Options are: 'group', "
|
||||
"'name', 'func' and None. Default is None."
|
||||
msgstr ""
|
||||
"SAVE_LIMIT_PER (%(option)s) 不是有效的选项。选项包"
|
||||
"括:'group'、'name'、'func' 和 None。默认值是 None。"
|
||||
|
||||
#. Translators: Cluster status descriptions
|
||||
#: conf.py:239
|
||||
msgid "Starting"
|
||||
msgstr "正在启动"
|
||||
|
||||
#: conf.py:240
|
||||
msgid "Working"
|
||||
msgstr "工作中"
|
||||
|
||||
#: conf.py:241
|
||||
msgid "Idle"
|
||||
msgstr "空闲"
|
||||
|
||||
#: conf.py:242
|
||||
msgid "Stopped"
|
||||
msgstr "已停止"
|
||||
|
||||
#: conf.py:243
|
||||
msgid "Stopping"
|
||||
msgstr "正在停止"
|
||||
|
||||
#. Translators: help text for qcluster management command
|
||||
#: management/commands/qcluster.py:11
|
||||
msgid "Starts a Django Q Cluster."
|
||||
msgstr "启动 Django Q 集群。"
|
||||
|
||||
#. Translators: help text for qinfo management command
|
||||
#: management/commands/qinfo.py:11
|
||||
msgid "General information over all clusters."
|
||||
msgstr "所有集群的常规信息。"
|
||||
|
||||
#. Translators: help text for qmemory management command
|
||||
#: management/commands/qmemory.py:9
|
||||
msgid "Monitors Q Cluster memory usage"
|
||||
msgstr "监视 Q 集群内存使用情况"
|
||||
|
||||
#. Translators: help text for qmonitor management command
|
||||
#: management/commands/qmonitor.py:9
|
||||
msgid "Monitors Q Cluster activity"
|
||||
msgstr "监视 Q 集群活动"
|
||||
|
||||
#: models.py:30 models.py:469
|
||||
msgid "Task id"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:31
|
||||
msgid "32-character task identifier."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:36 models.py:235 models.py:470
|
||||
msgid "Name"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:37
|
||||
msgid "Optional human-readable name for lookup and display."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:41 models.py:240 models.py:468
|
||||
msgid "Function"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:42
|
||||
msgid "Dotted import path to the callable, e.g. myapp.tasks.job."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:47 models.py:247
|
||||
msgid "Hook"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:48
|
||||
msgid "Optional dotted path to a callback that receives the task result."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:53 models.py:253 models.py:472
|
||||
msgid "Arguments"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:54
|
||||
msgid "Pickled positional arguments (tuple)."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:59 models.py:259 models.py:473
|
||||
msgid "Keyword arguments"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:60
|
||||
msgid "Pickled keyword arguments (dict)."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:65
|
||||
msgid "Result"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:66
|
||||
msgid "Pickled return value or error payload."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:72 models.py:471
|
||||
msgid "Group"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:73
|
||||
msgid "Optional group id to aggregate related tasks."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:80 models.py:328
|
||||
#, fuzzy
|
||||
#| msgid "Clusters"
|
||||
msgid "Cluster"
|
||||
msgstr "集群"
|
||||
|
||||
#: models.py:81
|
||||
msgid "Name of the cluster that executed this task."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:85
|
||||
msgid "Started at"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:86
|
||||
msgid "When the worker started executing the task."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:90
|
||||
#, fuzzy
|
||||
#| msgid "Stopped"
|
||||
msgid "Stopped at"
|
||||
msgstr "已停止"
|
||||
|
||||
#: models.py:91
|
||||
msgid "When the worker finished (success or failure)."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:96 monitor_terminal.py:180
|
||||
msgid "Success"
|
||||
msgstr "成功"
|
||||
|
||||
#: models.py:97
|
||||
msgid "False if the task raised an exception or timed out."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:101
|
||||
msgid "Attempt count"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:102
|
||||
msgid "How many times execution was attempted."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:172
|
||||
msgid "Task"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:173
|
||||
msgid "Tasks"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:194
|
||||
msgid "Successful task"
|
||||
msgstr "成功的任务"
|
||||
|
||||
#: models.py:195
|
||||
msgid "Successful tasks"
|
||||
msgstr "成功的任务"
|
||||
|
||||
#: models.py:210
|
||||
msgid "Failed task"
|
||||
msgstr "失败的任务"
|
||||
|
||||
#: models.py:211
|
||||
msgid "Failed tasks"
|
||||
msgstr "失败的任务"
|
||||
|
||||
#: models.py:219 models.py:347
|
||||
msgid "Please install croniter to enable cron expressions"
|
||||
msgstr "请安装 croniter 以启用 cron 表达式"
|
||||
|
||||
#: models.py:236
|
||||
msgid "Optional label to identify this schedule in the admin."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:241
|
||||
msgid "e.g. module.tasks.function"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:248
|
||||
msgid "e.g. module.tasks.result_function"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:254
|
||||
msgid "e.g. 1, 2, 'John'"
|
||||
msgstr "例如 1, 2, 'John'"
|
||||
|
||||
#: models.py:260
|
||||
msgid "e.g. x=1, y=2, name='John'"
|
||||
msgstr "例如 x=1, y=2, name='John'"
|
||||
|
||||
#: models.py:274
|
||||
msgid "Once"
|
||||
msgstr "一次"
|
||||
|
||||
#: models.py:275 models.py:296
|
||||
msgid "Minutes"
|
||||
msgstr "分钟"
|
||||
|
||||
#: models.py:276
|
||||
msgid "Hourly"
|
||||
msgstr "每小时"
|
||||
|
||||
#: models.py:277
|
||||
msgid "Daily"
|
||||
msgstr "每天"
|
||||
|
||||
#: models.py:278
|
||||
msgid "Weekly"
|
||||
msgstr "每周"
|
||||
|
||||
#: models.py:279
|
||||
msgid "Biweekly"
|
||||
msgstr "每两周"
|
||||
|
||||
#: models.py:280
|
||||
msgid "Monthly"
|
||||
msgstr "每月"
|
||||
|
||||
#: models.py:281
|
||||
msgid "Bimonthly"
|
||||
msgstr "每两个月"
|
||||
|
||||
#: models.py:282
|
||||
msgid "Quarterly"
|
||||
msgstr "每季度"
|
||||
|
||||
#: models.py:283
|
||||
msgid "Yearly"
|
||||
msgstr "每年"
|
||||
|
||||
#: models.py:284 models.py:313
|
||||
msgid "Cron"
|
||||
msgstr "Cron"
|
||||
|
||||
#: models.py:290
|
||||
msgid "Schedule Type"
|
||||
msgstr "计划类型"
|
||||
|
||||
#: models.py:291
|
||||
msgid "How often this task should be enqueued."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:297
|
||||
msgid "Number of minutes for the Minutes type"
|
||||
msgstr "'分钟'类型的分钟数"
|
||||
|
||||
#: models.py:300
|
||||
msgid "Repeats"
|
||||
msgstr "重复次数"
|
||||
|
||||
#: models.py:300
|
||||
msgid "n = n times, -1 = forever"
|
||||
msgstr "n = n 次,-1 = 永远"
|
||||
|
||||
#: models.py:303
|
||||
msgid "Next Run"
|
||||
msgstr "下次运行"
|
||||
|
||||
#: models.py:304
|
||||
msgid "When this schedule runs next (stored in UTC)."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:314
|
||||
msgid "Cron expression"
|
||||
msgstr "Cron 表达式"
|
||||
|
||||
#: models.py:320
|
||||
msgid "Last task id"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:321
|
||||
msgid "Id of the last task spawned from this schedule (read-only)."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:329 models.py:427
|
||||
msgid "Name of the target cluster"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:336
|
||||
msgid "Intended date kwarg"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:337
|
||||
msgid "Name of kwarg to pass intended schedule date"
|
||||
msgstr "传递预定计划日期的 kwarg 的名称"
|
||||
|
||||
#: models.py:418
|
||||
msgid "Scheduled task"
|
||||
msgstr "计划任务"
|
||||
|
||||
#: models.py:419
|
||||
msgid "Scheduled tasks"
|
||||
msgstr "计划任务"
|
||||
|
||||
#: models.py:426
|
||||
#, fuzzy
|
||||
#| msgid "Clusters"
|
||||
msgid "Cluster key"
|
||||
msgstr "集群"
|
||||
|
||||
#: models.py:430
|
||||
msgid "Payload"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:431
|
||||
msgid "Signed serialized task package (do not edit manually)."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:435
|
||||
msgid "Lock until"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:436
|
||||
msgid "Prevent any cluster from pulling until"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:474
|
||||
#, fuzzy
|
||||
#| msgid "Queued task"
|
||||
msgid "Queue options"
|
||||
msgstr "排队的任务"
|
||||
|
||||
#: models.py:478
|
||||
msgid "Queued task"
|
||||
msgstr "排队的任务"
|
||||
|
||||
#: models.py:479
|
||||
msgid "Queued tasks"
|
||||
msgstr "排队的任务"
|
||||
|
||||
#: monitor.py:41
|
||||
#, python-format
|
||||
msgid "%(name)s monitoring at %(id)s"
|
||||
msgstr "%(name)s 正在 %(id)s 处监视"
|
||||
|
||||
#: monitor.py:61
|
||||
#, python-format
|
||||
msgid "Processed '%(info_name)s' (%(task_name)s)"
|
||||
msgstr "已处理 '%(info_name)s' (%(task_name)s)"
|
||||
|
||||
#: monitor.py:67
|
||||
#, python-format
|
||||
msgid "Failed '%(info_name)s' (%(task_name)s) - %(task_result)s"
|
||||
msgstr "'%(info_name)s' (%(task_name)s) 失败 - %(task_result)s"
|
||||
|
||||
#: monitor.py:74
|
||||
#, python-format
|
||||
msgid "%(name)s stopped monitoring results"
|
||||
msgstr "%(name)s 已停止监视结果"
|
||||
|
||||
#: monitor_terminal.py:64 monitor_terminal.py:348
|
||||
msgid "Host"
|
||||
msgstr "主机"
|
||||
|
||||
#: monitor_terminal.py:68 monitor_terminal.py:352 monitor_terminal.py:457
|
||||
msgid "Id"
|
||||
msgstr "ID"
|
||||
|
||||
#: monitor_terminal.py:72
|
||||
msgid "State"
|
||||
msgstr "状态"
|
||||
|
||||
#: monitor_terminal.py:76
|
||||
msgid "Pool"
|
||||
msgstr "连接池"
|
||||
|
||||
#: monitor_terminal.py:80
|
||||
msgid "TQ"
|
||||
msgstr "TQ"
|
||||
|
||||
#: monitor_terminal.py:84
|
||||
msgid "RQ"
|
||||
msgstr "RQ"
|
||||
|
||||
#: monitor_terminal.py:88
|
||||
msgid "RC"
|
||||
msgstr "RC"
|
||||
|
||||
#: monitor_terminal.py:92
|
||||
msgid "Up"
|
||||
msgstr "运行中"
|
||||
|
||||
#: monitor_terminal.py:172 monitor_terminal.py:286
|
||||
msgid "Queued"
|
||||
msgstr "已排队"
|
||||
|
||||
#: monitor_terminal.py:190 monitor_terminal.py:294
|
||||
msgid "Failures"
|
||||
msgstr "失败"
|
||||
|
||||
#: monitor_terminal.py:201 monitor_terminal.py:496
|
||||
msgid "[Press q to quit]"
|
||||
msgstr "[按 q 键退出]"
|
||||
|
||||
#: monitor_terminal.py:227
|
||||
msgid "day"
|
||||
msgstr "天"
|
||||
|
||||
#: monitor_terminal.py:248
|
||||
msgid "second"
|
||||
msgstr "秒"
|
||||
|
||||
#: monitor_terminal.py:251
|
||||
msgid "minute"
|
||||
msgstr "分钟"
|
||||
|
||||
#: monitor_terminal.py:254
|
||||
msgid "hour"
|
||||
msgstr "小时"
|
||||
|
||||
#: monitor_terminal.py:263
|
||||
#, python-format
|
||||
msgid "-- %(prefix)s %(version)s on %(info)s --"
|
||||
msgstr "-- %(prefix)s %(version)s 在 %(info)s 上 --"
|
||||
|
||||
#: monitor_terminal.py:273
|
||||
msgid "Clusters"
|
||||
msgstr "集群"
|
||||
|
||||
#: monitor_terminal.py:277
|
||||
msgid "Workers"
|
||||
msgstr "工作进程"
|
||||
|
||||
#: monitor_terminal.py:281
|
||||
msgid "Restarts"
|
||||
msgstr "重启次数"
|
||||
|
||||
#: monitor_terminal.py:290
|
||||
msgid "Successes"
|
||||
msgstr "成功次数"
|
||||
|
||||
#: monitor_terminal.py:299
|
||||
msgid "Schedules"
|
||||
msgstr "计划"
|
||||
|
||||
#: monitor_terminal.py:303
|
||||
#, python-format
|
||||
msgid "Tasks/%(per)s"
|
||||
msgstr "任务/%(per)s"
|
||||
|
||||
#: monitor_terminal.py:307
|
||||
msgid "Avg time"
|
||||
msgstr "平均时间"
|
||||
|
||||
#: monitor_terminal.py:357
|
||||
msgid "Available (%)"
|
||||
msgstr "可用 (%)"
|
||||
|
||||
#: monitor_terminal.py:363
|
||||
msgid "Available (MB)"
|
||||
msgstr "可用 (MB)"
|
||||
|
||||
#: monitor_terminal.py:368
|
||||
msgid "Total (MB)"
|
||||
msgstr "总计 (MB)"
|
||||
|
||||
#: monitor_terminal.py:373
|
||||
msgid "Sentinel (MB)"
|
||||
msgstr "哨兵 (MB)"
|
||||
|
||||
#: monitor_terminal.py:379
|
||||
msgid "Monitor (MB)"
|
||||
msgstr "监视器 (MB)"
|
||||
|
||||
#: monitor_terminal.py:385
|
||||
msgid "Workers (MB)"
|
||||
msgstr "工作进程 (MB)"
|
||||
|
||||
#: monitor_terminal.py:485
|
||||
#, python-format
|
||||
msgid "Available lowest (): %(memory_percent)s ((at)s)"
|
||||
msgstr "最低可用 (): %(memory_percent)s ((at)s)"
|
||||
|
||||
#: monitor_terminal.py:507
|
||||
msgid "No clusters appear to be running."
|
||||
msgstr "似乎没有集群正在运行。"
|
||||
|
||||
#: pusher.py:40
|
||||
#, python-format
|
||||
msgid "%(name)s pushing tasks at %(id)s"
|
||||
msgstr "%(name)s 正在 %(id)s 处推送任务"
|
||||
|
||||
#: pusher.py:67
|
||||
#, python-format
|
||||
msgid "queueing from %(list_key)s"
|
||||
msgstr "从 %(list_key)s 排队"
|
||||
|
||||
#: pusher.py:71
|
||||
#, python-format
|
||||
msgid "%(name)s stopped pushing tasks"
|
||||
msgstr "%(name)s 已停止推送任务"
|
||||
|
||||
#: scheduler.py:110
|
||||
#, python-format
|
||||
msgid "%(process_name)s failed to create a task from schedule [%(schedule)s]"
|
||||
msgstr "%(process_name)s 无法从计划 [%(schedule)s] 创建任务"
|
||||
|
||||
#: scheduler.py:121
|
||||
#, python-format
|
||||
msgid ""
|
||||
"%(process_name)s created task %(task_name)s from schedule [%(schedule)s]"
|
||||
msgstr "%(process_name)s 从计划 [%(schedule)s] 创建了任务 %(task_name)s"
|
||||
|
||||
#: signals.py:22
|
||||
#, python-format
|
||||
msgid "malformed return hook '%(hook)s' for [%(name)s]"
|
||||
msgstr "格式错误的返回钩子 '%(hook)s' 用于 [%(name)s]"
|
||||
|
||||
#: signals.py:30
|
||||
#, python-format
|
||||
msgid "return hook %(hook)s failed on [%(name)s] because %(error)s"
|
||||
msgstr "返回钩子 %(hook)s 在 [%(name)s] 上失败,原因是 %(error)s"
|
||||
|
||||
#: timeout.py:30 timeout.py:43
|
||||
msgid "SIGALARM is not available on your platform"
|
||||
msgstr ""
|
||||
|
||||
#: worker.py:49
|
||||
#, python-format
|
||||
msgid "%(proc_name)s ready for work at %(id)s"
|
||||
msgstr "%(proc_name)s 已准备好在 %(id)s 处工作"
|
||||
|
||||
#: worker.py:69
|
||||
#, fuzzy, python-format
|
||||
#| msgid "%(proc_name)s processing '%(func_name)s' (%(task_name)s)"
|
||||
msgid "%(proc_name)s processing %(task_name)s '%(func_name)s'"
|
||||
msgstr "%(proc_name)s 正在处理任务 %(task_name)s '%(func_name)s'"
|
||||
|
||||
#: worker.py:135
|
||||
#, python-format
|
||||
msgid "%(proc_name)s stopped doing work"
|
||||
msgstr "%(proc_name)s 已停止工作"
|
||||
|
||||
#, python-format
|
||||
#~ msgid ""
|
||||
#~ "Could not process '%(func_name)s'. Check the location of the function and "
|
||||
#~ "the args/kwargs."
|
||||
#~ msgstr "无法处理函数 '%(func_name)s'。检查函数的位置和args/kwargs。"
|
||||
@@ -1,10 +1,8 @@
|
||||
import os
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from django_q.cluster import Cluster
|
||||
import os
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
@@ -25,12 +23,10 @@ class Command(BaseCommand):
|
||||
dest="cluster_name",
|
||||
default=None,
|
||||
help="Set alternative cluster name instead of the name in Q_CLUSTER settings (for multi-queue setup). "
|
||||
"On Linux you should set name through `Q_CLUSTER_NAME=cluster_name python manage.py qcluster` instead.",
|
||||
"On Linux you should set name through `Q_CLUSTER_NAME=cluster_name python manage.py qcluster` instead."
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
# Ensure that the cluster starts only if the SECRET_KEY is set, as it is required for signing.
|
||||
settings.SECRET_KEY
|
||||
# Set alternative cluster_name before creating the cluster (cluster_name is broker's queue_name, too)
|
||||
cluster_name = options.get("cluster_name")
|
||||
if cluster_name:
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
from datetime import timedelta
|
||||
from django_q.brokers import get_broker
|
||||
from django_q.models import Failure, Schedule, Success
|
||||
from django_q.status import Stat
|
||||
from django.db.models import F, Sum
|
||||
from django.db import connection
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.utils.translation import gettext as _
|
||||
from django.utils import timezone
|
||||
|
||||
from django_q import VERSION
|
||||
from django_q.conf import Conf
|
||||
from django_q.monitor_terminal import get_ids, info
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
@@ -28,7 +34,13 @@ class Command(BaseCommand):
|
||||
|
||||
def handle(self, *args, **options):
|
||||
if options.get("ids", True):
|
||||
get_ids()
|
||||
stat = Stat.get_all()
|
||||
if not stat:
|
||||
print(_("No clusters appear to be running."))
|
||||
|
||||
for s in stat:
|
||||
print(s.cluster_id)
|
||||
|
||||
elif options.get("config", False):
|
||||
hide = [
|
||||
"conf",
|
||||
@@ -38,6 +50,7 @@ class Command(BaseCommand):
|
||||
"WORKING",
|
||||
"SIGNAL_NAMES",
|
||||
"STOPPED",
|
||||
"SECRET_KEY",
|
||||
]
|
||||
settings = [
|
||||
a for a in dir(Conf) if not a.startswith("__") and a not in hide
|
||||
@@ -48,4 +61,69 @@ class Command(BaseCommand):
|
||||
if value is not None:
|
||||
self.stdout.write(f"{setting}: {value}")
|
||||
else:
|
||||
info()
|
||||
broker = get_broker()
|
||||
|
||||
broker.ping()
|
||||
|
||||
stats = Stat.get_all(broker=broker)
|
||||
clusters = len(stats)
|
||||
workers = 0
|
||||
reincarnations = 0
|
||||
for cluster in stats:
|
||||
workers += len(cluster.workers)
|
||||
reincarnations += cluster.reincarnations
|
||||
|
||||
# calculate tasks pm and avg exec time
|
||||
tasks_per = 0
|
||||
per = _("day")
|
||||
exec_time = 0
|
||||
last_tasks = Success.objects.filter(
|
||||
stopped__gte=timezone.now() - timedelta(hours=24)
|
||||
)
|
||||
tasks_per_day = last_tasks.count()
|
||||
if tasks_per_day > 0:
|
||||
# average execution time over the last 24 hours
|
||||
if connection.vendor != "sqlite":
|
||||
exec_time = last_tasks.aggregate(
|
||||
time_taken=Sum(F("stopped") - F("started"))
|
||||
)
|
||||
exec_time = exec_time["time_taken"].total_seconds() / tasks_per_day
|
||||
else:
|
||||
# can't sum timedeltas on sqlite
|
||||
for t in last_tasks:
|
||||
exec_time += t.time_taken()
|
||||
exec_time = exec_time / tasks_per_day
|
||||
# tasks per second/minute/hour/day in the last 24 hours
|
||||
if tasks_per_day > 24 * 60 * 60:
|
||||
tasks_per = tasks_per_day / (24 * 60 * 60)
|
||||
per = _("second")
|
||||
elif tasks_per_day > 24 * 60:
|
||||
tasks_per = tasks_per_day / (24 * 60)
|
||||
per = _("minute")
|
||||
elif tasks_per_day > 24:
|
||||
tasks_per = tasks_per_day / 24
|
||||
per = _("hour")
|
||||
else:
|
||||
tasks_per = tasks_per_day
|
||||
|
||||
print(
|
||||
_("-- %(prefix)s %(version)s on %(info)s --")
|
||||
% {
|
||||
"prefix": Conf.PREFIX.capitalize(),
|
||||
"version": ".".join(str(v) for v in VERSION),
|
||||
"info": broker.info(),
|
||||
}
|
||||
)
|
||||
print(_("Clusters: %(clusters)s") % {"clusters": clusters})
|
||||
print(_("Workers: %(workers)s") % {"workers": workers})
|
||||
print(_("Restarts: %(restarts)s") % {"restarts": reincarnations})
|
||||
|
||||
print("")
|
||||
print(_("Queued: %(queue_size)s") % {"queue_size": str(broker.queue_size())})
|
||||
print(_("Successes: %(success_count)s") % {"success_count": str(Success.objects.count())})
|
||||
print(_("Failures: %(failure_count)s") % {"failure_count": str(Failure.objects.count())})
|
||||
|
||||
print("")
|
||||
print(_("Schedules: %(schedules_count)s") % {"schedules_count": str(Schedule.objects.count())})
|
||||
print(_("Tasks/%(per)s: %(amount)s") % {"per": per, "amount": f"{tasks_per:.2f}"})
|
||||
print(_("Avg time: %(time)s") % {"time": f"{exec_time:.4f}"})
|
||||
|
||||
@@ -1,7 +1,19 @@
|
||||
import curses
|
||||
from django_q.conf import Conf
|
||||
import signal
|
||||
|
||||
import time
|
||||
from django_q.status import Stat
|
||||
from django_q.brokers import get_broker
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.utils.translation import gettext as _
|
||||
from django.utils import timezone
|
||||
import curses
|
||||
|
||||
from django_q.monitor_terminal import memory
|
||||
try:
|
||||
import psutil
|
||||
except ImportError:
|
||||
psutil = None
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
@@ -25,7 +37,103 @@ class Command(BaseCommand):
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
memory(
|
||||
memory_stats = MemoryTerminalStats(
|
||||
run_once=options.get("run_once", False),
|
||||
workers=options.get("workers", False),
|
||||
)
|
||||
curses.wrapper(memory_stats.start)
|
||||
|
||||
|
||||
def get_process_mb(pid):
|
||||
try:
|
||||
process = psutil.Process(pid)
|
||||
mb_used = round(process.memory_info().rss / 1024**2, 2)
|
||||
except psutil.NoSuchProcess:
|
||||
mb_used = "NO_PROCESS_FOUND"
|
||||
return mb_used
|
||||
|
||||
|
||||
class MemoryTerminalStats:
|
||||
stop_writing = False
|
||||
|
||||
def __init__(self, run_once=False, workers=False):
|
||||
self.run_once = run_once
|
||||
self.workers = workers
|
||||
|
||||
def start(self, stdscr):
|
||||
self.show_stats()
|
||||
|
||||
def on_exit(self, signum, frame):
|
||||
# exit clean
|
||||
self.stop_writing = True
|
||||
|
||||
def show_stats(self):
|
||||
signal.signal(signal.SIGTERM, self.on_exit)
|
||||
signal.signal(signal.SIGINT, self.on_exit)
|
||||
scr = curses.initscr()
|
||||
|
||||
if not broker:
|
||||
broker = get_broker()
|
||||
|
||||
broker.ping()
|
||||
if not psutil:
|
||||
scr.addstr(0, 0, 'Cannot start "qmemory" command. Missing "psutil" library.')
|
||||
scr.refresh()
|
||||
return
|
||||
|
||||
MEMORY_AVAILABLE_LOWEST_PERCENTAGE = 100.0
|
||||
MEMORY_AVAILABLE_LOWEST_PERCENTAGE_AT = timezone.now()
|
||||
|
||||
stats = Stat.get_all(broker=broker)
|
||||
|
||||
if not stats:
|
||||
scr.addstr(1, 0, "Cluster is not running")
|
||||
scr.refresh()
|
||||
while not self.stop_writing:
|
||||
data = []
|
||||
for stat in stats:
|
||||
# memory available (%)
|
||||
memory_available_percentage = round(
|
||||
psutil.virtual_memory().available
|
||||
* 100
|
||||
/ psutil.virtual_memory().total,
|
||||
2,
|
||||
)
|
||||
# memory available (MB)
|
||||
memory_available = round(
|
||||
psutil.virtual_memory().available / 1024**2, 2
|
||||
)
|
||||
if memory_available_percentage < MEMORY_AVAILABLE_LOWEST_PERCENTAGE:
|
||||
MEMORY_AVAILABLE_LOWEST_PERCENTAGE = memory_available_percentage
|
||||
MEMORY_AVAILABLE_LOWEST_PERCENTAGE_AT = timezone.now()
|
||||
|
||||
data.append(f"Host: {str(stat.host)}")
|
||||
data.append(f"ID: {str(stat.cluster_id)[-8:]}")
|
||||
data.append(f"Available (%): {memory_available_percentage}")
|
||||
data.append(f"Available (MB): {memory_available}")
|
||||
data.append(f"Total (MB): {round(psutil.virtual_memory().total / 1024**2, 2)}")
|
||||
data.append(f"Sentinel (MB): {get_process_mb(stat.sentinel)}")
|
||||
data.append(f"Monitor (MB): {get_process_mb(getattr(stat, 'monitor', None))}")
|
||||
|
||||
if self.workers:
|
||||
data.append("")
|
||||
for worker_num in range(Conf.WORKERS):
|
||||
data.append(f"Worker #{worker_num+1} (MB): {get_process_mb(stat.workers[worker_num])}")
|
||||
|
||||
data.append("")
|
||||
data.append(_("Available lowest: %(memory_percent)s (%(at)s)")
|
||||
% {
|
||||
"memory_percent": str(MEMORY_AVAILABLE_LOWEST_PERCENTAGE),
|
||||
"at": MEMORY_AVAILABLE_LOWEST_PERCENTAGE_AT.strftime(
|
||||
"%Y-%m-%d %H:%M:%S+00:00"
|
||||
),
|
||||
})
|
||||
|
||||
|
||||
for idx, item in enumerate(data):
|
||||
scr.addstr(idx, 0, item)
|
||||
|
||||
scr.refresh()
|
||||
time.sleep(0.5)
|
||||
if self.run_once:
|
||||
return
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import curses
|
||||
from django_q.brokers import get_broker
|
||||
from django_q.models import Failure, Success
|
||||
from django_q.conf import Conf
|
||||
from django_q.status import Stat
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.utils.translation import gettext as _
|
||||
from django.utils import timezone
|
||||
import time
|
||||
|
||||
from django_q.monitor_terminal import monitor
|
||||
import signal
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
@@ -18,4 +25,109 @@ class Command(BaseCommand):
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
monitor(run_once=options.get("run_once", False))
|
||||
memory_stats = MonitorTerminalStats(
|
||||
run_once=options.get("run_once", False),
|
||||
)
|
||||
curses.wrapper(memory_stats.start)
|
||||
|
||||
|
||||
class MonitorTerminalStats:
|
||||
stop_writing = False
|
||||
table_cell_size = 20
|
||||
|
||||
def __init__(self, run_once=False):
|
||||
self.run_once = run_once
|
||||
|
||||
def start(self, stdscr):
|
||||
self.show_stats()
|
||||
|
||||
def on_exit(self, signum, frame):
|
||||
# exit clean
|
||||
self.stop_writing = True
|
||||
|
||||
def get_table_cell(self, data):
|
||||
spaces = self.table_cell_size - len(str(data))
|
||||
return data + " " * spaces + "// "
|
||||
|
||||
def show_stats(self):
|
||||
signal.signal(signal.SIGTERM, self.on_exit)
|
||||
signal.signal(signal.SIGINT, self.on_exit)
|
||||
scr = curses.initscr()
|
||||
|
||||
broker = get_broker()
|
||||
|
||||
broker.ping()
|
||||
|
||||
stats = Stat.get_all(broker=broker)
|
||||
|
||||
if not stats:
|
||||
scr.addstr(1, 0, "Cluster is not running")
|
||||
scr.refresh()
|
||||
|
||||
while not self.stop_writing:
|
||||
data = []
|
||||
table_headers = [
|
||||
_("Host"),
|
||||
_("Id"),
|
||||
_("State"),
|
||||
_("Pool"),
|
||||
_("TQ"),
|
||||
_("RQ"),
|
||||
_("RC"),
|
||||
_("Up"),
|
||||
]
|
||||
|
||||
data.append("".join([self.get_table_cell(header) for header in table_headers]))
|
||||
|
||||
for stat in stats:
|
||||
tasks = str(stat.task_q_size)
|
||||
|
||||
if stat.task_q_size > 0:
|
||||
tasks = str(stat.task_q_size)
|
||||
if Conf.QUEUE_LIMIT and stat.task_q_size == Conf.QUEUE_LIMIT:
|
||||
tasks += " (at maximum size)"
|
||||
results = stat.done_q_size
|
||||
if results > 0:
|
||||
results = str(results)
|
||||
# color workers
|
||||
workers = len(stat.workers)
|
||||
# format uptime
|
||||
uptime = (timezone.now() - stat.tob).total_seconds()
|
||||
hours, remainder = divmod(uptime, 3600)
|
||||
minutes, seconds = divmod(remainder, 60)
|
||||
uptime = "%d:%02d:%02d" % (hours, minutes, seconds)
|
||||
# print to the terminal
|
||||
stat_values = [
|
||||
str(stat.host),
|
||||
str(stat.cluster_id)[-8:],
|
||||
str(stat.status),
|
||||
str(workers),
|
||||
str(tasks),
|
||||
str(results),
|
||||
str(stat.reincarnations),
|
||||
str(uptime),
|
||||
]
|
||||
|
||||
data.append("".join([self.get_table_cell(val) for val in stat_values]))
|
||||
|
||||
data.append("")
|
||||
queue_size = broker.queue_size()
|
||||
lock_size = broker.lock_size()
|
||||
if lock_size:
|
||||
queue_size = f"{queue_size}({lock_size})"
|
||||
|
||||
|
||||
data.append("")
|
||||
data.append(_("info: %(broker_info)s") % {"broker_info": broker.info()})
|
||||
data.append("")
|
||||
data.append(_("Queued: %(queue_size)s") % {"queue_size": str(broker.queue_size())})
|
||||
data.append(_("Successes: %(success_count)s") % {"success_count": str(Success.objects.count())})
|
||||
data.append(_("Failures: %(failure_count)s") % {"failure_count": str(Failure.objects.count())})
|
||||
|
||||
for idx, item in enumerate(data):
|
||||
scr.addstr(idx, 0, item)
|
||||
|
||||
scr.refresh()
|
||||
time.sleep(0.5)
|
||||
if self.run_once:
|
||||
return
|
||||
|
||||
@@ -4,6 +4,7 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = []
|
||||
|
||||
operations = [
|
||||
|
||||
@@ -2,6 +2,7 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("django_q", "0001_initial"),
|
||||
]
|
||||
|
||||
@@ -2,6 +2,7 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("django_q", "0002_auto_20150630_1624"),
|
||||
]
|
||||
|
||||
@@ -2,6 +2,7 @@ from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("django_q", "0003_auto_20150708_1326"),
|
||||
]
|
||||
|
||||
@@ -2,6 +2,7 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("django_q", "0004_auto_20150710_1043"),
|
||||
]
|
||||
|
||||
@@ -2,6 +2,7 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("django_q", "0005_auto_20150718_1506"),
|
||||
]
|
||||
|
||||
@@ -2,6 +2,7 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("django_q", "0006_auto_20150805_1817"),
|
||||
]
|
||||
|
||||
@@ -2,6 +2,7 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("django_q", "0007_ormq"),
|
||||
]
|
||||
|
||||
@@ -2,6 +2,7 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("django_q", "0008_auto_20160224_1026"),
|
||||
]
|
||||
|
||||
@@ -3,6 +3,7 @@ from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("django_q", "0009_auto_20171009_0915"),
|
||||
]
|
||||
|
||||
@@ -4,6 +4,7 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("django_q", "0010_auto_20200610_0856"),
|
||||
]
|
||||
|
||||
@@ -6,6 +6,7 @@ import django_q.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("django_q", "0011_auto_20200628_1055"),
|
||||
]
|
||||
|
||||
@@ -4,6 +4,7 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("django_q", "0012_auto_20200702_1608"),
|
||||
]
|
||||
|
||||
@@ -4,6 +4,7 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("django_q", "0013_task_attempt_count"),
|
||||
]
|
||||
|
||||
@@ -4,6 +4,7 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("django_q", "0014_schedule_cluster"),
|
||||
]
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# Generated by Django 4.1.2 on 2023-01-15 22:34
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
import django_q.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("django_q", "0015_alter_schedule_schedule_type"),
|
||||
]
|
||||
|
||||
@@ -4,6 +4,7 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("django_q", "0016_schedule_intended_date_kwarg"),
|
||||
]
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
# Generated by Django 4.2.7 on 2024-03-05 17:03
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("django_q", "0017_task_cluster_alter"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddIndex(
|
||||
model_name="task",
|
||||
index=models.Index(
|
||||
condition=models.Q(("success", True)),
|
||||
fields=["group", "name", "func"],
|
||||
name="success_index",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -1,325 +0,0 @@
|
||||
# Generated by Django 6.0.4 on 2026-04-17 20:55
|
||||
|
||||
import django.utils.timezone
|
||||
import picklefield.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
import django_q.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("django_q", "0018_task_success_index"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="task",
|
||||
options={
|
||||
"ordering": ["-stopped"],
|
||||
"verbose_name": "Task",
|
||||
"verbose_name_plural": "Tasks",
|
||||
},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="ormq",
|
||||
name="key",
|
||||
field=models.CharField(
|
||||
help_text="Name of the target cluster",
|
||||
max_length=100,
|
||||
verbose_name="Cluster key",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="ormq",
|
||||
name="lock",
|
||||
field=models.DateTimeField(
|
||||
help_text="Prevent any cluster from pulling until",
|
||||
null=True,
|
||||
verbose_name="Lock until",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="ormq",
|
||||
name="payload",
|
||||
field=models.TextField(
|
||||
help_text="Signed serialized task package (do not edit manually).",
|
||||
verbose_name="Payload",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="schedule",
|
||||
name="args",
|
||||
field=models.TextField(
|
||||
blank=True,
|
||||
help_text="e.g. 1, 2, 'John'",
|
||||
null=True,
|
||||
verbose_name="Arguments",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="schedule",
|
||||
name="cluster",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
default=None,
|
||||
help_text="Name of the target cluster",
|
||||
max_length=100,
|
||||
null=True,
|
||||
verbose_name="Cluster",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="schedule",
|
||||
name="cron",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
help_text="Cron expression",
|
||||
max_length=100,
|
||||
null=True,
|
||||
validators=[django_q.models.validate_cron],
|
||||
verbose_name="Cron",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="schedule",
|
||||
name="func",
|
||||
field=models.CharField(
|
||||
help_text="e.g. module.tasks.function",
|
||||
max_length=256,
|
||||
verbose_name="Function",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="schedule",
|
||||
name="hook",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
help_text="e.g. module.tasks.result_function",
|
||||
max_length=256,
|
||||
null=True,
|
||||
verbose_name="Hook",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="schedule",
|
||||
name="intended_date_kwarg",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
help_text="Name of kwarg to pass intended schedule date",
|
||||
max_length=100,
|
||||
null=True,
|
||||
validators=[django_q.models.validate_kwarg],
|
||||
verbose_name="Intended date kwarg",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="schedule",
|
||||
name="kwargs",
|
||||
field=models.TextField(
|
||||
blank=True,
|
||||
help_text="e.g. x=1, y=2, name='John'",
|
||||
null=True,
|
||||
verbose_name="Keyword arguments",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="schedule",
|
||||
name="minutes",
|
||||
field=models.PositiveSmallIntegerField(
|
||||
blank=True,
|
||||
help_text="Number of minutes for the Minutes type",
|
||||
null=True,
|
||||
verbose_name="Minutes",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="schedule",
|
||||
name="name",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
help_text="Optional label to identify this schedule in the admin.",
|
||||
max_length=100,
|
||||
null=True,
|
||||
verbose_name="Name",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="schedule",
|
||||
name="next_run",
|
||||
field=models.DateTimeField(
|
||||
default=django.utils.timezone.now,
|
||||
help_text="When this schedule runs next (stored in UTC).",
|
||||
null=True,
|
||||
verbose_name="Next Run",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="schedule",
|
||||
name="schedule_type",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("O", "Once"),
|
||||
("I", "Minutes"),
|
||||
("H", "Hourly"),
|
||||
("D", "Daily"),
|
||||
("W", "Weekly"),
|
||||
("BW", "Biweekly"),
|
||||
("M", "Monthly"),
|
||||
("BM", "Bimonthly"),
|
||||
("Q", "Quarterly"),
|
||||
("Y", "Yearly"),
|
||||
("C", "Cron"),
|
||||
],
|
||||
default="O",
|
||||
help_text="How often this task should be enqueued.",
|
||||
max_length=2,
|
||||
verbose_name="Schedule Type",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="schedule",
|
||||
name="task",
|
||||
field=models.CharField(
|
||||
editable=False,
|
||||
help_text="Id of the last task spawned from this schedule (read-only).",
|
||||
max_length=100,
|
||||
null=True,
|
||||
verbose_name="Last task id",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="task",
|
||||
name="args",
|
||||
field=picklefield.fields.PickledObjectField(
|
||||
editable=False,
|
||||
help_text="Pickled positional arguments (tuple).",
|
||||
null=True,
|
||||
protocol=-1,
|
||||
verbose_name="Arguments",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="task",
|
||||
name="attempt_count",
|
||||
field=models.IntegerField(
|
||||
default=0,
|
||||
help_text="How many times execution was attempted.",
|
||||
verbose_name="Attempt count",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="task",
|
||||
name="cluster",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
default=None,
|
||||
help_text="Name of the cluster that executed this task.",
|
||||
max_length=100,
|
||||
null=True,
|
||||
verbose_name="Cluster",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="task",
|
||||
name="func",
|
||||
field=models.CharField(
|
||||
help_text="Dotted import path to the callable, e.g. myapp.tasks.job.",
|
||||
max_length=256,
|
||||
verbose_name="Function",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="task",
|
||||
name="group",
|
||||
field=models.CharField(
|
||||
editable=False,
|
||||
help_text="Optional group id to aggregate related tasks.",
|
||||
max_length=100,
|
||||
null=True,
|
||||
verbose_name="Group",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="task",
|
||||
name="hook",
|
||||
field=models.CharField(
|
||||
help_text="Optional dotted path to a callback that receives the task result.",
|
||||
max_length=256,
|
||||
null=True,
|
||||
verbose_name="Hook",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="task",
|
||||
name="id",
|
||||
field=models.CharField(
|
||||
editable=False,
|
||||
help_text="32-character task identifier.",
|
||||
max_length=32,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="Task id",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="task",
|
||||
name="kwargs",
|
||||
field=picklefield.fields.PickledObjectField(
|
||||
editable=False,
|
||||
help_text="Pickled keyword arguments (dict).",
|
||||
null=True,
|
||||
protocol=-1,
|
||||
verbose_name="Keyword arguments",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="task",
|
||||
name="name",
|
||||
field=models.CharField(
|
||||
editable=False,
|
||||
help_text="Optional human-readable name for lookup and display.",
|
||||
max_length=100,
|
||||
verbose_name="Name",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="task",
|
||||
name="result",
|
||||
field=picklefield.fields.PickledObjectField(
|
||||
editable=False,
|
||||
help_text="Pickled return value or error payload.",
|
||||
null=True,
|
||||
protocol=-1,
|
||||
verbose_name="Result",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="task",
|
||||
name="started",
|
||||
field=models.DateTimeField(
|
||||
editable=False,
|
||||
help_text="When the worker started executing the task.",
|
||||
verbose_name="Started at",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="task",
|
||||
name="stopped",
|
||||
field=models.DateTimeField(
|
||||
editable=False,
|
||||
help_text="When the worker finished (success or failure).",
|
||||
verbose_name="Stopped at",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="task",
|
||||
name="success",
|
||||
field=models.BooleanField(
|
||||
default=True,
|
||||
editable=False,
|
||||
help_text="False if the task raised an exception or timed out.",
|
||||
verbose_name="Success",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -1,108 +1,43 @@
|
||||
from datetime import datetime, timedelta
|
||||
from keyword import iskeyword
|
||||
import ast
|
||||
|
||||
# Django
|
||||
from django import get_version
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.template.defaultfilters import truncatechars
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import is_aware
|
||||
from django.utils.html import format_html
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
# External
|
||||
from picklefield import PickledObjectField
|
||||
from picklefield.fields import dbsafe_decode
|
||||
|
||||
# Local
|
||||
from django_q.conf import croniter
|
||||
from django_q.conf import croniter, Conf
|
||||
from django_q.signing import SignedPackage
|
||||
from django_q.utils import add_months, add_years, localtime
|
||||
|
||||
from .utils import get_func_repr
|
||||
from django_q.utils import localtime, add_months, add_years
|
||||
|
||||
|
||||
class Task(models.Model):
|
||||
id = models.CharField(
|
||||
max_length=32,
|
||||
primary_key=True,
|
||||
editable=False,
|
||||
verbose_name=_("Task id"),
|
||||
help_text=_("32-character task identifier."),
|
||||
)
|
||||
name = models.CharField(
|
||||
max_length=100,
|
||||
editable=False,
|
||||
verbose_name=_("Name"),
|
||||
help_text=_("Optional human-readable name for lookup and display."),
|
||||
)
|
||||
func = models.CharField(
|
||||
max_length=256,
|
||||
verbose_name=_("Function"),
|
||||
help_text=_("Dotted import path to the callable, e.g. myapp.tasks.job."),
|
||||
)
|
||||
hook = models.CharField(
|
||||
max_length=256,
|
||||
null=True,
|
||||
verbose_name=_("Hook"),
|
||||
help_text=_(
|
||||
"Optional dotted path to a callback that receives the task result."
|
||||
),
|
||||
)
|
||||
args = PickledObjectField(
|
||||
null=True,
|
||||
protocol=-1,
|
||||
verbose_name=_("Arguments"),
|
||||
help_text=_("Pickled positional arguments (tuple)."),
|
||||
)
|
||||
kwargs = PickledObjectField(
|
||||
null=True,
|
||||
protocol=-1,
|
||||
verbose_name=_("Keyword arguments"),
|
||||
help_text=_("Pickled keyword arguments (dict)."),
|
||||
)
|
||||
result = PickledObjectField(
|
||||
null=True,
|
||||
protocol=-1,
|
||||
verbose_name=_("Result"),
|
||||
help_text=_("Pickled return value or error payload."),
|
||||
)
|
||||
group = models.CharField(
|
||||
max_length=100,
|
||||
editable=False,
|
||||
null=True,
|
||||
verbose_name=_("Group"),
|
||||
help_text=_("Optional group id to aggregate related tasks."),
|
||||
)
|
||||
cluster = models.CharField(
|
||||
max_length=100,
|
||||
default=None,
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name=_("Cluster"),
|
||||
help_text=_("Name of the cluster that executed this task."),
|
||||
)
|
||||
started = models.DateTimeField(
|
||||
editable=False,
|
||||
verbose_name=_("Started at"),
|
||||
help_text=_("When the worker started executing the task."),
|
||||
)
|
||||
stopped = models.DateTimeField(
|
||||
editable=False,
|
||||
verbose_name=_("Stopped at"),
|
||||
help_text=_("When the worker finished (success or failure)."),
|
||||
)
|
||||
success = models.BooleanField(
|
||||
default=True,
|
||||
editable=False,
|
||||
verbose_name=_("Success"),
|
||||
help_text=_("False if the task raised an exception or timed out."),
|
||||
)
|
||||
attempt_count = models.IntegerField(
|
||||
default=0,
|
||||
verbose_name=_("Attempt count"),
|
||||
help_text=_("How many times execution was attempted."),
|
||||
)
|
||||
id = models.CharField(max_length=32, primary_key=True, editable=False)
|
||||
name = models.CharField(max_length=100, editable=False)
|
||||
func = models.CharField(max_length=256)
|
||||
hook = models.CharField(max_length=256, null=True)
|
||||
args = PickledObjectField(null=True, protocol=-1)
|
||||
kwargs = PickledObjectField(null=True, protocol=-1)
|
||||
result = PickledObjectField(null=True, protocol=-1)
|
||||
group = models.CharField(max_length=100, editable=False, null=True)
|
||||
cluster = models.CharField(max_length=100, default=None, null=True, blank=True)
|
||||
started = models.DateTimeField(editable=False)
|
||||
stopped = models.DateTimeField(editable=False)
|
||||
success = models.BooleanField(default=True, editable=False)
|
||||
attempt_count = models.IntegerField(default=0)
|
||||
|
||||
@staticmethod
|
||||
def get_result(task_id):
|
||||
@@ -123,7 +58,7 @@ class Task(models.Model):
|
||||
.exclude(success=False)
|
||||
.values_list("result", flat=True)
|
||||
)
|
||||
return values
|
||||
return decode_results(values)
|
||||
|
||||
def group_result(self, failures=False):
|
||||
if self.group:
|
||||
@@ -166,21 +101,16 @@ class Task(models.Model):
|
||||
def time_taken(self):
|
||||
return (self.stopped - self.started).total_seconds()
|
||||
|
||||
@property
|
||||
def short_result(self):
|
||||
return truncatechars(self.result, 100)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name or self.id}"
|
||||
|
||||
class Meta:
|
||||
app_label = "django_q"
|
||||
verbose_name = _("Task")
|
||||
verbose_name_plural = _("Tasks")
|
||||
ordering = ["-stopped"]
|
||||
indexes = [
|
||||
models.Index(
|
||||
name="success_index",
|
||||
fields=["group", "name", "func"],
|
||||
condition=Q(success=True),
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
class SuccessManager(models.Manager):
|
||||
@@ -230,36 +160,17 @@ def validate_kwarg(value):
|
||||
|
||||
|
||||
class Schedule(models.Model):
|
||||
name = models.CharField(
|
||||
max_length=100,
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name=_("Name"),
|
||||
help_text=_("Optional label to identify this schedule in the admin."),
|
||||
)
|
||||
func = models.CharField(
|
||||
max_length=256,
|
||||
verbose_name=_("Function"),
|
||||
help_text=_("e.g. module.tasks.function"),
|
||||
)
|
||||
name = models.CharField(max_length=100, null=True, blank=True)
|
||||
func = models.CharField(max_length=256, help_text="e.g. module.tasks.function")
|
||||
hook = models.CharField(
|
||||
max_length=256,
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name=_("Hook"),
|
||||
help_text=_("e.g. module.tasks.result_function"),
|
||||
)
|
||||
args = models.TextField(
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name=_("Arguments"),
|
||||
help_text=_("e.g. 1, 2, 'John'"),
|
||||
help_text="e.g. module.tasks.result_function",
|
||||
)
|
||||
args = models.TextField(null=True, blank=True, help_text=_("e.g. 1, 2, 'John'"))
|
||||
kwargs = models.TextField(
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name=_("Keyword arguments"),
|
||||
help_text=_("e.g. x=1, y=2, name='John'"),
|
||||
null=True, blank=True, help_text=_("e.g. x=1, y=2, name='John'")
|
||||
)
|
||||
ONCE = "O"
|
||||
MINUTES = "I"
|
||||
@@ -286,59 +197,65 @@ class Schedule(models.Model):
|
||||
(CRON, _("Cron")),
|
||||
)
|
||||
schedule_type = models.CharField(
|
||||
max_length=2,
|
||||
choices=TYPE,
|
||||
default=TYPE[0][0],
|
||||
verbose_name=_("Schedule Type"),
|
||||
help_text=_("How often this task should be enqueued."),
|
||||
max_length=2, choices=TYPE, default=TYPE[0][0], verbose_name=_("Schedule Type")
|
||||
)
|
||||
minutes = models.PositiveSmallIntegerField(
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name=_("Minutes"),
|
||||
help_text=_("Number of minutes for the Minutes type"),
|
||||
null=True, blank=True, help_text=_("Number of minutes for the Minutes type")
|
||||
)
|
||||
repeats = models.IntegerField(
|
||||
default=-1, verbose_name=_("Repeats"), help_text=_("n = n times, -1 = forever")
|
||||
)
|
||||
next_run = models.DateTimeField(
|
||||
verbose_name=_("Next Run"),
|
||||
help_text=_("When this schedule runs next (stored in UTC)."),
|
||||
default=timezone.now,
|
||||
null=True,
|
||||
verbose_name=_("Next Run"), default=timezone.now, null=True
|
||||
)
|
||||
cron = models.CharField(
|
||||
max_length=100,
|
||||
null=True,
|
||||
blank=True,
|
||||
validators=[validate_cron],
|
||||
verbose_name=_("Cron"),
|
||||
help_text=_("Cron expression"),
|
||||
)
|
||||
task = models.CharField(
|
||||
max_length=100,
|
||||
null=True,
|
||||
editable=False,
|
||||
verbose_name=_("Last task id"),
|
||||
help_text=_("Id of the last task spawned from this schedule (read-only)."),
|
||||
)
|
||||
task = models.CharField(max_length=100, null=True, editable=False)
|
||||
cluster = models.CharField(
|
||||
max_length=100,
|
||||
default=None,
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name=_("Cluster"),
|
||||
help_text=_("Name of the target cluster"),
|
||||
max_length=100, default=None, null=True, blank=True,
|
||||
help_text=_("Name of the target cluster")
|
||||
)
|
||||
intended_date_kwarg = models.CharField(
|
||||
max_length=100,
|
||||
null=True,
|
||||
blank=True,
|
||||
validators=[validate_kwarg],
|
||||
verbose_name=_("Intended date kwarg"),
|
||||
help_text=_("Name of kwarg to pass intended schedule date"),
|
||||
)
|
||||
|
||||
def parse_kwargs(self):
|
||||
if not self.kwargs:
|
||||
return {}
|
||||
try:
|
||||
# first try the dict syntax
|
||||
return ast.literal_eval(self.kwargs)
|
||||
except (SyntaxError, ValueError):
|
||||
# else use the kwargs syntax
|
||||
try:
|
||||
parsed_kwargs = (
|
||||
ast.parse(f"f({self.kwargs})").body[0].value.keywords
|
||||
)
|
||||
return {
|
||||
kwarg.arg: ast.literal_eval(kwarg.value)
|
||||
for kwarg in parsed_kwargs
|
||||
}
|
||||
except (SyntaxError, ValueError):
|
||||
return {}
|
||||
|
||||
def parse_args(self):
|
||||
if not self.args:
|
||||
return tuple()
|
||||
args = ast.literal_eval(self.args)
|
||||
# single value won't eval to tuple, so:
|
||||
if type(args) != tuple:
|
||||
args = (args,)
|
||||
return args
|
||||
|
||||
def calculate_next_run(self, next_run=None):
|
||||
# next run is always in UTC
|
||||
next_run = next_run or self.next_run
|
||||
@@ -398,18 +315,12 @@ class Schedule(models.Model):
|
||||
url = reverse("admin:django_q_success_change", args=(task.id,))
|
||||
else:
|
||||
url = reverse("admin:django_q_failure_change", args=(task.id,))
|
||||
return format_html('<a href="{}">[{}]</a>', url, task.name)
|
||||
return format_html(f'<a href="{url}">[{task.name}]</a>')
|
||||
return None
|
||||
|
||||
def __str__(self):
|
||||
return self.func
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.pk is None and self.schedule_type == self.CRON:
|
||||
self.next_run = self.calculate_next_run()
|
||||
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
success.boolean = True
|
||||
success.short_description = _("success")
|
||||
last_run.allow_tags = True
|
||||
@@ -423,20 +334,10 @@ class Schedule(models.Model):
|
||||
|
||||
|
||||
class OrmQ(models.Model):
|
||||
key = models.CharField(
|
||||
max_length=100,
|
||||
verbose_name=_("Cluster key"),
|
||||
help_text=_("Name of the target cluster"),
|
||||
)
|
||||
payload = models.TextField(
|
||||
verbose_name=_("Payload"),
|
||||
help_text=_("Signed serialized task package (do not edit manually)."),
|
||||
)
|
||||
lock = models.DateTimeField(
|
||||
null=True,
|
||||
verbose_name=_("Lock until"),
|
||||
help_text=_("Prevent any cluster from pulling until"),
|
||||
)
|
||||
key = models.CharField(max_length=100, help_text=_("Name of the target cluster"))
|
||||
payload = models.TextField()
|
||||
lock = models.DateTimeField(null=True, help_text=_("Prevent any cluster from pulling until"))
|
||||
|
||||
|
||||
@cached_property
|
||||
def task(self):
|
||||
@@ -446,16 +347,24 @@ class OrmQ(models.Model):
|
||||
return {"id": "*" + e.__class__.__name__}
|
||||
|
||||
def func(self):
|
||||
return get_func_repr(self.task.get("func"))
|
||||
if isinstance(self.task, dict):
|
||||
return self.task.get("func_name", "")
|
||||
return self.task.func_name
|
||||
|
||||
def task_id(self):
|
||||
return self.task.get("id")
|
||||
if isinstance(self.task, dict):
|
||||
return self.task.get("id", "")
|
||||
return self.task.id
|
||||
|
||||
def name(self):
|
||||
return self.task.get("name")
|
||||
if isinstance(self.task, dict):
|
||||
return self.task["name"]
|
||||
return self.task.name
|
||||
|
||||
def group(self):
|
||||
return self.task.get("group")
|
||||
if isinstance(self.task, dict):
|
||||
return self.task.get("group", "")
|
||||
return self.task.group
|
||||
|
||||
def args(self):
|
||||
return self.task.get("args")
|
||||
@@ -467,15 +376,15 @@ class OrmQ(models.Model):
|
||||
exclude = {"id", "name", "group", "func", "args", "kwargs"}
|
||||
return {k: v for k, v in self.task.items() if k not in exclude}
|
||||
|
||||
func.short_description = _("Function")
|
||||
task_id.short_description = _("Task id")
|
||||
name.short_description = _("Name")
|
||||
group.short_description = _("Group")
|
||||
args.short_description = _("Arguments")
|
||||
kwargs.short_description = _("Keyword arguments")
|
||||
q_options.short_description = _("Queue options")
|
||||
|
||||
class Meta:
|
||||
app_label = "django_q"
|
||||
verbose_name = _("Queued task")
|
||||
verbose_name_plural = _("Queued tasks")
|
||||
|
||||
|
||||
# Backwards compatibility for Django 1.7
|
||||
def decode_results(values):
|
||||
if get_version().split(".")[1] == "7":
|
||||
# decode values in 1.7
|
||||
return [dbsafe_decode(v) for v in values]
|
||||
return values
|
||||
|
||||
@@ -1,24 +1,15 @@
|
||||
from multiprocessing.process import current_process
|
||||
from multiprocessing.queues import Queue
|
||||
|
||||
from django import core, db
|
||||
from django.apps.registry import apps
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
try:
|
||||
apps.check_apps_ready()
|
||||
except core.exceptions.AppRegistryNotReady:
|
||||
import django
|
||||
|
||||
django.setup()
|
||||
|
||||
from django_q.brokers import Broker, get_broker
|
||||
from django_q.conf import Conf, logger, setproctitle
|
||||
from django_q.models import Success, Task
|
||||
from django_q.worker import WorkerProcess
|
||||
from django_q.queue_task import QueueTask
|
||||
from django_q.models import Task
|
||||
from queue import Queue
|
||||
from queue import Empty
|
||||
from typing import Optional, Tuple
|
||||
from django_q.brokers import get_broker
|
||||
from django_q.process_manager import ProcessManager
|
||||
from django_q.signals import post_execute
|
||||
from django_q.signing import SignedPackage
|
||||
from django_q.tasks import async_chain
|
||||
from django_q.utils import close_old_django_connections, get_func_repr
|
||||
from django_q.conf import logger
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from multiprocessing import current_process
|
||||
|
||||
try:
|
||||
import setproctitle
|
||||
@@ -26,187 +17,86 @@ except ModuleNotFoundError:
|
||||
setproctitle = None
|
||||
|
||||
|
||||
def monitor(result_queue: Queue, broker: Broker = None):
|
||||
"""
|
||||
Gets finished tasks from the result queue and saves them to Django
|
||||
:type broker: brokers.Broker
|
||||
:type result_queue: multiprocessing.Queue
|
||||
"""
|
||||
if not broker:
|
||||
broker = get_broker()
|
||||
proc_name = current_process().name
|
||||
if setproctitle:
|
||||
setproctitle.setproctitle(f"qcluster {proc_name} monitor")
|
||||
logger.info(
|
||||
_("%(name)s monitoring at %(id)s")
|
||||
% {"name": proc_name, "id": current_process().pid}
|
||||
)
|
||||
for task in iter(result_queue.get, "STOP"):
|
||||
# save the result
|
||||
if task.get("cached", False):
|
||||
save_cached(task, broker)
|
||||
class Monitor(ProcessManager):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.task_queue = Queue()
|
||||
|
||||
@staticmethod
|
||||
def save_task(task, broker=None) -> Tuple[QueueTask, Optional[Task]]:
|
||||
task_db_obj = None
|
||||
if broker is None:
|
||||
broker = get_broker()
|
||||
if task.cached:
|
||||
task.save_cached(broker)
|
||||
else:
|
||||
save_task(task, broker)
|
||||
print("SAVE TO DB")
|
||||
task_db_obj = task.save_to_db(broker)
|
||||
# acknowledge result
|
||||
ack_id = task.pop("ack_id", False)
|
||||
if ack_id and (task["success"] or task.get("ack_failure", False)):
|
||||
broker.acknowledge(ack_id)
|
||||
if task.ack_id and (task.has_succeeded or not task.ack_failure):
|
||||
broker.acknowledge(task.ack_id)
|
||||
# signal execution done
|
||||
post_execute.send(sender="django_q", task=task)
|
||||
# log the result
|
||||
info_name = get_func_repr(task["func"])
|
||||
if task["success"]:
|
||||
# log success
|
||||
logger.info(
|
||||
_("Processed '%(info_name)s' (%(task_name)s)")
|
||||
% {"info_name": info_name, "task_name": task["name"]}
|
||||
)
|
||||
else:
|
||||
# log failure
|
||||
logger.error(
|
||||
_("Failed '%(info_name)s' (%(task_name)s) - %(task_result)s")
|
||||
% {
|
||||
"info_name": info_name,
|
||||
"task_name": task["name"],
|
||||
"task_result": task["result"],
|
||||
}
|
||||
)
|
||||
logger.info(_("%(name)s stopped monitoring results") % {"name": proc_name})
|
||||
return task, task_db_obj
|
||||
|
||||
|
||||
def save_task(task, broker: Broker):
|
||||
"""
|
||||
Saves the task package to Django or the cache
|
||||
:param task: the task package
|
||||
:type broker: brokers.Broker
|
||||
"""
|
||||
# SAVE LIMIT < 0 : Don't save success
|
||||
if not task.get("save", Conf.SAVE_LIMIT >= 0) and task["success"]:
|
||||
return
|
||||
# enqueues next in a chain
|
||||
if task.get("chain", None):
|
||||
async_chain(
|
||||
task["chain"],
|
||||
group=task["group"],
|
||||
cached=task["cached"],
|
||||
sync=task["sync"],
|
||||
broker=broker,
|
||||
)
|
||||
# SAVE LIMIT > 0: Prune database, SAVE_LIMIT 0: No pruning
|
||||
if not task.get("sync", False):
|
||||
close_old_django_connections()
|
||||
@property
|
||||
def is_done(self):
|
||||
return self.status.value == self.Status.IDLE.value and self.task_queue.empty()
|
||||
|
||||
try:
|
||||
filters = {}
|
||||
if (
|
||||
Conf.SAVE_LIMIT_PER
|
||||
and Conf.SAVE_LIMIT_PER in {"group", "name", "func"}
|
||||
and Conf.SAVE_LIMIT_PER in task
|
||||
):
|
||||
value = task[Conf.SAVE_LIMIT_PER]
|
||||
if Conf.SAVE_LIMIT_PER == "func":
|
||||
value = get_func_repr(value)
|
||||
filters[Conf.SAVE_LIMIT_PER] = value
|
||||
def get_target(self):
|
||||
return self.run_monitor
|
||||
|
||||
# check if we should clean the success tasks
|
||||
if Conf.SAVE_LIMIT > 0:
|
||||
with db.transaction.atomic(using=db.router.db_for_write(Success)):
|
||||
success_tasks_qs = Success.objects.filter(**filters)
|
||||
success_tasks_pks = [
|
||||
success_task.pk
|
||||
for success_task in success_tasks_qs.select_for_update()
|
||||
]
|
||||
if task["success"] and len(success_tasks_pks) >= Conf.SAVE_LIMIT:
|
||||
success_tasks_qs.last().delete()
|
||||
|
||||
# check if this task has previous results
|
||||
try:
|
||||
task_obj = Task.objects.get(id=task["id"], name=task["name"])
|
||||
# only update the result if it hasn't succeeded yet
|
||||
if not task_obj.success:
|
||||
task_obj.stopped = task["stopped"]
|
||||
task_obj.result = task["result"]
|
||||
task_obj.success = task["success"]
|
||||
task_obj.attempt_count = task_obj.attempt_count + 1
|
||||
task_obj.save()
|
||||
|
||||
except Task.DoesNotExist:
|
||||
# convert func to string
|
||||
func = get_func_repr(task["func"])
|
||||
task_obj = Task.objects.create(
|
||||
id=task["id"],
|
||||
name=task["name"],
|
||||
func=func,
|
||||
hook=task.get("hook"),
|
||||
args=task["args"],
|
||||
kwargs=task["kwargs"],
|
||||
cluster=task.get("cluster"),
|
||||
started=task["started"],
|
||||
stopped=task["stopped"],
|
||||
result=task["result"],
|
||||
group=task.get("group"),
|
||||
success=task["success"],
|
||||
attempt_count=1,
|
||||
)
|
||||
|
||||
if (
|
||||
Conf.MAX_ATTEMPTS > 0
|
||||
and task_obj.attempt_count >= Conf.MAX_ATTEMPTS
|
||||
and task.get("ack_id")
|
||||
):
|
||||
broker.acknowledge(task["ack_id"])
|
||||
|
||||
except Exception:
|
||||
logger.exception("Could not save task result")
|
||||
|
||||
|
||||
def save_cached(task, broker: Broker):
|
||||
task_key = f"{broker.list_key}:{task['id']}"
|
||||
timeout = task["cached"]
|
||||
if timeout is True:
|
||||
timeout = None
|
||||
try:
|
||||
group = task.get("group", None)
|
||||
iter_count = task.get("iter_count", 0)
|
||||
# if it's a group append to the group list
|
||||
if group:
|
||||
group_key = f"{broker.list_key}:{group}:keys"
|
||||
group_list = broker.cache.get(group_key) or []
|
||||
# if it's an iter group, check if we are ready
|
||||
if iter_count and len(group_list) == iter_count - 1:
|
||||
group_args = f"{broker.list_key}:{group}:args"
|
||||
# collate the results into a Task result
|
||||
results = [
|
||||
SignedPackage.loads(broker.cache.get(k))["result"]
|
||||
for k in group_list
|
||||
]
|
||||
results.append(task["result"])
|
||||
task["result"] = results
|
||||
task["id"] = group
|
||||
task["args"] = SignedPackage.loads(broker.cache.get(group_args))
|
||||
task.pop("iter_count", None)
|
||||
task.pop("group", None)
|
||||
if task.get("iter_cached", None):
|
||||
task["cached"] = task.pop("iter_cached", None)
|
||||
save_cached(task, broker=broker)
|
||||
else:
|
||||
save_task(task, broker)
|
||||
broker.cache.delete_many(group_list)
|
||||
broker.cache.delete_many([group_key, group_args])
|
||||
def run_item(self):
|
||||
if self.is_idle:
|
||||
try:
|
||||
task = self.task_queue.get_nowait()
|
||||
except Empty:
|
||||
# if the queue is empty, then just stop
|
||||
return
|
||||
# save the group list
|
||||
group_list.append(task_key)
|
||||
broker.cache.set(group_key, group_list, timeout)
|
||||
# async_task next in a chain
|
||||
if task.get("chain", None):
|
||||
async_chain(
|
||||
task["chain"],
|
||||
group=group,
|
||||
cached=task["cached"],
|
||||
sync=task["sync"],
|
||||
broker=broker,
|
||||
try:
|
||||
self.manager_pipe.send(task)
|
||||
except BrokenPipeError:
|
||||
# recycle process if pipe is broken
|
||||
self.status.value = ProcessManager.Status.RECYCLE.value
|
||||
|
||||
def add_task(self, task):
|
||||
self.task_queue.put(task)
|
||||
|
||||
def run_monitor(self, status, pipe) -> None:
|
||||
broker = get_broker()
|
||||
proc_name = current_process().name
|
||||
if setproctitle:
|
||||
setproctitle.setproctitle(f"qcluster {proc_name} monitor")
|
||||
logger.info(
|
||||
_("%(name)s monitoring at %(id)s") % {"name": proc_name, "id": current_process().pid}
|
||||
)
|
||||
# save the task
|
||||
broker.cache.set(task_key, SignedPackage.dumps(task), timeout)
|
||||
except Exception:
|
||||
logger.exception("Could not save task result")
|
||||
status.value = self.Status.IDLE.value
|
||||
|
||||
while True:
|
||||
task = pipe.recv()
|
||||
if task == "STOP":
|
||||
logger.info(f"Monitor {proc_name} shut down")
|
||||
break
|
||||
status.value = self.Status.BUSY.value
|
||||
# save the result
|
||||
task, __ = Monitor.save_task(task, broker=broker)
|
||||
# log the result
|
||||
if task.has_succeeded:
|
||||
# log success
|
||||
logger.info(
|
||||
_("Processed '%(info_name)s' (%(task_name)s)")
|
||||
% {"info_name": task.func_name, "task_name": task.name}
|
||||
)
|
||||
else:
|
||||
# log failure
|
||||
logger.error(
|
||||
_("Failed '%(info_name)s' (%(task_name)s) - %(task_result)s")
|
||||
% {
|
||||
"info_name": task.func_name,
|
||||
"task_name": task.name,
|
||||
"task_result": task.result,
|
||||
}
|
||||
)
|
||||
status.value = self.Status.IDLE.value
|
||||
logger.info(_("%(name)s stopped monitoring results") % {"name": proc_name})
|
||||
|
||||
@@ -1,518 +0,0 @@
|
||||
from datetime import timedelta
|
||||
|
||||
# django
|
||||
from django.db import connection
|
||||
from django.db.models import F, Sum
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from django_q import VERSION, models
|
||||
from django_q.brokers import get_broker
|
||||
|
||||
# local
|
||||
from django_q.conf import Conf
|
||||
from django_q.status import Stat
|
||||
|
||||
# optional
|
||||
try:
|
||||
import psutil
|
||||
except ImportError:
|
||||
psutil = None
|
||||
|
||||
|
||||
def get_process_mb(pid):
|
||||
try:
|
||||
process = psutil.Process(pid)
|
||||
mb_used = round(process.memory_info().rss / 1024**2, 2)
|
||||
except psutil.NoSuchProcess:
|
||||
mb_used = "NO_PROCESS_FOUND"
|
||||
return mb_used
|
||||
|
||||
|
||||
BLESSED_INSTALL_MESSAGE = (
|
||||
"Blessed is not installed. Please install blessed to use this: "
|
||||
"https://pypi.org/project/blessed/"
|
||||
)
|
||||
|
||||
|
||||
def monitor(run_once=False, broker=None):
|
||||
if not broker:
|
||||
broker = get_broker()
|
||||
try:
|
||||
from blessed import Terminal
|
||||
|
||||
term = Terminal()
|
||||
except ImportError:
|
||||
print(BLESSED_INSTALL_MESSAGE)
|
||||
return
|
||||
|
||||
broker.ping()
|
||||
with term.fullscreen(), term.hidden_cursor(), term.cbreak():
|
||||
val = None
|
||||
start_width = int(term.width / 8)
|
||||
while val not in (
|
||||
"q",
|
||||
"Q",
|
||||
):
|
||||
col_width = int(term.width / 8)
|
||||
# In case of resize
|
||||
if col_width != start_width:
|
||||
print(term.clear())
|
||||
start_width = col_width
|
||||
print(
|
||||
term.move(0, 0)
|
||||
+ term.black_on_green(term.center(_("Host"), width=col_width - 1))
|
||||
)
|
||||
print(
|
||||
term.move(0, 1 * col_width)
|
||||
+ term.black_on_green(term.center(_("Id"), width=col_width - 1))
|
||||
)
|
||||
print(
|
||||
term.move(0, 2 * col_width)
|
||||
+ term.black_on_green(term.center(_("State"), width=col_width - 1))
|
||||
)
|
||||
print(
|
||||
term.move(0, 3 * col_width)
|
||||
+ term.black_on_green(term.center(_("Pool"), width=col_width - 1))
|
||||
)
|
||||
print(
|
||||
term.move(0, 4 * col_width)
|
||||
+ term.black_on_green(term.center(_("TQ"), width=col_width - 1))
|
||||
)
|
||||
print(
|
||||
term.move(0, 5 * col_width)
|
||||
+ term.black_on_green(term.center(_("RQ"), width=col_width - 1))
|
||||
)
|
||||
print(
|
||||
term.move(0, 6 * col_width)
|
||||
+ term.black_on_green(term.center(_("RC"), width=col_width - 1))
|
||||
)
|
||||
print(
|
||||
term.move(0, 7 * col_width)
|
||||
+ term.black_on_green(term.center(_("Up"), width=col_width - 1))
|
||||
)
|
||||
i = 2
|
||||
stats = Stat.get_all(broker=broker)
|
||||
print(term.clear_eos())
|
||||
for stat in stats:
|
||||
status = stat.status
|
||||
# color status
|
||||
if stat.status == Conf.STARTING:
|
||||
status = term.yellow(str(Conf.STARTING))
|
||||
elif stat.status == Conf.WORKING:
|
||||
status = term.green(str(Conf.WORKING))
|
||||
elif stat.status == Conf.STOPPING:
|
||||
status = term.yellow(str(Conf.STOPPING))
|
||||
elif stat.status == Conf.STOPPED:
|
||||
status = term.red(str(Conf.STOPPED))
|
||||
elif stat.status == Conf.IDLE:
|
||||
status = str(Conf.IDLE)
|
||||
else:
|
||||
status = _("Unknown")
|
||||
# color q's
|
||||
tasks = str(stat.task_q_size)
|
||||
if stat.task_q_size > 0:
|
||||
tasks = term.cyan(str(stat.task_q_size))
|
||||
if Conf.QUEUE_LIMIT and stat.task_q_size == Conf.QUEUE_LIMIT:
|
||||
tasks = term.green(str(stat.task_q_size))
|
||||
results = stat.done_q_size
|
||||
if results > 0:
|
||||
results = term.cyan(str(results))
|
||||
else:
|
||||
results = str(results)
|
||||
# color workers
|
||||
workers = len(stat.workers)
|
||||
if workers < Conf.WORKERS:
|
||||
workers = term.yellow(str(workers))
|
||||
else:
|
||||
workers = str(workers)
|
||||
# format uptime
|
||||
uptime = (timezone.now() - stat.tob).total_seconds()
|
||||
hours, remainder = divmod(uptime, 3600)
|
||||
minutes, seconds = divmod(remainder, 60)
|
||||
uptime = "%d:%02d:%02d" % (hours, minutes, seconds)
|
||||
# print to the terminal
|
||||
print(
|
||||
term.move(i, 0)
|
||||
+ term.center(stat.host[: col_width - 1], width=col_width - 1)
|
||||
)
|
||||
print(
|
||||
term.move(i, 1 * col_width)
|
||||
+ term.center(str(stat.cluster_id)[-8:], width=col_width - 1)
|
||||
)
|
||||
print(
|
||||
term.move(i, 2 * col_width)
|
||||
+ term.center(status, width=col_width - 1)
|
||||
)
|
||||
print(
|
||||
term.move(i, 3 * col_width)
|
||||
+ term.center(workers, width=col_width - 1)
|
||||
)
|
||||
print(
|
||||
term.move(i, 4 * col_width)
|
||||
+ term.center(tasks, width=col_width - 1)
|
||||
)
|
||||
print(
|
||||
term.move(i, 5 * col_width)
|
||||
+ term.center(results, width=col_width - 1)
|
||||
)
|
||||
print(
|
||||
term.move(i, 6 * col_width)
|
||||
+ term.center(str(stat.reincarnations), width=col_width - 1)
|
||||
)
|
||||
print(
|
||||
term.move(i, 7 * col_width)
|
||||
+ term.center(uptime, width=col_width - 1)
|
||||
)
|
||||
i += 1
|
||||
# bottom bar
|
||||
i += 1
|
||||
queue_size = broker.queue_size()
|
||||
lock_size = broker.lock_size()
|
||||
if lock_size:
|
||||
queue_size = f"{queue_size}({lock_size})"
|
||||
print(
|
||||
term.move(i, 0)
|
||||
+ term.white_on_cyan(term.center(broker.info(), width=col_width * 2))
|
||||
)
|
||||
print(
|
||||
term.move(i, 2 * col_width)
|
||||
+ term.black_on_cyan(term.center(_("Queued"), width=col_width))
|
||||
)
|
||||
print(
|
||||
term.move(i, 3 * col_width)
|
||||
+ term.white_on_cyan(term.center(str(queue_size), width=col_width))
|
||||
)
|
||||
print(
|
||||
term.move(i, 4 * col_width)
|
||||
+ term.black_on_cyan(term.center(_("Success"), width=col_width))
|
||||
)
|
||||
print(
|
||||
term.move(i, 5 * col_width)
|
||||
+ term.white_on_cyan(
|
||||
term.center(str(models.Success.objects.count()), width=col_width)
|
||||
)
|
||||
)
|
||||
print(
|
||||
term.move(i, 6 * col_width)
|
||||
+ term.black_on_cyan(term.center(_("Failures"), width=col_width))
|
||||
)
|
||||
print(
|
||||
term.move(i, 7 * col_width)
|
||||
+ term.white_on_cyan(
|
||||
term.center(str(models.Failure.objects.count()), width=col_width)
|
||||
)
|
||||
)
|
||||
# for testing
|
||||
if run_once:
|
||||
return Stat.get_all(broker=broker)
|
||||
print(term.move(i + 2, 0) + term.center(_("[Press q to quit]")))
|
||||
val = term.inkey(timeout=1)
|
||||
|
||||
|
||||
def info(broker=None):
|
||||
if not broker:
|
||||
broker = get_broker()
|
||||
try:
|
||||
from blessed import Terminal
|
||||
|
||||
term = Terminal()
|
||||
except ImportError:
|
||||
print(BLESSED_INSTALL_MESSAGE)
|
||||
return
|
||||
|
||||
broker.ping()
|
||||
stat = Stat.get_all(broker=broker)
|
||||
# general stats
|
||||
clusters = len(stat)
|
||||
workers = 0
|
||||
reincarnations = 0
|
||||
for cluster in stat:
|
||||
workers += len(cluster.workers)
|
||||
reincarnations += cluster.reincarnations
|
||||
# calculate tasks pm and avg exec time
|
||||
tasks_per = 0
|
||||
per = _("day")
|
||||
exec_time = 0
|
||||
last_tasks = models.Success.objects.filter(
|
||||
stopped__gte=timezone.now() - timedelta(hours=24)
|
||||
)
|
||||
tasks_per_day = last_tasks.count()
|
||||
if tasks_per_day > 0:
|
||||
# average execution time over the last 24 hours
|
||||
if connection.vendor != "sqlite":
|
||||
exec_time = last_tasks.aggregate(
|
||||
time_taken=Sum(F("stopped") - F("started"))
|
||||
)
|
||||
exec_time = exec_time["time_taken"].total_seconds() / tasks_per_day
|
||||
else:
|
||||
# can't sum timedeltas on sqlite
|
||||
for t in last_tasks:
|
||||
exec_time += t.time_taken()
|
||||
exec_time = exec_time / tasks_per_day
|
||||
# tasks per second/minute/hour/day in the last 24 hours
|
||||
if tasks_per_day > 24 * 60 * 60:
|
||||
tasks_per = tasks_per_day / (24 * 60 * 60)
|
||||
per = _("second")
|
||||
elif tasks_per_day > 24 * 60:
|
||||
tasks_per = tasks_per_day / (24 * 60)
|
||||
per = _("minute")
|
||||
elif tasks_per_day > 24:
|
||||
tasks_per = tasks_per_day / 24
|
||||
per = _("hour")
|
||||
else:
|
||||
tasks_per = tasks_per_day
|
||||
# print to terminal
|
||||
print(term.clear_eos())
|
||||
col_width = int(term.width / 6)
|
||||
print(
|
||||
term.black_on_green(
|
||||
term.center(
|
||||
_("-- %(prefix)s %(version)s on %(info)s --")
|
||||
% {
|
||||
"prefix": Conf.PREFIX.capitalize(),
|
||||
"version": ".".join(str(v) for v in VERSION),
|
||||
"info": broker.info(),
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
print(
|
||||
term.cyan(_("Clusters"))
|
||||
+ term.move_x(1 * col_width)
|
||||
+ term.white(str(clusters))
|
||||
+ term.move_x(2 * col_width)
|
||||
+ term.cyan(_("Workers"))
|
||||
+ term.move_x(3 * col_width)
|
||||
+ term.white(str(workers))
|
||||
+ term.move_x(4 * col_width)
|
||||
+ term.cyan(_("Restarts"))
|
||||
+ term.move_x(5 * col_width)
|
||||
+ term.white(str(reincarnations))
|
||||
)
|
||||
print(
|
||||
term.cyan(_("Queued"))
|
||||
+ term.move_x(1 * col_width)
|
||||
+ term.white(str(broker.queue_size()))
|
||||
+ term.move_x(2 * col_width)
|
||||
+ term.cyan(_("Successes"))
|
||||
+ term.move_x(3 * col_width)
|
||||
+ term.white(str(models.Success.objects.count()))
|
||||
+ term.move_x(4 * col_width)
|
||||
+ term.cyan(_("Failures"))
|
||||
+ term.move_x(5 * col_width)
|
||||
+ term.white(str(models.Failure.objects.count()))
|
||||
)
|
||||
print(
|
||||
term.cyan(_("Schedules"))
|
||||
+ term.move_x(1 * col_width)
|
||||
+ term.white(str(models.Schedule.objects.count()))
|
||||
+ term.move_x(2 * col_width)
|
||||
+ term.cyan(_("Tasks/%(per)s") % {"per": per})
|
||||
+ term.move_x(3 * col_width)
|
||||
+ term.white(f"{tasks_per:.2f}")
|
||||
+ term.move_x(4 * col_width)
|
||||
+ term.cyan(_("Avg time"))
|
||||
+ term.move_x(5 * col_width)
|
||||
+ term.white(f"{exec_time:.4f}")
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
def memory(run_once=False, workers=False, broker=None):
|
||||
if not broker:
|
||||
broker = get_broker()
|
||||
try:
|
||||
from blessed import Terminal
|
||||
|
||||
term = Terminal()
|
||||
except ImportError:
|
||||
print(BLESSED_INSTALL_MESSAGE)
|
||||
return
|
||||
broker.ping()
|
||||
if not psutil:
|
||||
print(term.clear_eos())
|
||||
print(
|
||||
term.white_on_red(
|
||||
'Cannot start "qmemory" command. Missing "psutil" library.'
|
||||
)
|
||||
)
|
||||
return
|
||||
with term.fullscreen(), term.hidden_cursor(), term.cbreak():
|
||||
MEMORY_AVAILABLE_LOWEST_PERCENTAGE = 100.0
|
||||
MEMORY_AVAILABLE_LOWEST_PERCENTAGE_AT = timezone.now()
|
||||
cols = 8
|
||||
val = None
|
||||
start_width = int(term.width / cols)
|
||||
while val not in ["q", "Q"]:
|
||||
col_width = int(term.width / cols)
|
||||
# In case of resize
|
||||
if col_width != start_width:
|
||||
print(term.clear())
|
||||
start_width = col_width
|
||||
# sentinel, monitor and workers memory usage
|
||||
print(
|
||||
term.move(0, 0 * col_width)
|
||||
+ term.black_on_green(term.center(_("Host"), width=col_width - 1))
|
||||
)
|
||||
print(
|
||||
term.move(0, 1 * col_width)
|
||||
+ term.black_on_green(term.center(_("Id"), width=col_width - 1))
|
||||
)
|
||||
print(
|
||||
term.move(0, 2 * col_width)
|
||||
+ term.black_on_green(
|
||||
term.center(_("Available (%)"), width=col_width - 1)
|
||||
)
|
||||
)
|
||||
print(
|
||||
term.move(0, 3 * col_width)
|
||||
+ term.black_on_green(
|
||||
term.center(_("Available (MB)"), width=col_width - 1)
|
||||
)
|
||||
)
|
||||
print(
|
||||
term.move(0, 4 * col_width)
|
||||
+ term.black_on_green(term.center(_("Total (MB)"), width=col_width - 1))
|
||||
)
|
||||
print(
|
||||
term.move(0, 5 * col_width)
|
||||
+ term.black_on_green(
|
||||
term.center(_("Sentinel (MB)"), width=col_width - 1)
|
||||
)
|
||||
)
|
||||
print(
|
||||
term.move(0, 6 * col_width)
|
||||
+ term.black_on_green(
|
||||
term.center(_("Monitor (MB)"), width=col_width - 1)
|
||||
)
|
||||
)
|
||||
print(
|
||||
term.move(0, 7 * col_width)
|
||||
+ term.black_on_green(
|
||||
term.center(_("Workers (MB)"), width=col_width - 1)
|
||||
)
|
||||
)
|
||||
row = 2
|
||||
stats = Stat.get_all(broker=broker)
|
||||
print(term.clear_eos())
|
||||
for stat in stats:
|
||||
# memory available (%)
|
||||
memory_available_percentage = round(
|
||||
psutil.virtual_memory().available
|
||||
* 100
|
||||
/ psutil.virtual_memory().total,
|
||||
2,
|
||||
)
|
||||
# memory available (MB)
|
||||
memory_available = round(psutil.virtual_memory().available / 1024**2, 2)
|
||||
if memory_available_percentage < MEMORY_AVAILABLE_LOWEST_PERCENTAGE:
|
||||
MEMORY_AVAILABLE_LOWEST_PERCENTAGE = memory_available_percentage
|
||||
MEMORY_AVAILABLE_LOWEST_PERCENTAGE_AT = timezone.now()
|
||||
print(
|
||||
term.move(row, 0 * col_width)
|
||||
+ term.center(stat.host[: col_width - 1], width=col_width - 1)
|
||||
)
|
||||
print(
|
||||
term.move(row, 1 * col_width)
|
||||
+ term.center(str(stat.cluster_id)[-8:], width=col_width - 1)
|
||||
)
|
||||
print(
|
||||
term.move(row, 2 * col_width)
|
||||
+ term.center(str(memory_available_percentage), width=col_width - 1)
|
||||
)
|
||||
print(
|
||||
term.move(row, 3 * col_width)
|
||||
+ term.center(str(memory_available), width=col_width - 1)
|
||||
)
|
||||
print(
|
||||
term.move(row, 4 * col_width)
|
||||
+ term.center(
|
||||
str(round(psutil.virtual_memory().total / 1024**2, 2)),
|
||||
width=col_width - 1,
|
||||
)
|
||||
)
|
||||
print(
|
||||
term.move(row, 5 * col_width)
|
||||
+ term.center(
|
||||
str(get_process_mb(stat.sentinel)), width=col_width - 1
|
||||
)
|
||||
)
|
||||
print(
|
||||
term.move(row, 6 * col_width)
|
||||
+ term.center(
|
||||
str(get_process_mb(getattr(stat, "monitor", None))),
|
||||
width=col_width - 1,
|
||||
)
|
||||
)
|
||||
workers_mb = 0
|
||||
for worker_pid in stat.workers:
|
||||
result = get_process_mb(worker_pid)
|
||||
if isinstance(result, str):
|
||||
result = 0
|
||||
workers_mb += result
|
||||
print(
|
||||
term.move(row, 7 * col_width)
|
||||
+ term.center(
|
||||
str(workers_mb or "NO_PROCESSES_FOUND"), width=col_width - 1
|
||||
)
|
||||
)
|
||||
row += 1
|
||||
# each worker's memory usage
|
||||
if workers:
|
||||
row += 2
|
||||
col_width = int(term.width / (1 + Conf.WORKERS))
|
||||
print(
|
||||
term.move(row, 0 * col_width)
|
||||
+ term.black_on_cyan(term.center(_("Id"), width=col_width - 1))
|
||||
)
|
||||
for worker_num in range(Conf.WORKERS):
|
||||
print(
|
||||
term.move(row, (worker_num + 1) * col_width)
|
||||
+ term.black_on_cyan(
|
||||
term.center(
|
||||
"Worker #{} (MB)".format(worker_num + 1),
|
||||
width=col_width - 1,
|
||||
)
|
||||
)
|
||||
)
|
||||
row += 2
|
||||
for stat in stats:
|
||||
print(
|
||||
term.move(row, 0 * col_width)
|
||||
+ term.center(str(stat.cluster_id)[-8:], width=col_width - 1)
|
||||
)
|
||||
for idx, worker_pid in enumerate(stat.workers):
|
||||
mb_used = get_process_mb(worker_pid)
|
||||
print(
|
||||
term.move(row, (idx + 1) * col_width)
|
||||
+ term.center(str(mb_used), width=col_width - 1)
|
||||
)
|
||||
row += 1
|
||||
row += 1
|
||||
print(
|
||||
term.move(row, 0)
|
||||
+ _("Available lowest (): %(memory_percent)s ((at)s)")
|
||||
% {
|
||||
"memory_percent": str(MEMORY_AVAILABLE_LOWEST_PERCENTAGE),
|
||||
"at": MEMORY_AVAILABLE_LOWEST_PERCENTAGE_AT.strftime(
|
||||
"%Y-%m-%d %H:%M:%S+00:00"
|
||||
),
|
||||
}
|
||||
)
|
||||
# for testing
|
||||
if run_once:
|
||||
return Stat.get_all(broker=broker)
|
||||
print(term.move(row + 2, 0) + term.center(_("[Press q to quit]")))
|
||||
val = term.inkey(timeout=1)
|
||||
|
||||
|
||||
def get_ids():
|
||||
# prints id (PID) of running clusters
|
||||
stat = Stat.get_all()
|
||||
if stat:
|
||||
for s in stat:
|
||||
print(s.cluster_id)
|
||||
else:
|
||||
print(_("No clusters appear to be running."))
|
||||
return True
|
||||
75
django_q/process_manager.py
Normal file
75
django_q/process_manager.py
Normal file
@@ -0,0 +1,75 @@
|
||||
from abc import ABC
|
||||
from django_q.conf import Conf, logger
|
||||
from django_q.humanhash import humanize
|
||||
import uuid
|
||||
from django_q.conf import Conf
|
||||
import enum
|
||||
from django import db
|
||||
from typing import Callable
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
import multiprocessing
|
||||
from multiprocessing import Process, Value
|
||||
|
||||
class ProcessManager(ABC):
|
||||
class Status(enum.IntEnum):
|
||||
IDLE = 1
|
||||
BUSY = 2
|
||||
DONE = 3
|
||||
RECYCLE = 4
|
||||
|
||||
target = None
|
||||
|
||||
def get_target(self) -> Callable:
|
||||
if self.target is None:
|
||||
raise ValueError("Process must have target specified")
|
||||
return self.target
|
||||
|
||||
def __init__(self):
|
||||
self.status = Value("i", self.Status.IDLE.value)
|
||||
self.process = self.spawn_process()
|
||||
self.name = humanize(uuid.uuid4().hex)
|
||||
|
||||
def spawn_process(self) -> Process:
|
||||
self.manager_pipe, process_pipe = multiprocessing.Pipe(duplex=True)
|
||||
p = Process(target=self.get_target(), args=(self.status, process_pipe))
|
||||
p.start()
|
||||
return p
|
||||
|
||||
def reincarnate_process(self):
|
||||
# kill connections before killing the process
|
||||
logger.critical(_("reincarnated worker %(name)s after death") % {"name": self.process.name})
|
||||
if not Conf.SYNC:
|
||||
db.connections.close_all()
|
||||
self.process.kill()
|
||||
self.process = self.spawn_process()
|
||||
self.mark_idle()
|
||||
|
||||
@property
|
||||
def has_results(self):
|
||||
# poll puller pipe for new tasks
|
||||
return self.manager_pipe.poll()
|
||||
|
||||
def get_result(self):
|
||||
# get the puller pipe task object back from the worker
|
||||
return self.manager_pipe.recv()
|
||||
|
||||
@property
|
||||
def is_alive(self):
|
||||
# get the puller status
|
||||
return self.process.is_alive()
|
||||
|
||||
@property
|
||||
def is_done(self):
|
||||
return self.status.value == self.Status.DONE.value
|
||||
|
||||
@property
|
||||
def is_idle(self):
|
||||
return self.status.value == self.Status.IDLE.value
|
||||
|
||||
@property
|
||||
def is_recycle(self):
|
||||
# worker needs to be recycled/reincarnated
|
||||
return self.status.value == self.Status.RECYCLE.value
|
||||
|
||||
def mark_idle(self):
|
||||
self.status.value = self.Status.IDLE.value
|
||||
85
django_q/puller.py
Normal file
85
django_q/puller.py
Normal file
@@ -0,0 +1,85 @@
|
||||
from django_q.worker import Worker
|
||||
from django_q.signing import BadSignature, SignedPackage
|
||||
from time import sleep
|
||||
from django_q.brokers import get_broker
|
||||
import multiprocessing
|
||||
from django_q.queue_task import QueueTask
|
||||
from django.utils import timezone
|
||||
import enum
|
||||
import traceback
|
||||
|
||||
from multiprocessing import Event, Process, Value, current_process
|
||||
from django_q.utils import close_old_django_connections
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_q.conf import Conf, logger, setproctitle, error_reporter, resource, psutil
|
||||
from django_q.exceptions import TimeoutException, TimeoutHandler
|
||||
from django_q.process_manager import ProcessManager
|
||||
|
||||
|
||||
|
||||
class Puller(ProcessManager):
|
||||
"""The Puller is responsible for pulling the tasks from the broker, then return them to be picked up by the
|
||||
guard"""
|
||||
|
||||
@staticmethod
|
||||
def get_tasks_from_broker(broker=None):
|
||||
queued_tasks = []
|
||||
if broker is None:
|
||||
broker = get_broker()
|
||||
try:
|
||||
task_set = broker.dequeue()
|
||||
except Exception:
|
||||
# broker probably crashed. Let the sentinel handle it.
|
||||
raise ValueError("Failed to pull task from broker")
|
||||
if task_set:
|
||||
logger.info(
|
||||
_("Found %(amount_tasks)s tasks") % {"amount_tasks": len(task_set)}
|
||||
)
|
||||
for task in task_set:
|
||||
print(task)
|
||||
logger.info("ONE TASK")
|
||||
ack_id = task[0]
|
||||
# unpack the task
|
||||
try:
|
||||
queue_task = SignedPackage.loads(task[1])
|
||||
except (TypeError, BadSignature):
|
||||
logger.exception("Failed to pull task from broker - bad task")
|
||||
broker.fail(ack_id)
|
||||
continue
|
||||
queue_task.cluster = Conf.CLUSTER_NAME # save actual cluster name to orm task table
|
||||
queue_task.ack_id = ack_id
|
||||
# send back to main process
|
||||
queued_tasks.append(queue_task)
|
||||
logger.debug(
|
||||
_("queueing from %(list_key)s") % {"list_key": broker.list_key}
|
||||
)
|
||||
return queued_tasks
|
||||
|
||||
def get_target(self):
|
||||
return self.run_puller
|
||||
|
||||
def stop_puller(self):
|
||||
self.status.value = self.Status.DONE.value
|
||||
|
||||
def run_puller(self, status, pipe) -> None:
|
||||
broker = get_broker()
|
||||
proc_name = current_process().name
|
||||
if setproctitle:
|
||||
setproctitle.setproctitle(f"qcluster {proc_name} puller")
|
||||
logger.info(
|
||||
_("%(name)s pulling tasks from broker %(id)s")
|
||||
% {"name": proc_name, "id": current_process().pid}
|
||||
)
|
||||
while True:
|
||||
if status.value == Worker.Status.DONE.value:
|
||||
logger.info("Stopping Puller")
|
||||
break
|
||||
try:
|
||||
queued_tasks = Puller.get_tasks_from_broker(broker=broker)
|
||||
except Exception:
|
||||
logger.exception("Couldn't get items from broker")
|
||||
sleep(10)
|
||||
break
|
||||
for queue_task in queued_tasks:
|
||||
pipe.send(queue_task)
|
||||
logger.info(_("%(name)s stopped pushing tasks") % {"name": current_process().name})
|
||||
@@ -1,71 +0,0 @@
|
||||
from multiprocessing import Event
|
||||
from multiprocessing.process import current_process
|
||||
from multiprocessing.queues import Queue
|
||||
from time import sleep
|
||||
|
||||
from django import core
|
||||
from django.apps.registry import apps
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
try:
|
||||
apps.check_apps_ready()
|
||||
except core.exceptions.AppRegistryNotReady:
|
||||
import django
|
||||
|
||||
django.setup()
|
||||
|
||||
from django_q.brokers import Broker, get_broker
|
||||
from django_q.conf import Conf, logger
|
||||
from django_q.signing import BadSignature, SignedPackage
|
||||
|
||||
try:
|
||||
import setproctitle
|
||||
except ModuleNotFoundError:
|
||||
setproctitle = None
|
||||
|
||||
|
||||
def pusher(task_queue: Queue, event: Event, broker: Broker = None):
|
||||
"""
|
||||
Pulls tasks of the broker and puts them in the task queue
|
||||
:type broker:
|
||||
:type task_queue: multiprocessing.Queue
|
||||
:type event: multiprocessing.Event
|
||||
"""
|
||||
if not broker:
|
||||
broker = get_broker()
|
||||
proc_name = current_process().name
|
||||
if setproctitle:
|
||||
setproctitle.setproctitle(f"qcluster {proc_name} pusher")
|
||||
logger.info(
|
||||
_("%(name)s pushing tasks at %(id)s")
|
||||
% {"name": proc_name, "id": current_process().pid}
|
||||
)
|
||||
while True:
|
||||
try:
|
||||
task_set = broker.dequeue()
|
||||
except Exception:
|
||||
logger.exception("Failed to pull task from broker")
|
||||
# broker probably crashed. Let the sentinel handle it.
|
||||
sleep(10)
|
||||
break
|
||||
if task_set:
|
||||
for task in task_set:
|
||||
ack_id = task[0]
|
||||
# unpack the task
|
||||
try:
|
||||
task = SignedPackage.loads(task[1])
|
||||
except (TypeError, BadSignature):
|
||||
logger.exception("Failed to push task to queue")
|
||||
broker.fail(ack_id)
|
||||
continue
|
||||
task["cluster"] = (
|
||||
Conf.CLUSTER_NAME
|
||||
) # save actual cluster name to orm task table
|
||||
task["ack_id"] = ack_id
|
||||
task_queue.put(task)
|
||||
logger.debug(
|
||||
_("queueing from %(list_key)s") % {"list_key": broker.list_key}
|
||||
)
|
||||
if event.is_set():
|
||||
break
|
||||
logger.info(_("%(name)s stopped pushing tasks") % {"name": current_process().name})
|
||||
211
django_q/queue_task.py
Normal file
211
django_q/queue_task.py
Normal file
@@ -0,0 +1,211 @@
|
||||
from __future__ import annotations
|
||||
from datetime import datetime
|
||||
from django_q.brokers import Broker
|
||||
from django.utils import timezone
|
||||
from django_q.signing import SignedPackage
|
||||
from django_q.models import Success, Task
|
||||
from django_q.utils import close_old_django_connections
|
||||
import enum
|
||||
from django_q import tasks
|
||||
import inspect
|
||||
import pydoc
|
||||
from django import db
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Callable, Optional, Union
|
||||
|
||||
from django_q.conf import Conf, logger
|
||||
|
||||
|
||||
@dataclass
|
||||
class QueueTask:
|
||||
class Status(enum.IntEnum):
|
||||
QUEUED = 0
|
||||
SUCCESS = 1
|
||||
FAILED = 2
|
||||
TIMEOUT = 3
|
||||
|
||||
func: Union[Callable, str]
|
||||
name: str
|
||||
group: Optional[str] = None
|
||||
cluster: str = Conf.CLUSTER_NAME
|
||||
queued_at: Optional[datetime] = timezone.now()
|
||||
finished_at: Optional[datetime] = None
|
||||
ack_id: Optional[str] = None
|
||||
started_at: Optional[datetime] = None
|
||||
id: str = "-1"
|
||||
timeout: Optional[int] = Conf.TIMEOUT
|
||||
status: Optional[Status] = None
|
||||
result: Any = None
|
||||
save: bool = Conf.SAVE_LIMIT >= 0
|
||||
chain: Union[str, QueueTask] = ""
|
||||
cached: bool = Conf.CACHED
|
||||
sync: bool = Conf.SYNC
|
||||
hook: Optional[str] = None
|
||||
args: tuple = field(default_factory=tuple)
|
||||
kwargs: dict = field(default_factory=dict)
|
||||
ack_failure: bool = Conf.ACK_FAILURES
|
||||
iter_count: Optional[int] = None
|
||||
iter_cached: Optional[int] = None
|
||||
|
||||
def callable_func(self):
|
||||
func = self.func
|
||||
if not callable(func):
|
||||
func = pydoc.locate(func)
|
||||
return func
|
||||
|
||||
@property
|
||||
def has_succeeded(self):
|
||||
return self.status == self.Status.SUCCESS
|
||||
|
||||
@property
|
||||
def has_timed_out(self):
|
||||
return self.status == self.Status.TIMEOUT
|
||||
|
||||
@property
|
||||
def is_callable(self):
|
||||
return self.callable_func is not None
|
||||
|
||||
@property
|
||||
def func_name(self):
|
||||
if inspect.isfunction(self.func):
|
||||
return f"{self.func.__module__}.{self.func.__name__}"
|
||||
elif inspect.ismethod(self.func) and hasattr(self.func.__self__, "__name__"):
|
||||
return (
|
||||
f"{self.func.__self__.__module__}." f"{self.func.__self__.__name__}.{self.func.__name__}"
|
||||
)
|
||||
else:
|
||||
return str(self.func)
|
||||
|
||||
def save_to_db(self, broker: Broker):
|
||||
"""
|
||||
Saves the task package to Django or the cache
|
||||
:param task: the task package
|
||||
:type broker: brokers.Broker
|
||||
"""
|
||||
# SAVE LIMIT < 0 : Don't save success
|
||||
if not self.save and self.has_succeeded:
|
||||
return
|
||||
# enqueues next in a chain
|
||||
if self.chain:
|
||||
tasks.async_chain(
|
||||
self.chain,
|
||||
group=self.group,
|
||||
cached=self.cached,
|
||||
sync=self.sync,
|
||||
broker=broker,
|
||||
)
|
||||
close_old_django_connections()
|
||||
|
||||
logger.debug(self.func_name)
|
||||
try:
|
||||
filters = {}
|
||||
if (
|
||||
Conf.SAVE_LIMIT_PER
|
||||
and Conf.SAVE_LIMIT_PER in {"group", "name", "func"}
|
||||
and Conf.SAVE_LIMIT_PER in self
|
||||
):
|
||||
value = getattr(self, Conf.SAVE_LIMIT_PER)
|
||||
if Conf.SAVE_LIMIT_PER == "func":
|
||||
value = self.func_name
|
||||
filters[Conf.SAVE_LIMIT_PER] = value
|
||||
|
||||
with db.transaction.atomic(using=db.router.db_for_write(Success)):
|
||||
last = Success.objects.filter(**filters).select_for_update().last()
|
||||
if (
|
||||
self.has_succeeded
|
||||
and 0 < Conf.SAVE_LIMIT <= Success.objects.filter(**filters).count()
|
||||
):
|
||||
# delete the last entry if we are hitting the limit
|
||||
last.delete()
|
||||
|
||||
# check if this task has previous results
|
||||
existing_task, created = Task.objects.get_or_create(
|
||||
id=self.id,
|
||||
name=self.name,
|
||||
defaults={
|
||||
'func': self.func_name,
|
||||
'stopped': self.finished_at,
|
||||
'hook': self.hook,
|
||||
'args': self.args,
|
||||
'kwargs': self.kwargs,
|
||||
'cluster': self.cluster,
|
||||
'started': self.started_at,
|
||||
'result': self.result,
|
||||
'group': self.group,
|
||||
'success': self.has_succeeded,
|
||||
'attempt_count': 1
|
||||
}
|
||||
)
|
||||
|
||||
# only update the result if it hasn't succeeded yet
|
||||
if not created and not existing_task.success:
|
||||
existing_task.stopped = self.finished_at
|
||||
existing_task.result = self.result
|
||||
existing_task.success = self.has_succeeded
|
||||
existing_task.attempt_count += 1
|
||||
existing_task.save()
|
||||
|
||||
if (
|
||||
Conf.MAX_ATTEMPTS > 0
|
||||
and existing_task.attempt_count >= Conf.MAX_ATTEMPTS
|
||||
):
|
||||
broker.acknowledge(self.ack_id)
|
||||
|
||||
return existing_task
|
||||
|
||||
except Exception:
|
||||
logger.exception("Could not save task result")
|
||||
|
||||
|
||||
def save_cached(self, broker: Broker):
|
||||
task_key = f'{broker.list_key}:{self.id}'
|
||||
timeout = self.cached
|
||||
if timeout is True:
|
||||
timeout = None
|
||||
try:
|
||||
group = self.group
|
||||
iter_count = self.iter_count
|
||||
# if it's a group append to the group list
|
||||
if group:
|
||||
group_key = f"{broker.list_key}:{group}:keys"
|
||||
group_list = broker.cache.get(group_key) or []
|
||||
# if it's an iter group, check if we are ready
|
||||
if iter_count and len(group_list) == iter_count - 1:
|
||||
group_args = f"{broker.list_key}:{group}:args"
|
||||
# collate the results into a Task result
|
||||
results = [
|
||||
SignedPackage.loads(broker.cache.get(k)).result
|
||||
for k in group_list
|
||||
]
|
||||
results.append(self.result)
|
||||
self.result = results
|
||||
self.id = group
|
||||
self.args = SignedPackage.loads(broker.cache.get(group_args))
|
||||
self.iter_count = None
|
||||
self.group = None
|
||||
if self.iter_cached:
|
||||
self.cached = self.iter_cached
|
||||
self.save_cached(broker=broker)
|
||||
else:
|
||||
self.save_to_db(broker)
|
||||
broker.cache.delete_many(group_list)
|
||||
broker.cache.delete_many([group_key, group_args])
|
||||
return
|
||||
# save the group list
|
||||
group_list.append(task_key)
|
||||
broker.cache.set(group_key, group_list, timeout)
|
||||
# async_task next in a chain
|
||||
if self.chain:
|
||||
tasks.async_chain(
|
||||
self.chain,
|
||||
group=group,
|
||||
cached=self.cached,
|
||||
sync=self.sync,
|
||||
broker=broker,
|
||||
)
|
||||
# save the task
|
||||
broker.cache.set(task_key, SignedPackage.dumps(self), timeout)
|
||||
except Exception:
|
||||
logger.exception("Could not save task result")
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
"""
|
||||
The code is derived from
|
||||
https://github.com/althonos/pronto/commit/3384010dfb4fc7c66a219f59276adef3288a886b
|
||||
"""
|
||||
|
||||
import multiprocessing
|
||||
import multiprocessing.queues
|
||||
import sys
|
||||
|
||||
|
||||
class SharedCounter:
|
||||
"""A synchronized shared counter.
|
||||
|
||||
The locking done by multiprocessing.Value ensures that only a single
|
||||
process or thread may read or write the in-memory ctypes object. However,
|
||||
in order to do n += 1, Python performs a read followed by a write, so a
|
||||
second process may read the old value before the new one is written by
|
||||
the first process. The solution is to use a multiprocessing.Lock to
|
||||
guarantee the atomicity of the modifications to Value.
|
||||
|
||||
This class comes almost entirely from Eli Bendersky's blog:
|
||||
http://eli.thegreenplace.net/2012/01/04/shared-counter-with-pythons-multiprocessing/
|
||||
"""
|
||||
|
||||
def __init__(self, n=0):
|
||||
self.count = multiprocessing.Value("i", n)
|
||||
|
||||
def increment(self, n=1):
|
||||
"""Increment the counter by n (default = 1)"""
|
||||
with self.count.get_lock():
|
||||
self.count.value += n
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
"""Return the value of the counter"""
|
||||
return self.count.value
|
||||
|
||||
|
||||
class Queue(multiprocessing.queues.Queue):
|
||||
"""A portable implementation of multiprocessing.Queue.
|
||||
|
||||
Because of multithreading / multiprocessing semantics, Queue.qsize() may
|
||||
raise the NotImplementedError exception on Unix platforms like Mac OS X
|
||||
where sem_getvalue() is not implemented. This subclass addresses this
|
||||
problem by using a synchronized shared counter (initialized to zero) and
|
||||
increasing / decreasing its value every time the put() and get() methods
|
||||
are called, respectively. This not only prevents NotImplementedError from
|
||||
being raised, but also allows us to implement a reliable version of both
|
||||
qsize() and empty().
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
if sys.version_info < (3, 0):
|
||||
super(Queue, self).__init__(*args, **kwargs)
|
||||
else:
|
||||
super(Queue, self).__init__(
|
||||
*args, ctx=multiprocessing.get_context(), **kwargs
|
||||
)
|
||||
self.size = SharedCounter(0)
|
||||
|
||||
def __getstate__(self):
|
||||
return super(Queue, self).__getstate__() + (self.size,)
|
||||
|
||||
def __setstate__(self, state):
|
||||
super(Queue, self).__setstate__(state[:-1])
|
||||
self.size = state[-1]
|
||||
|
||||
def put(self, *args, **kwargs):
|
||||
super(Queue, self).put(*args, **kwargs)
|
||||
self.size.increment(1)
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
x = super(Queue, self).get(*args, **kwargs)
|
||||
self.size.increment(-1)
|
||||
return x
|
||||
|
||||
def qsize(self) -> int:
|
||||
"""Reliable implementation of multiprocessing.Queue.qsize()"""
|
||||
return self.size.value
|
||||
|
||||
def empty(self) -> bool:
|
||||
"""Reliable implementation of multiprocessing.Queue.empty()"""
|
||||
return not self.qsize() > 0
|
||||
@@ -1,72 +1,39 @@
|
||||
import ast
|
||||
from multiprocessing.process import current_process
|
||||
|
||||
from django import core, db
|
||||
from django.apps.registry import apps
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
try:
|
||||
apps.check_apps_ready()
|
||||
except core.exceptions.AppRegistryNotReady:
|
||||
import django
|
||||
|
||||
django.setup()
|
||||
|
||||
from django_q.brokers import Broker, get_broker
|
||||
from django_q.conf import Conf, logger
|
||||
from django_q.humanhash import humanize
|
||||
from django_q.utils import localtime
|
||||
import uuid
|
||||
from django_q.models import Schedule
|
||||
from django_q.tasks import async_task
|
||||
from django_q.utils import close_old_django_connections, localtime
|
||||
from django_q import tasks
|
||||
import ast
|
||||
from django_q.humanhash import humanize
|
||||
from django import db
|
||||
from time import sleep
|
||||
from django_q.brokers import get_broker
|
||||
from django.utils import timezone
|
||||
|
||||
from multiprocessing import Value, current_process
|
||||
from django_q.utils import close_old_django_connections
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_q.conf import Conf, logger
|
||||
from django_q.process_manager import ProcessManager
|
||||
|
||||
|
||||
def scheduler(broker: Broker = None):
|
||||
"""
|
||||
Creates a task from a schedule at the scheduled time and schedules next run
|
||||
"""
|
||||
if not broker:
|
||||
broker = get_broker()
|
||||
close_old_django_connections()
|
||||
try:
|
||||
# Only default cluster will handler schedule with default(null) cluster
|
||||
Q_default = (
|
||||
db.models.Q(cluster__isnull=True)
|
||||
if Conf.CLUSTER_NAME == Conf.PREFIX
|
||||
else db.models.Q(pk__in=[])
|
||||
)
|
||||
|
||||
class Scheduler(ProcessManager):
|
||||
"""The Scheduler is responsible for scheduling new tasks"""
|
||||
|
||||
@staticmethod
|
||||
def schedule_tasks(broker=None):
|
||||
logger.debug("Start sheduling")
|
||||
if broker is None:
|
||||
broker = get_broker()
|
||||
q_default = db.models.Q(cluster__isnull=True) if Conf.CLUSTER_NAME == Conf.PREFIX else db.models.Q(pk__in=[])
|
||||
with db.transaction.atomic(using=db.router.db_for_write(Schedule)):
|
||||
for s in (
|
||||
Schedule.objects.select_for_update()
|
||||
.exclude(repeats=0)
|
||||
.filter(next_run__lt=timezone.now())
|
||||
.filter(Q_default | db.models.Q(cluster=Conf.CLUSTER_NAME))
|
||||
.filter(db.models.Q(next_run__lt=timezone.now()), q_default | db.models.Q(cluster=Conf.CLUSTER_NAME))
|
||||
):
|
||||
args = ()
|
||||
kwargs = {}
|
||||
# get args, kwargs and hook
|
||||
if s.kwargs:
|
||||
try:
|
||||
# first try the dict syntax
|
||||
kwargs = ast.literal_eval(s.kwargs)
|
||||
except (SyntaxError, ValueError):
|
||||
# else use the kwargs syntax
|
||||
try:
|
||||
parsed_kwargs = (
|
||||
ast.parse(f"f({s.kwargs})").body[0].value.keywords
|
||||
)
|
||||
kwargs = {
|
||||
kwarg.arg: ast.literal_eval(kwarg.value)
|
||||
for kwarg in parsed_kwargs
|
||||
}
|
||||
except (SyntaxError, ValueError):
|
||||
kwargs = {}
|
||||
if s.args:
|
||||
args = ast.literal_eval(s.args)
|
||||
# single value won't eval to tuple, so:
|
||||
if type(args) is not tuple:
|
||||
args = (args,)
|
||||
args = s.parse_args()
|
||||
kwargs = s.parse_kwargs()
|
||||
q_options = kwargs.get("q_options", {})
|
||||
if s.intended_date_kwarg:
|
||||
kwargs[s.intended_date_kwarg] = s.next_run.isoformat()
|
||||
@@ -74,35 +41,23 @@ def scheduler(broker: Broker = None):
|
||||
q_options["hook"] = s.hook
|
||||
# set up the next run time
|
||||
if s.schedule_type != s.ONCE:
|
||||
next_run = s.next_run
|
||||
while True:
|
||||
next_run = s.calculate_next_run(next_run)
|
||||
if Conf.CATCH_UP or next_run > localtime():
|
||||
break
|
||||
next_run = s.calculate_next_run(s.next_run)
|
||||
if not Conf.CATCH_UP:
|
||||
while next_run <= localtime():
|
||||
next_run = s.calculate_next_run(next_run)
|
||||
|
||||
s.next_run = next_run
|
||||
|
||||
# Little Fix for already broken numbers
|
||||
if s.repeats < -1:
|
||||
s.repeats = -1
|
||||
|
||||
# Check if the value is not zero
|
||||
if s.repeats > 0:
|
||||
s.repeats -= 1
|
||||
|
||||
s.repeats += -1
|
||||
# send it to the cluster; any cluster name is allowed in multi-queue scenarios
|
||||
# because `broker_name` is confusing, using `cluster` name is recommended and takes precedence
|
||||
q_options["cluster"] = s.cluster or q_options.get(
|
||||
"cluster", q_options.pop("broker_name", None)
|
||||
)
|
||||
if (
|
||||
q_options["cluster"] is None
|
||||
or q_options["cluster"] == Conf.CLUSTER_NAME
|
||||
):
|
||||
# because `broker_name` is confusing, using `cluster` name is recommended and take
|
||||
q_options["cluster"] = s.cluster or q_options.get("cluster", q_options.pop("broker_name", None))
|
||||
if q_options['cluster'] is None or q_options['cluster'] == Conf.CLUSTER_NAME:
|
||||
q_options["broker"] = broker
|
||||
|
||||
q_options["group"] = q_options.get("group", s.name or s.id)
|
||||
kwargs["q_options"] = q_options
|
||||
s.task = async_task(s.func, *args, **kwargs)
|
||||
|
||||
s.task = tasks.async_task(s.func, *args, **kwargs)
|
||||
# log it
|
||||
if not s.task:
|
||||
logger.error(
|
||||
@@ -136,5 +91,35 @@ def scheduler(broker: Broker = None):
|
||||
s.repeats = 0
|
||||
# save the schedule
|
||||
s.save()
|
||||
except Exception:
|
||||
logger.exception("Could not create task from schedule")
|
||||
|
||||
def get_target(self):
|
||||
return self.run_scheduler
|
||||
|
||||
def stop_scheduler(self) -> None:
|
||||
# send stop signal to worker
|
||||
try:
|
||||
self.manager_pipe.send("STOP")
|
||||
except BrokenPipeError:
|
||||
# recycle process if pipe is broken
|
||||
self.status.value = ProcessManager.Status.DONE.value
|
||||
|
||||
def run_scheduler(self, status, pipe) -> None:
|
||||
self.process_name = current_process().name
|
||||
self.process_id = current_process().pid
|
||||
status.value = self.Status.BUSY.value
|
||||
logger.info(
|
||||
_("%(proc_name)s scheduling at %(id)s")
|
||||
% {"proc_name": self.process_name, "id": self.process_id}
|
||||
)
|
||||
while True:
|
||||
if pipe.poll() and pipe.recv() == "STOP":
|
||||
status.value = self.Status.DONE.value
|
||||
break
|
||||
broker = get_broker()
|
||||
close_old_django_connections()
|
||||
try:
|
||||
Scheduler.schedule_tasks(broker=broker)
|
||||
except Exception:
|
||||
logger.exception("Could not create task from schedule")
|
||||
# sleep 60 seconds for next schedule
|
||||
sleep(60)
|
||||
|
||||
@@ -32,9 +32,6 @@ def call_hook(sender, instance, **kwargs):
|
||||
)
|
||||
|
||||
|
||||
# args: proc_name
|
||||
post_spawn = Signal()
|
||||
|
||||
# args: task
|
||||
pre_enqueue = Signal()
|
||||
|
||||
@@ -43,6 +40,3 @@ pre_execute = Signal()
|
||||
|
||||
# args: task
|
||||
post_execute = Signal()
|
||||
|
||||
# args: func, task
|
||||
post_execute_in_worker = Signal()
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
"""Package signing."""
|
||||
|
||||
import pickle
|
||||
|
||||
from django_q import core_signing as signing
|
||||
|
||||
@@ -41,14 +41,7 @@ class Stat(Status):
|
||||
self.status = sentinel.status()
|
||||
self.done_q_size = 0
|
||||
self.task_q_size = 0
|
||||
if Conf.QSIZE:
|
||||
self.done_q_size = sentinel.result_queue.qsize()
|
||||
self.task_q_size = sentinel.task_queue.qsize()
|
||||
if sentinel.monitor:
|
||||
self.monitor = sentinel.monitor.pid
|
||||
if sentinel.pusher:
|
||||
self.pusher = sentinel.pusher.pid
|
||||
self.workers = [w.pid for w in sentinel.pool]
|
||||
self.workers = [w.process.pid for w in sentinel.pool.workers]
|
||||
|
||||
def uptime(self) -> float:
|
||||
return (timezone.now() - self.tob).total_seconds()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Provides task functionality."""
|
||||
|
||||
# Standard
|
||||
from django_q.helpers import run_cluster_once
|
||||
from django_q.queue_task import QueueTask
|
||||
from multiprocessing import Value
|
||||
from time import sleep, time
|
||||
|
||||
@@ -13,14 +14,13 @@ from django_q.brokers import get_broker
|
||||
from django_q.conf import Conf, logger
|
||||
from django_q.humanhash import uuid
|
||||
from django_q.models import Schedule, Task
|
||||
from django_q.queues import Queue
|
||||
from django_q.signals import pre_enqueue
|
||||
from django_q.signing import SignedPackage
|
||||
|
||||
|
||||
def async_task(func, *args, **kwargs):
|
||||
"""Queue a task for the cluster."""
|
||||
keywords = kwargs.copy()
|
||||
given_kwargs = kwargs.copy()
|
||||
opt_keys = (
|
||||
"hook",
|
||||
"group",
|
||||
@@ -35,47 +35,40 @@ def async_task(func, *args, **kwargs):
|
||||
"cluster",
|
||||
"timeout",
|
||||
)
|
||||
q_options = keywords.pop("q_options", {})
|
||||
q_options = given_kwargs.pop("q_options", {})
|
||||
# get an id
|
||||
tag = uuid()
|
||||
# build the task package
|
||||
task = {
|
||||
"id": tag[1],
|
||||
"name": keywords.pop("task_name", None)
|
||||
or q_options.pop("task_name", None)
|
||||
or tag[0],
|
||||
"func": func,
|
||||
"args": args,
|
||||
}
|
||||
task = QueueTask(
|
||||
id=tag[1],
|
||||
name=given_kwargs.pop("task_name", None) or q_options.pop("task_name", None) or tag[0],
|
||||
func=func,
|
||||
args=args
|
||||
)
|
||||
|
||||
# don't serialize the broker
|
||||
broker = given_kwargs.pop("broker", None) or q_options.pop("broker", None) or get_broker(task.cluster) or get_broker()
|
||||
|
||||
print(broker.list_key)
|
||||
|
||||
# push optionals
|
||||
for key in opt_keys:
|
||||
if q_options and key in q_options:
|
||||
task[key] = q_options[key]
|
||||
elif key in keywords:
|
||||
task[key] = keywords.pop(key)
|
||||
# don't serialize the broker
|
||||
broker = task.pop("broker", None) or get_broker(task.get("cluster"))
|
||||
# overrides
|
||||
if "cached" not in task and Conf.CACHED:
|
||||
task["cached"] = Conf.CACHED
|
||||
if "sync" not in task and Conf.SYNC:
|
||||
task["sync"] = Conf.SYNC
|
||||
if "ack_failure" not in task and Conf.ACK_FAILURES:
|
||||
task["ack_failure"] = Conf.ACK_FAILURES
|
||||
if key in q_options or key in given_kwargs:
|
||||
setattr(task, key, q_options.pop(key, None) or given_kwargs.pop(key, None))
|
||||
|
||||
# finalize
|
||||
task["kwargs"] = keywords
|
||||
task["started"] = timezone.now()
|
||||
task.kwargs = given_kwargs
|
||||
# signal it
|
||||
pre_enqueue.send(sender="django_q", task=task)
|
||||
# sign it
|
||||
pack = SignedPackage.dumps(task)
|
||||
if task.get("sync", False):
|
||||
if task.sync:
|
||||
return _sync(pack)
|
||||
# push it
|
||||
enqueue_id = broker.enqueue(pack)
|
||||
logger.info(f"Enqueued [{broker.list_key}] {enqueue_id}")
|
||||
logger.debug(f"Pushed {tag}")
|
||||
return task["id"]
|
||||
return task.id
|
||||
|
||||
|
||||
def schedule(func, *args, **kwargs):
|
||||
@@ -112,7 +105,7 @@ def schedule(func, *args, **kwargs):
|
||||
raise IntegrityError("A schedule with the same name already exists.")
|
||||
|
||||
# create and return the schedule
|
||||
s = Schedule(
|
||||
schedule = Schedule(
|
||||
name=name,
|
||||
func=func,
|
||||
hook=hook,
|
||||
@@ -126,11 +119,9 @@ def schedule(func, *args, **kwargs):
|
||||
cluster=cluster,
|
||||
intended_date_kwarg=intended_date_kwarg,
|
||||
)
|
||||
# make sure we trigger validation
|
||||
s.full_clean()
|
||||
s.save()
|
||||
return s
|
||||
|
||||
schedule.full_clean()
|
||||
schedule.save()
|
||||
return schedule
|
||||
|
||||
def result(task_id, wait=0, cached=Conf.CACHED):
|
||||
"""
|
||||
@@ -149,7 +140,7 @@ def result(task_id, wait=0, cached=Conf.CACHED):
|
||||
start = time()
|
||||
while True:
|
||||
r = Task.get_result(task_id)
|
||||
if r is not None:
|
||||
if r:
|
||||
return r
|
||||
if (time() - start) * 1000 >= wait >= 0:
|
||||
break
|
||||
@@ -166,7 +157,7 @@ def result_cached(task_id, wait=0, broker=None):
|
||||
while True:
|
||||
r = broker.cache.get(f"{broker.list_key}:{task_id}")
|
||||
if r:
|
||||
return SignedPackage.loads(r)["result"]
|
||||
return SignedPackage.loads(r).result
|
||||
if (time() - start) * 1000 >= wait >= 0:
|
||||
break
|
||||
sleep(0.01)
|
||||
@@ -225,8 +216,8 @@ def result_group_cached(group_id, failures=False, wait=0, count=None, broker=Non
|
||||
result_list = []
|
||||
for task_key in group_list:
|
||||
task = SignedPackage.loads(broker.cache.get(task_key))
|
||||
if task["success"] or failures:
|
||||
result_list.append(task["result"])
|
||||
if task.has_succeeded or failures:
|
||||
result_list.append(task.result)
|
||||
return result_list
|
||||
if (time() - start) * 1000 >= wait >= 0:
|
||||
break
|
||||
@@ -269,17 +260,17 @@ def fetch_cached(task_id, wait=0, broker=None):
|
||||
if r:
|
||||
task = SignedPackage.loads(r)
|
||||
return Task(
|
||||
id=task["id"],
|
||||
name=task["name"],
|
||||
func=task["func"],
|
||||
hook=task.get("hook"),
|
||||
args=task["args"],
|
||||
kwargs=task["kwargs"],
|
||||
cluster=task.get("cluster"),
|
||||
started=task["started"],
|
||||
stopped=task["stopped"],
|
||||
result=task["result"],
|
||||
success=task["success"],
|
||||
id=task.id,
|
||||
name=task.name,
|
||||
func=task.func,
|
||||
hook=task.hook,
|
||||
args=task.args,
|
||||
kwargs=task.kwargs,
|
||||
cluster=task.cluster,
|
||||
started=task.started_at,
|
||||
stopped=task.finished_at,
|
||||
result=task.result,
|
||||
success=task.has_succeeded,
|
||||
)
|
||||
if (time() - start) * 1000 >= wait >= 0:
|
||||
break
|
||||
@@ -338,20 +329,20 @@ def fetch_group_cached(group_id, failures=True, wait=0, count=None, broker=None)
|
||||
task_list = []
|
||||
for task_key in group_list:
|
||||
task = SignedPackage.loads(broker.cache.get(task_key))
|
||||
if task["success"] or failures:
|
||||
if task.has_succeeded or failures:
|
||||
t = Task(
|
||||
id=task["id"],
|
||||
name=task["name"],
|
||||
func=task["func"],
|
||||
hook=task.get("hook"),
|
||||
args=task["args"],
|
||||
kwargs=task["kwargs"],
|
||||
cluster=task.get("cluster"),
|
||||
started=task["started"],
|
||||
stopped=task["stopped"],
|
||||
result=task["result"],
|
||||
group=task.get("group"),
|
||||
success=task["success"],
|
||||
id=task.id,
|
||||
name=task.name,
|
||||
func=task.func,
|
||||
hook=task.hook,
|
||||
args=task.args,
|
||||
kwargs=task.kwargs,
|
||||
cluster=task.cluster,
|
||||
started=task.started_at,
|
||||
stopped=task.finished_at,
|
||||
result=task.result,
|
||||
group=task.group,
|
||||
success=task.has_succeeded,
|
||||
)
|
||||
task_list.append(t)
|
||||
return task_list
|
||||
@@ -388,7 +379,7 @@ def count_group_cached(group_id, failures=False, broker=None):
|
||||
failure_count = 0
|
||||
for task_key in group_list:
|
||||
task = SignedPackage.loads(broker.cache.get(task_key))
|
||||
if not task["success"]:
|
||||
if not task.has_succeeded:
|
||||
failure_count += 1
|
||||
return failure_count
|
||||
|
||||
@@ -461,9 +452,7 @@ def async_iter(func, args_iter, **kwargs):
|
||||
# save the original arguments
|
||||
broker = options["broker"]
|
||||
broker.cache.set(
|
||||
f"{broker.list_key}:{iter_group}:args",
|
||||
SignedPackage.dumps(args_iter),
|
||||
timeout=None,
|
||||
f"{broker.list_key}:{iter_group}:args", SignedPackage.dumps(args_iter)
|
||||
)
|
||||
for args in args_iter:
|
||||
if not isinstance(args, tuple):
|
||||
@@ -572,12 +561,10 @@ class Chain:
|
||||
A sequential chain of tasks
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, chain=None, group=None, cached=Conf.CACHED, sync=Conf.SYNC, broker=None
|
||||
):
|
||||
def __init__(self, chain=None, group=None, cached=Conf.CACHED, sync=Conf.SYNC):
|
||||
self.chain = chain or []
|
||||
self.group = group or ""
|
||||
self.broker = broker or get_broker()
|
||||
self.broker = get_broker()
|
||||
self.cached = cached
|
||||
self.sync = sync
|
||||
self.started = False
|
||||
@@ -734,14 +721,17 @@ class AsyncTask:
|
||||
return self.id
|
||||
|
||||
def result(self, wait=0):
|
||||
|
||||
if self.started:
|
||||
return result(self.id, wait=wait, cached=self.cached)
|
||||
|
||||
def fetch(self, wait=0):
|
||||
|
||||
if self.started:
|
||||
return fetch(self.id, wait=wait, cached=self.cached)
|
||||
|
||||
def result_group(self, failures=False, wait=0, count=None):
|
||||
|
||||
if self.started and self.group:
|
||||
return result_group(
|
||||
self.group,
|
||||
@@ -752,6 +742,7 @@ class AsyncTask:
|
||||
)
|
||||
|
||||
def fetch_group(self, failures=True, wait=0, count=None):
|
||||
|
||||
if self.started and self.group:
|
||||
return fetch_group(
|
||||
self.group,
|
||||
@@ -764,19 +755,7 @@ class AsyncTask:
|
||||
|
||||
def _sync(pack):
|
||||
"""Simulate a package travelling through the cluster."""
|
||||
from django_q.monitor import monitor
|
||||
from django_q.worker import worker
|
||||
|
||||
task_queue = Queue()
|
||||
result_queue = Queue()
|
||||
task = SignedPackage.loads(pack)
|
||||
task_queue.put(task)
|
||||
task_queue.put("STOP")
|
||||
worker(task_queue, result_queue, Value("f", -1))
|
||||
result_queue.put("STOP")
|
||||
monitor(result_queue)
|
||||
task_queue.close()
|
||||
task_queue.join_thread()
|
||||
result_queue.close()
|
||||
result_queue.join_thread()
|
||||
return task["id"]
|
||||
run_cluster_once(workers=1, tasks=[task])
|
||||
|
||||
return task.id
|
||||
|
||||
@@ -116,7 +116,7 @@ CACHES = {
|
||||
"LOCATION": f"redis://{REDIS_HOST}:6379/0",
|
||||
"OPTIONS": {
|
||||
"CLIENT_CLASS": "django_redis.client.DefaultClient",
|
||||
"PARSER_CLASS": "redis.connection.DefaultParser",
|
||||
"PARSER_CLASS": "redis.connection.HiredisParser",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,10 +46,6 @@ def hello():
|
||||
return "hello"
|
||||
|
||||
|
||||
def return_falsy_value():
|
||||
return []
|
||||
|
||||
|
||||
def result(obj):
|
||||
print(f"RESULT HOOK {obj.name} : {obj.result()}")
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ def test_admin_views(admin_client, monkeypatch):
|
||||
|
||||
# resubmit the failure
|
||||
url = reverse("admin:django_q_failure_changelist")
|
||||
data = {"action": "resubmit_task", "_selected_action": [f.pk]}
|
||||
data = {"action": "retry_failed", "_selected_action": [f.pk]}
|
||||
response = admin_client.post(url, data)
|
||||
assert response.status_code == 302
|
||||
assert Failure.objects.filter(name=f.id).exists() is False
|
||||
@@ -84,10 +84,3 @@ def test_admin_views(admin_client, monkeypatch):
|
||||
data = {"post": "yes"}
|
||||
response = admin_client.post(url, data)
|
||||
assert response.status_code == 302
|
||||
# Resubmit a successful task.
|
||||
url = reverse("admin:django_q_success_changelist")
|
||||
data = {"action": "resubmit_task", "_selected_action": [t.pk]}
|
||||
initial_queue_count = OrmQ.objects.count()
|
||||
response = admin_client.post(url, data)
|
||||
assert response.status_code == 302
|
||||
assert OrmQ.objects.count() > initial_queue_count
|
||||
|
||||
@@ -35,57 +35,6 @@ def test_broker(monkeypatch):
|
||||
assert broker.get_stats("test:*") is None
|
||||
|
||||
|
||||
def test_broker_set_stat_prunes_stale_master_list():
|
||||
# regression: set_stat should prune stale master-list entries on register
|
||||
broker = Broker()
|
||||
a_key = f"{Conf.Q_STAT}:A"
|
||||
b_key = f"{Conf.Q_STAT}:B"
|
||||
broker.cache.delete(Conf.Q_STAT)
|
||||
|
||||
broker.set_stat(a_key, "state_a", 3)
|
||||
assert a_key in broker.cache.get(Conf.Q_STAT)
|
||||
|
||||
# Drop A's per-stat value to simulate a dead cluster whose TTL expired,
|
||||
# leaving the master-list entry as a stale reference.
|
||||
broker.cache.delete(a_key)
|
||||
assert broker.get_stat(a_key) is None
|
||||
assert a_key in broker.cache.get(Conf.Q_STAT)
|
||||
|
||||
# Registering B should prune stale A from the master list.
|
||||
broker.set_stat(b_key, "state_b", 3)
|
||||
key_list = broker.cache.get(Conf.Q_STAT)
|
||||
assert a_key not in key_list
|
||||
assert b_key in key_list
|
||||
assert broker.get_stat(b_key) == "state_b"
|
||||
|
||||
|
||||
def test_broker_set_stat_skips_master_list_write_on_repeat(monkeypatch):
|
||||
# set_stat should only write the master list when membership changes
|
||||
broker = Broker()
|
||||
a_key = f"{Conf.Q_STAT}:A"
|
||||
broker.cache.delete(Conf.Q_STAT)
|
||||
|
||||
writes = []
|
||||
orig_set = broker.cache.set
|
||||
|
||||
def counting_set(key, value, timeout=None, **kw):
|
||||
if key == Conf.Q_STAT:
|
||||
writes.append(key)
|
||||
return orig_set(key, value, timeout, **kw)
|
||||
|
||||
monkeypatch.setattr(broker.cache, "set", counting_set)
|
||||
|
||||
# First call adds a new entry, one master-list write expected
|
||||
broker.set_stat(a_key, "state_a", 3)
|
||||
assert len(writes) == 1
|
||||
|
||||
# Subsequent calls for the same key must not rewrite the master list
|
||||
broker.set_stat(a_key, "state_a", 3)
|
||||
broker.set_stat(a_key, "state_a", 3)
|
||||
broker.set_stat(a_key, "state_a", 3)
|
||||
assert len(writes) == 1
|
||||
|
||||
|
||||
def test_redis(monkeypatch):
|
||||
monkeypatch.setattr(Conf, "DJANGO_REDIS", None)
|
||||
broker = get_broker()
|
||||
@@ -102,7 +51,7 @@ def test_redis(monkeypatch):
|
||||
|
||||
|
||||
def test_custom(monkeypatch):
|
||||
monkeypatch.setattr(Conf, "BROKER_CLASS", "django_q.brokers.redis_broker.Redis")
|
||||
monkeypatch.setattr(Conf, "BROKER_CLASS", "brokers.redis_broker.Redis")
|
||||
broker = get_broker()
|
||||
assert broker.ping() is True
|
||||
assert broker.info() is not None
|
||||
@@ -175,7 +124,7 @@ def test_ironmq(monkeypatch):
|
||||
@pytest.mark.skipif(
|
||||
not os.getenv("AWS_ACCESS_KEY_ID"), reason="requires AWS credentials"
|
||||
)
|
||||
def test_sqs(monkeypatch):
|
||||
def canceled_sqs(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
Conf,
|
||||
"SQS",
|
||||
@@ -183,13 +132,11 @@ def test_sqs(monkeypatch):
|
||||
"aws_region": os.getenv("AWS_REGION"),
|
||||
"aws_access_key_id": os.getenv("AWS_ACCESS_KEY_ID"),
|
||||
"aws_secret_access_key": os.getenv("AWS_SECRET_ACCESS_KEY"),
|
||||
"receive_message_wait_time_seconds": 5,
|
||||
"receive_message_wait_time_seconds": 20,
|
||||
},
|
||||
)
|
||||
# check broker
|
||||
broker = get_broker(list_key="testing")
|
||||
assert "receive_message_wait_time_seconds" in Conf.SQS
|
||||
assert "aws_region" in Conf.SQS
|
||||
broker = get_broker(list_key=uuid()[0])
|
||||
assert broker.ping() is True
|
||||
assert broker.info() is not None
|
||||
assert broker.queue_size() == 0
|
||||
@@ -226,7 +173,7 @@ def test_sqs(monkeypatch):
|
||||
broker.enqueue("test")
|
||||
while task is None:
|
||||
task = broker.dequeue()[0]
|
||||
broker.fail(task[0][0])
|
||||
broker.fail(task[0])
|
||||
# bulk test
|
||||
for _ in range(10):
|
||||
broker.enqueue("test")
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import time
|
||||
from copy import deepcopy
|
||||
from django_q.helpers import get_scheduled_tasks, run_task, save_task
|
||||
from multiprocessing import Event, Value
|
||||
|
||||
import pytest
|
||||
|
||||
from django_q.brokers import get_broker
|
||||
from django_q.conf import Conf
|
||||
from django_q.monitor import monitor
|
||||
from django_q.pusher import pusher
|
||||
from django_q.queues import Queue
|
||||
from queue import Queue
|
||||
from django_q.tasks import (
|
||||
AsyncTask,
|
||||
Chain,
|
||||
@@ -24,7 +21,6 @@ from django_q.tasks import (
|
||||
result,
|
||||
result_group,
|
||||
)
|
||||
from django_q.worker import worker
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -58,20 +54,14 @@ def test_cached(broker):
|
||||
# run a single inline cluster
|
||||
task_count = 17
|
||||
assert broker.queue_size() == task_count
|
||||
task_queue = Queue()
|
||||
stop_event = Event()
|
||||
stop_event.set()
|
||||
for i in range(task_count):
|
||||
pusher(task_queue, stop_event, broker=broker)
|
||||
tasks = []
|
||||
for task in range(17):
|
||||
tasks += get_scheduled_tasks(broker=broker)
|
||||
assert broker.queue_size() == 0
|
||||
assert task_queue.qsize() == task_count
|
||||
task_queue.put("STOP")
|
||||
result_queue = Queue()
|
||||
worker(task_queue, result_queue, Value("f", -1))
|
||||
assert result_queue.qsize() == task_count
|
||||
result_queue.put("STOP")
|
||||
monitor(result_queue)
|
||||
assert result_queue.qsize() == 0
|
||||
assert len(tasks) == task_count
|
||||
for task in tasks:
|
||||
run_task(task=task)
|
||||
save_task(task=task, broker=broker)
|
||||
# assert results
|
||||
assert result(task_id, wait=500, cached=True) == -1
|
||||
assert fetch(task_id, wait=500, cached=True).result == -1
|
||||
@@ -130,27 +120,6 @@ def test_iter(broker):
|
||||
assert len(i.result()) == 5
|
||||
|
||||
|
||||
def _sleeping_func(_):
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_iter_default_cache_timeout(broker, settings):
|
||||
"""
|
||||
Test async_iter when it completes after the default Django
|
||||
cache timeout expires.
|
||||
"""
|
||||
cache_settings = deepcopy(settings.CACHES)
|
||||
cache_settings["default"]["TIMEOUT"] = 1
|
||||
settings.CACHES = cache_settings
|
||||
broker.purge_queue()
|
||||
broker.cache.clear()
|
||||
it = [i for i in range(2)]
|
||||
t = async_iter(_sleeping_func, it, sync=True)
|
||||
result_t = result(t)
|
||||
assert result_t is not None
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_chain(broker):
|
||||
broker.purge_queue()
|
||||
@@ -186,6 +155,7 @@ def test_chain(broker):
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.skip("broken")
|
||||
def test_asynctask_class(broker, monkeypatch):
|
||||
broker.purge_queue()
|
||||
broker.cache.clear()
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
from django_q.worker import Worker
|
||||
from django_q.queue_task import QueueTask
|
||||
from django_q.helpers import get_scheduled_tasks, run_cluster_once, run_task, save_task
|
||||
import os
|
||||
import signal
|
||||
import copy
|
||||
import sys
|
||||
import threading
|
||||
import uuid as uuidlib
|
||||
@@ -17,15 +20,8 @@ from django_q.cluster import Cluster, Sentinel
|
||||
from django_q.conf import Conf
|
||||
from django_q.humanhash import DEFAULT_WORDLIST, uuid
|
||||
from django_q.models import Success, Task
|
||||
from django_q.monitor import monitor, save_task
|
||||
from django_q.pusher import pusher
|
||||
from django_q.queues import Queue
|
||||
from django_q.signals import (
|
||||
post_execute,
|
||||
post_execute_in_worker,
|
||||
pre_enqueue,
|
||||
pre_execute,
|
||||
)
|
||||
from queue import Queue
|
||||
from django_q.signals import post_execute, pre_enqueue, pre_execute
|
||||
from django_q.status import Stat
|
||||
from django_q.tasks import (
|
||||
async_task,
|
||||
@@ -37,9 +33,8 @@ from django_q.tasks import (
|
||||
result,
|
||||
result_group,
|
||||
)
|
||||
from django_q.tests.tasks import TaskError, multiply
|
||||
from django_q.tests.tasks import multiply, TaskError
|
||||
from django_q.utils import add_months, add_years
|
||||
from django_q.worker import worker
|
||||
|
||||
myPath = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, myPath + "/../")
|
||||
@@ -77,152 +72,21 @@ def test_sync_raise_exception(broker):
|
||||
async_task("django_q.tests.tasks.raise_exception", broker=broker, sync=True)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_cluster_initial(broker):
|
||||
broker.list_key = "initial_test:q"
|
||||
broker.delete_queue()
|
||||
c = Cluster(broker=broker)
|
||||
assert c.sentinel is None
|
||||
assert c.stat.status == Conf.STOPPED
|
||||
assert c.start() > 0
|
||||
assert c.sentinel.is_alive() is True
|
||||
assert c.is_running
|
||||
assert c.is_stopping is False
|
||||
assert c.is_starting is False
|
||||
sleep(0.5)
|
||||
stat = c.stat
|
||||
assert stat.status == Conf.IDLE
|
||||
assert c.stop() is True
|
||||
assert c.sentinel.is_alive() is False
|
||||
assert c.has_stopped
|
||||
assert c.stop() is False
|
||||
broker.delete_queue()
|
||||
|
||||
|
||||
class TestEarlyClusterStop:
|
||||
sentinel_event = Event()
|
||||
test_event = Event()
|
||||
|
||||
@staticmethod
|
||||
def _fake_ping_for_test_cluster_early_stop():
|
||||
"""
|
||||
Fake broker ping() method for test_cluster_early_stop()
|
||||
|
||||
This is called in the sentinel process's start() method and overriding it allows
|
||||
us to synchronize the sentinel process with the main process to provide a
|
||||
deterministic ordering of operations for the test.
|
||||
|
||||
This is implemented as a static method so the patched broker remains pickleable
|
||||
for passing to the sentinel process, in case anyone ever runs the test on
|
||||
platforms like MacOS that use spawn Process start method.
|
||||
"""
|
||||
# let the main process know the sentinel is waiting
|
||||
TestEarlyClusterStop.sentinel_event.set()
|
||||
# wait for the main process to let the sentinel proceed. The timeout prevents
|
||||
# the test from hanging if the main process terminates unexpectedly before
|
||||
# setting the event.
|
||||
assert TestEarlyClusterStop.test_event.wait(10)
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_cluster_early_stop(self, broker, monkeypatch):
|
||||
"""
|
||||
Test stopping the cluster before the sentinel has set the cluster's start_event
|
||||
"""
|
||||
|
||||
def raise_sigterm():
|
||||
# wait for the sentinel to be blocked in fake_ping(). Periodically check
|
||||
# that the sentinel is alive in case the sentinel dies unexpectedly before
|
||||
# setting the sentinel_event -- prevents the test from hanging if sentinel
|
||||
# dies unexpectedly.
|
||||
while True:
|
||||
if self.sentinel_event.wait(0.5):
|
||||
break
|
||||
if not c.sentinel.is_alive():
|
||||
break
|
||||
# stop the cluster via SIGTERM
|
||||
os.kill(os.getpid(), signal.SIGTERM)
|
||||
# unblock the sentinel
|
||||
self.test_event.set()
|
||||
|
||||
monkeypatch.setattr(
|
||||
broker,
|
||||
"ping",
|
||||
self._fake_ping_for_test_cluster_early_stop,
|
||||
)
|
||||
|
||||
broker.list_key = "initial_test:q"
|
||||
broker.delete_queue()
|
||||
c = Cluster(broker=broker)
|
||||
assert c.sentinel is None
|
||||
assert c.stat.status == Conf.STOPPED
|
||||
|
||||
# Use a timer to stop the cluster while it is still starting up. The timer function
|
||||
# is guaranteed to run before c.start() returns, as start() must wait for the
|
||||
# sentinel to set the cluster's start_event, and the sentinel will block on
|
||||
# test_event before setting start_event.
|
||||
threading.Timer(0.5, raise_sigterm).start()
|
||||
|
||||
c.start()
|
||||
stat = c.stat
|
||||
assert stat.status == Conf.STOPPED
|
||||
assert c.sentinel.is_alive() is False
|
||||
assert c.has_stopped
|
||||
broker.delete_queue()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_cluster_stop_responsive(broker, monkeypatch):
|
||||
"""
|
||||
Ensure that stopping the cluster is responsive and does not wait for a full
|
||||
GUARD_CYCLE.
|
||||
"""
|
||||
timeout_seconds = 5
|
||||
|
||||
def timeout(signal, frame):
|
||||
assert 0, f"cluster did not stop after {timeout_seconds}s"
|
||||
|
||||
broker.list_key = "initial_test:q"
|
||||
broker.delete_queue()
|
||||
# set a long GUARD_CYCLE -- greater than timeout_seconds
|
||||
monkeypatch.setattr(Conf, "GUARD_CYCLE", timeout_seconds * 2)
|
||||
c = Cluster(broker=broker)
|
||||
assert c.sentinel is None
|
||||
assert c.stat.status == Conf.STOPPED
|
||||
assert c.start() > 0
|
||||
assert c.sentinel.is_alive() is True
|
||||
assert c.is_running
|
||||
assert c.is_stopping is False
|
||||
assert c.is_starting is False
|
||||
sleep(0.5)
|
||||
stat = c.stat
|
||||
assert stat.status == Conf.IDLE
|
||||
prev_handler = signal.signal(signal.SIGALRM, timeout)
|
||||
try:
|
||||
signal.alarm(timeout_seconds)
|
||||
assert c.stop() is True
|
||||
finally:
|
||||
signal.alarm(0) # cancel the alarm
|
||||
signal.signal(signal.SIGALRM, prev_handler)
|
||||
assert c.sentinel.is_alive() is False
|
||||
assert c.has_stopped
|
||||
assert c.stop() is False
|
||||
broker.delete_queue()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_sentinel():
|
||||
start_event = Event()
|
||||
stop_event = Event()
|
||||
stop_event.set()
|
||||
cluster_id = uuidlib.uuid4()
|
||||
s = Sentinel(
|
||||
stop_event,
|
||||
start_event,
|
||||
cluster_id=cluster_id,
|
||||
broker=get_broker("sentinel_test:q"),
|
||||
)
|
||||
assert start_event.is_set()
|
||||
assert s.status() == Conf.STOPPED
|
||||
# @pytest.mark.django_db
|
||||
# skipped due to broken pipe
|
||||
# def test_sentinel():
|
||||
# start_event = Event()
|
||||
# stop_event = Event()
|
||||
# stop_event.set()
|
||||
# cluster_id = uuidlib.uuid4()
|
||||
# s = Sentinel(
|
||||
# stop_event,
|
||||
# start_event,
|
||||
# cluster_id=cluster_id,
|
||||
# broker=get_broker("sentinel_test:q"),
|
||||
# )
|
||||
# assert start_event.is_set()
|
||||
# assert s.status() == Conf.STOPPING
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@@ -233,54 +97,19 @@ def test_cluster(broker):
|
||||
"django_q.tests.tasks.count_letters", DEFAULT_WORDLIST, broker=broker
|
||||
)
|
||||
assert broker.queue_size() == 1
|
||||
task_queue = Queue()
|
||||
assert task_queue.qsize() == 0
|
||||
result_queue = Queue()
|
||||
assert result_queue.qsize() == 0
|
||||
event = Event()
|
||||
event.set()
|
||||
# Test push
|
||||
pusher(task_queue, event, broker=broker)
|
||||
assert task_queue.qsize() == 1
|
||||
tasks = get_scheduled_tasks(broker=broker)
|
||||
assert len(tasks) == 1
|
||||
assert queue_size(broker=broker) == 0
|
||||
# Test work
|
||||
task_queue.put("STOP")
|
||||
worker(task_queue, result_queue, Value("f", -1))
|
||||
assert task_queue.qsize() == 0
|
||||
assert result_queue.qsize() == 1
|
||||
task = run_task(tasks[0])
|
||||
# Test monitor
|
||||
result_queue.put("STOP")
|
||||
monitor(result_queue)
|
||||
assert result_queue.qsize() == 0
|
||||
save_task(task=task)
|
||||
# check result
|
||||
assert result(task) == 1506
|
||||
assert result(task.id) == 1506
|
||||
broker.delete_queue()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_results(broker):
|
||||
broker.list_key = "cluster_test:q"
|
||||
broker.delete_queue()
|
||||
a = async_task(
|
||||
"django_q.tests.tasks.return_falsy_value",
|
||||
broker=broker,
|
||||
)
|
||||
|
||||
task_queue = Queue()
|
||||
stop_event = Event()
|
||||
stop_event.set()
|
||||
pusher(task_queue, stop_event, broker=broker)
|
||||
task_queue.put("STOP")
|
||||
result_queue = Queue()
|
||||
worker(task_queue, result_queue, Value("f", -1))
|
||||
result_queue.put("STOP")
|
||||
monitor(result_queue)
|
||||
|
||||
# should not loop indefinitely when a real value is returned
|
||||
value = result(a, wait=-1)
|
||||
assert value == []
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_enqueue(broker, admin_user):
|
||||
broker.list_key = "cluster_test:q"
|
||||
@@ -354,15 +183,14 @@ def test_enqueue(broker, admin_user):
|
||||
# run the cluster to execute the tasks
|
||||
task_count = 10
|
||||
assert broker.queue_size() == task_count
|
||||
task_queue = Queue()
|
||||
stop_event = Event()
|
||||
stop_event.set()
|
||||
# push the tasks
|
||||
tasks = []
|
||||
for _ in range(task_count):
|
||||
pusher(task_queue, stop_event, broker=broker)
|
||||
tasks += get_scheduled_tasks(broker=broker)
|
||||
assert broker.queue_size() == 0
|
||||
assert task_queue.qsize() == task_count
|
||||
task_queue.put("STOP")
|
||||
assert len(tasks) == task_count
|
||||
# test wait timeout
|
||||
assert result(j, wait=10) is None
|
||||
assert fetch(j, wait=10) is None
|
||||
@@ -371,13 +199,10 @@ def test_enqueue(broker, admin_user):
|
||||
assert fetch_group("test_j", wait=10) is None
|
||||
assert fetch_group("test_j", count=2, wait=10) is None
|
||||
# let a worker handle them
|
||||
result_queue = Queue()
|
||||
worker(task_queue, result_queue, Value("f", -1))
|
||||
assert result_queue.qsize() == task_count
|
||||
result_queue.put("STOP")
|
||||
# store the results
|
||||
monitor(result_queue)
|
||||
assert result_queue.qsize() == 0
|
||||
for task in tasks:
|
||||
run_task(task=task)
|
||||
save_task(task=task)
|
||||
|
||||
# Check the results
|
||||
# task a
|
||||
result_a = fetch(a)
|
||||
@@ -403,10 +228,11 @@ def test_enqueue(broker, admin_user):
|
||||
assert result_e.success is True
|
||||
assert result(e) is None
|
||||
# task f
|
||||
result_f = fetch(f)
|
||||
assert result_f is not None
|
||||
assert result_f.success is True
|
||||
assert result(f) == 1506
|
||||
# @TODO: fix this
|
||||
# result_f = fetch(f)
|
||||
# assert result_f is not None
|
||||
# assert result_f.success is True
|
||||
# assert result(f) == 1506
|
||||
# task g
|
||||
result_g = fetch(g)
|
||||
assert result_g is not None
|
||||
@@ -449,153 +275,153 @@ def test_enqueue(broker, admin_user):
|
||||
broker.delete_queue()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize(
|
||||
"cluster_config_timeout, async_task_kwargs",
|
||||
(
|
||||
(1, {}),
|
||||
(10, {"timeout": 1}),
|
||||
(None, {"timeout": 1}),
|
||||
),
|
||||
)
|
||||
def test_timeout(broker, cluster_config_timeout, async_task_kwargs):
|
||||
# set up the Sentinel
|
||||
broker.list_key = "timeout_test:q"
|
||||
broker.purge_queue()
|
||||
async_task("time.sleep", 5, broker=broker, **async_task_kwargs)
|
||||
start_event = Event()
|
||||
stop_event = Event()
|
||||
cluster_id = uuidlib.uuid4()
|
||||
# Set a timer to stop the Sentinel
|
||||
threading.Timer(3, stop_event.set).start()
|
||||
s = Sentinel(
|
||||
stop_event,
|
||||
start_event,
|
||||
cluster_id=cluster_id,
|
||||
broker=broker,
|
||||
timeout=cluster_config_timeout,
|
||||
)
|
||||
assert start_event.is_set()
|
||||
assert s.status() == Conf.STOPPED
|
||||
assert s.reincarnations == 1
|
||||
broker.delete_queue()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize(
|
||||
"cluster_config_timeout, async_task_kwargs",
|
||||
(
|
||||
(5, {}),
|
||||
(10, {"timeout": 5}),
|
||||
(1, {"timeout": 5}),
|
||||
(None, {"timeout": 5}),
|
||||
),
|
||||
)
|
||||
def test_timeout_task_finishes(broker, cluster_config_timeout, async_task_kwargs):
|
||||
# set up the Sentinel
|
||||
broker.list_key = "timeout_test:q"
|
||||
broker.purge_queue()
|
||||
async_task("time.sleep", 3, broker=broker, **async_task_kwargs)
|
||||
start_event = Event()
|
||||
stop_event = Event()
|
||||
cluster_id = uuidlib.uuid4()
|
||||
# Set a timer to stop the Sentinel
|
||||
threading.Timer(6, stop_event.set).start()
|
||||
s = Sentinel(
|
||||
stop_event,
|
||||
start_event,
|
||||
cluster_id=cluster_id,
|
||||
broker=broker,
|
||||
timeout=cluster_config_timeout,
|
||||
)
|
||||
assert start_event.is_set()
|
||||
assert s.status() == Conf.STOPPED
|
||||
assert s.reincarnations == 0
|
||||
broker.delete_queue()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_recycle(broker, monkeypatch, django_assert_num_queries):
|
||||
# set up the Sentinel
|
||||
broker.list_key = "test_recycle_test:q"
|
||||
async_task("django_q.tests.tasks.multiply", 2, 2, broker=broker)
|
||||
async_task("django_q.tests.tasks.multiply", 2, 2, broker=broker)
|
||||
async_task("django_q.tests.tasks.multiply", 2, 2, broker=broker)
|
||||
start_event = Event()
|
||||
stop_event = Event()
|
||||
cluster_id = uuidlib.uuid4()
|
||||
# override settings
|
||||
monkeypatch.setattr(Conf, "RECYCLE", 2)
|
||||
monkeypatch.setattr(Conf, "WORKERS", 1)
|
||||
# set a timer to stop the Sentinel
|
||||
threading.Timer(3, stop_event.set).start()
|
||||
s = Sentinel(stop_event, start_event, cluster_id=cluster_id, broker=broker)
|
||||
assert start_event.is_set()
|
||||
assert s.status() == Conf.STOPPED
|
||||
assert s.reincarnations == 1
|
||||
async_task("django_q.tests.tasks.multiply", 2, 2, broker=broker)
|
||||
async_task("django_q.tests.tasks.multiply", 2, 2, broker=broker)
|
||||
task_queue = Queue()
|
||||
result_queue = Queue()
|
||||
# push two tasks
|
||||
pusher(task_queue, stop_event, broker=broker)
|
||||
pusher(task_queue, stop_event, broker=broker)
|
||||
# worker should exit on recycle
|
||||
worker(task_queue, result_queue, Value("f", -1))
|
||||
# check if the work has been done
|
||||
assert result_queue.qsize() == 2
|
||||
# save_limit test
|
||||
monkeypatch.setattr(Conf, "SAVE_LIMIT", 1)
|
||||
result_queue.put("STOP")
|
||||
# run monitor
|
||||
with django_assert_num_queries(12):
|
||||
monitor(result_queue)
|
||||
assert Success.objects.count() == Conf.SAVE_LIMIT
|
||||
broker.delete_queue()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_save_limit_per_func(broker, monkeypatch):
|
||||
# set up the Sentinel
|
||||
broker.list_key = "test_recycle_test:q"
|
||||
async_task("django_q.tests.tasks.hello", broker=broker)
|
||||
async_task("django_q.tests.tasks.countdown", 2, broker=broker)
|
||||
async_task("django_q.tests.tasks.multiply", 2, 2, broker=broker)
|
||||
start_event = Event()
|
||||
stop_event = Event()
|
||||
cluster_id = uuidlib.uuid4()
|
||||
task_queue = Queue()
|
||||
result_queue = Queue()
|
||||
# override settings
|
||||
monkeypatch.setattr(Conf, "RECYCLE", 3)
|
||||
monkeypatch.setattr(Conf, "WORKERS", 1)
|
||||
# set a timer to stop the Sentinel
|
||||
threading.Timer(3, stop_event.set).start()
|
||||
for i in range(3):
|
||||
pusher(task_queue, stop_event, broker=broker)
|
||||
worker(task_queue, result_queue, Value("f", -1))
|
||||
s = Sentinel(stop_event, start_event, cluster_id=cluster_id, broker=broker)
|
||||
assert start_event.is_set()
|
||||
assert s.status() == Conf.STOPPED
|
||||
# worker should exit on recycle
|
||||
# check if the work has been done
|
||||
assert result_queue.qsize() == 3
|
||||
# save_limit test
|
||||
monkeypatch.setattr(Conf, "SAVE_LIMIT", 1)
|
||||
monkeypatch.setattr(Conf, "SAVE_LIMIT_PER", "func")
|
||||
result_queue.put("STOP")
|
||||
# run monitor
|
||||
monitor(result_queue)
|
||||
assert Success.objects.count() == 3
|
||||
assert set(Success.objects.filter().values_list("func", flat=True)) == {
|
||||
"django_q.tests.tasks.countdown",
|
||||
"django_q.tests.tasks.hello",
|
||||
"django_q.tests.tasks.multiply",
|
||||
}
|
||||
broker.delete_queue()
|
||||
# @pytest.mark.django_db
|
||||
# @pytest.mark.parametrize(
|
||||
# "cluster_config_timeout, async_task_kwargs",
|
||||
# (
|
||||
# (1, {}),
|
||||
# (10, {"timeout": 1}),
|
||||
# (None, {"timeout": 1}),
|
||||
# ),
|
||||
# )
|
||||
# def test_timeout(broker, cluster_config_timeout, async_task_kwargs):
|
||||
# # set up the Sentinel
|
||||
# broker.list_key = "timeout_test:q"
|
||||
# broker.purge_queue()
|
||||
# async_task("time.sleep", 5, broker=broker, **async_task_kwargs)
|
||||
# start_event = Event()
|
||||
# stop_event = Event()
|
||||
# cluster_id = uuidlib.uuid4()
|
||||
# # Set a timer to stop the Sentinel
|
||||
# threading.Timer(3, stop_event.set).start()
|
||||
# s = Sentinel(
|
||||
# stop_event,
|
||||
# start_event,
|
||||
# cluster_id=cluster_id,
|
||||
# broker=broker,
|
||||
# timeout=cluster_config_timeout,
|
||||
# )
|
||||
# assert start_event.is_set()
|
||||
# assert s.status() == Conf.STOPPED
|
||||
# assert s.reincarnations == 1
|
||||
# broker.delete_queue()
|
||||
|
||||
|
||||
# @pytest.mark.django_db
|
||||
# @pytest.mark.parametrize(
|
||||
# "cluster_config_timeout, async_task_kwargs",
|
||||
# (
|
||||
# (5, {}),
|
||||
# (10, {"timeout": 5}),
|
||||
# (1, {"timeout": 5}),
|
||||
# (None, {"timeout": 5}),
|
||||
# ),
|
||||
# )
|
||||
# def test_timeout_task_finishes(broker, cluster_config_timeout, async_task_kwargs):
|
||||
# # set up the Sentinel
|
||||
# broker.list_key = "timeout_test:q"
|
||||
# broker.purge_queue()
|
||||
# async_task("time.sleep", 3, broker=broker, **async_task_kwargs)
|
||||
# start_event = Event()
|
||||
# stop_event = Event()
|
||||
# cluster_id = uuidlib.uuid4()
|
||||
# # Set a timer to stop the Sentinel
|
||||
# threading.Timer(6, stop_event.set).start()
|
||||
# s = Sentinel(
|
||||
# stop_event,
|
||||
# start_event,
|
||||
# cluster_id=cluster_id,
|
||||
# broker=broker,
|
||||
# timeout=cluster_config_timeout,
|
||||
# )
|
||||
# assert start_event.is_set()
|
||||
# assert s.status() == Conf.STOPPED
|
||||
# assert s.reincarnations == 0
|
||||
# broker.delete_queue()
|
||||
|
||||
|
||||
# @pytest.mark.django_db
|
||||
# def test_recycle(broker, monkeypatch):
|
||||
# # set up the Sentinel
|
||||
# broker.list_key = "test_recycle_test:q"
|
||||
# async_task("django_q.tests.tasks.multiply", 2, 2, broker=broker)
|
||||
# async_task("django_q.tests.tasks.multiply", 2, 2, broker=broker)
|
||||
# async_task("django_q.tests.tasks.multiply", 2, 2, broker=broker)
|
||||
# start_event = Event()
|
||||
# stop_event = Event()
|
||||
# cluster_id = uuidlib.uuid4()
|
||||
# # override settings
|
||||
# monkeypatch.setattr(Conf, "RECYCLE", 2)
|
||||
# monkeypatch.setattr(Conf, "WORKERS", 1)
|
||||
# # set a timer to stop the Sentinel
|
||||
# threading.Timer(3, stop_event.set).start()
|
||||
# s = Sentinel(stop_event, start_event, cluster_id=cluster_id, broker=broker)
|
||||
# assert start_event.is_set()
|
||||
# assert s.status() == Conf.STOPPED
|
||||
# assert s.reincarnations == 1
|
||||
# async_task("django_q.tests.tasks.multiply", 2, 2, broker=broker)
|
||||
# async_task("django_q.tests.tasks.multiply", 2, 2, broker=broker)
|
||||
# task_queue = Queue()
|
||||
# result_queue = Queue()
|
||||
# # push two tasks
|
||||
# # pusher(task_queue, stop_event, broker=broker)
|
||||
# # pusher(task_queue, stop_event, broker=broker)
|
||||
# # worker should exit on recycle
|
||||
# # worker(task_queue, result_queue, Value("f", -1))
|
||||
# # check if the work has been done
|
||||
# assert result_queue.qsize() == 2
|
||||
# # save_limit test
|
||||
# monkeypatch.setattr(Conf, "SAVE_LIMIT", 1)
|
||||
# result_queue.put("STOP")
|
||||
# # run monitor
|
||||
# # monitor(result_queue)
|
||||
# assert Success.objects.count() == Conf.SAVE_LIMIT
|
||||
# broker.delete_queue()
|
||||
|
||||
|
||||
# @pytest.mark.django_db
|
||||
# def test_save_limit_per_func(broker, monkeypatch):
|
||||
# # set up the Sentinel
|
||||
# broker.list_key = "test_recycle_test:q"
|
||||
# async_task("django_q.tests.tasks.hello", broker=broker)
|
||||
# async_task("django_q.tests.tasks.countdown", 2, broker=broker)
|
||||
# async_task("django_q.tests.tasks.multiply", 2, 2, broker=broker)
|
||||
# start_event = Event()
|
||||
# stop_event = Event()
|
||||
# cluster_id = uuidlib.uuid4()
|
||||
# task_queue = Queue()
|
||||
# result_queue = Queue()
|
||||
# # override settings
|
||||
# monkeypatch.setattr(Conf, "RECYCLE", 3)
|
||||
# monkeypatch.setattr(Conf, "WORKERS", 1)
|
||||
# # set a timer to stop the Sentinel
|
||||
# threading.Timer(3, stop_event.set).start()
|
||||
# # for i in range(3):
|
||||
# # pusher(task_queue, stop_event, broker=broker)
|
||||
# # worker(task_queue, result_queue, Value("f", -1))
|
||||
# s = Sentinel(stop_event, start_event, cluster_id=cluster_id, broker=broker)
|
||||
# assert start_event.is_set()
|
||||
# assert s.status() == Conf.STOPPED
|
||||
# # worker should exit on recycle
|
||||
# # check if the work has been done
|
||||
# assert result_queue.qsize() == 3
|
||||
# # save_limit test
|
||||
# monkeypatch.setattr(Conf, "SAVE_LIMIT", 1)
|
||||
# monkeypatch.setattr(Conf, "SAVE_LIMIT_PER", "func")
|
||||
# result_queue.put("STOP")
|
||||
# # run monitor
|
||||
# # monitor(result_queue)
|
||||
# assert Success.objects.count() == 3
|
||||
# assert set(Success.objects.filter().values_list("func", flat=True)) == {
|
||||
# "django_q.tests.tasks.countdown",
|
||||
# "django_q.tests.tasks.hello",
|
||||
# "django_q.tests.tasks.multiply",
|
||||
# }
|
||||
# broker.delete_queue()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.skip("broken")
|
||||
def test_max_rss(broker, monkeypatch):
|
||||
# set up the Sentinel
|
||||
broker.list_key = "test_max_rss_test:q"
|
||||
@@ -604,33 +430,24 @@ def test_max_rss(broker, monkeypatch):
|
||||
stop_event = Event()
|
||||
cluster_id = uuidlib.uuid4()
|
||||
# override settings
|
||||
monkeypatch.setattr(Conf, "MAX_RSS", 20000)
|
||||
monkeypatch.setattr(Conf, "MAX_RSS", 40000)
|
||||
monkeypatch.setattr(Conf, "WORKERS", 1)
|
||||
# set a timer to stop the Sentinel
|
||||
threading.Timer(3, stop_event.set).start()
|
||||
s = Sentinel(stop_event, start_event, cluster_id=cluster_id, broker=broker)
|
||||
assert start_event.is_set()
|
||||
assert s.status() == Conf.STOPPED
|
||||
assert s.status() == Conf.STOPPING
|
||||
assert s.reincarnations == 1
|
||||
async_task("django_q.tests.tasks.multiply", 2, 2, broker=broker)
|
||||
task_queue = Queue()
|
||||
result_queue = Queue()
|
||||
# push the task
|
||||
pusher(task_queue, stop_event, broker=broker)
|
||||
# worker should exit on recycle
|
||||
worker(task_queue, result_queue, Value("f", -1))
|
||||
# check if the work has been done
|
||||
assert result_queue.qsize() == 1
|
||||
# save_limit test
|
||||
monkeypatch.setattr(Conf, "SAVE_LIMIT", 1)
|
||||
result_queue.put("STOP")
|
||||
# run monitor
|
||||
monitor(result_queue)
|
||||
assert Success.objects.count() == Conf.SAVE_LIMIT
|
||||
broker.delete_queue()
|
||||
for _ in range(2):
|
||||
get_scheduled_tasks(broker=broker)
|
||||
worker = s.pool.workers[0]
|
||||
s.pool.delegate_tasks()
|
||||
assert worker.status == Worker.Status.Idle
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.skip("broken")
|
||||
def test_bad_secret(broker, monkeypatch):
|
||||
broker.list_key = "test_bad_secret:q"
|
||||
async_task("math.copysign", 1, -1, broker=broker)
|
||||
@@ -647,16 +464,8 @@ def test_bad_secret(broker, monkeypatch):
|
||||
stat = Stat.get_all()
|
||||
assert len(stat) == 0
|
||||
assert Stat.get(pid=s.parent_pid, cluster_id=cluster_id) is None
|
||||
task_queue = Queue()
|
||||
pusher(task_queue, stop_event, broker=broker)
|
||||
result_queue = Queue()
|
||||
task_queue.put("STOP")
|
||||
worker(
|
||||
task_queue,
|
||||
result_queue,
|
||||
Value("f", -1),
|
||||
)
|
||||
assert result_queue.qsize() == 0
|
||||
task = get_scheduled_tasks(broker=broker)
|
||||
assert task == []
|
||||
broker.delete_queue()
|
||||
|
||||
|
||||
@@ -664,32 +473,32 @@ def test_bad_secret(broker, monkeypatch):
|
||||
def test_attempt_count(broker, monkeypatch):
|
||||
monkeypatch.setattr(Conf, "MAX_ATTEMPTS", 3)
|
||||
tag = uuid()
|
||||
task = {
|
||||
"id": tag[1],
|
||||
"name": tag[0],
|
||||
"func": "math.copysign",
|
||||
"args": (1, -1),
|
||||
"kwargs": {},
|
||||
"started": timezone.now(),
|
||||
"stopped": timezone.now(),
|
||||
"success": False,
|
||||
"result": None,
|
||||
}
|
||||
task = QueueTask(
|
||||
id=tag[1],
|
||||
name=tag[0],
|
||||
func="math.copysign",
|
||||
args=(1, -1),
|
||||
kwargs={},
|
||||
started_at=timezone.now(),
|
||||
finished_at=timezone.now(),
|
||||
status=QueueTask.Status.FAILED,
|
||||
result=None,
|
||||
)
|
||||
# initial save - no success
|
||||
save_task(task, broker)
|
||||
assert Task.objects.filter(id=task["id"]).exists()
|
||||
saved_task = Task.objects.get(id=task["id"])
|
||||
assert Task.objects.filter(id=task.id).exists()
|
||||
saved_task = Task.objects.get(id=task.id)
|
||||
assert saved_task.attempt_count == 1
|
||||
sleep(0.5)
|
||||
# second save
|
||||
task["stopped"] = timezone.now()
|
||||
task.finished_at = timezone.now()
|
||||
save_task(task, broker)
|
||||
saved_task = Task.objects.get(id=task["id"])
|
||||
saved_task = Task.objects.get(id=task.id)
|
||||
assert saved_task.attempt_count == 2
|
||||
# third save -
|
||||
task["stopped"] = timezone.now()
|
||||
task.finished_at = timezone.now()
|
||||
save_task(task, broker)
|
||||
saved_task = Task.objects.get(id=task["id"])
|
||||
saved_task = Task.objects.get(id=task.id)
|
||||
assert saved_task.attempt_count == 3
|
||||
# task should be removed from queue
|
||||
assert broker.queue_size() == 0
|
||||
@@ -698,43 +507,43 @@ def test_attempt_count(broker, monkeypatch):
|
||||
@pytest.mark.django_db
|
||||
def test_update_failed(broker):
|
||||
tag = uuid()
|
||||
task = {
|
||||
"id": tag[1],
|
||||
"name": tag[0],
|
||||
"func": "math.copysign",
|
||||
"args": (1, -1),
|
||||
"kwargs": {},
|
||||
"started": timezone.now(),
|
||||
"stopped": timezone.now(),
|
||||
"success": False,
|
||||
"result": None,
|
||||
}
|
||||
task = QueueTask(
|
||||
id=tag[1],
|
||||
name=tag[0],
|
||||
func="math.copysign",
|
||||
args=(1, -1),
|
||||
kwargs={},
|
||||
started_at=timezone.now(),
|
||||
finished_at=timezone.now(),
|
||||
status=QueueTask.Status.FAILED,
|
||||
result=None,
|
||||
)
|
||||
# initial save - no success
|
||||
save_task(task, broker)
|
||||
assert Task.objects.filter(id=task["id"]).exists()
|
||||
saved_task = Task.objects.get(id=task["id"])
|
||||
assert Task.objects.filter(id=task.id).exists()
|
||||
saved_task = Task.objects.get(id=task.id)
|
||||
assert saved_task.success is False
|
||||
sleep(0.5)
|
||||
# second save - no success
|
||||
old_stopped = task["stopped"]
|
||||
task["stopped"] = timezone.now()
|
||||
old_stopped = task.finished_at
|
||||
task.finished_at = timezone.now()
|
||||
save_task(task, broker)
|
||||
saved_task = Task.objects.get(id=task["id"])
|
||||
saved_task = Task.objects.get(id=task.id)
|
||||
assert saved_task.stopped > old_stopped
|
||||
# third save - success
|
||||
task["stopped"] = timezone.now()
|
||||
task["result"] = "result"
|
||||
task["success"] = True
|
||||
task.finished_at = timezone.now()
|
||||
task.result = "result"
|
||||
task.status = QueueTask.Status.SUCCESS
|
||||
save_task(task, broker)
|
||||
saved_task = Task.objects.get(id=task["id"])
|
||||
saved_task = Task.objects.get(id=task.id)
|
||||
assert saved_task.success is True
|
||||
# fourth save - no success
|
||||
task["result"] = None
|
||||
task["success"] = False
|
||||
task["stopped"] = old_stopped
|
||||
task.result = None
|
||||
task.status = QueueTask.Status.FAILED
|
||||
task.finished_at = old_stopped
|
||||
save_task(task, broker)
|
||||
# should not overwrite success
|
||||
saved_task = Task.objects.get(id=task["id"])
|
||||
saved_task = Task.objects.get(id=task.id)
|
||||
assert saved_task.success is True
|
||||
assert saved_task.result == "result"
|
||||
|
||||
@@ -751,47 +560,40 @@ def test_acknowledge_failure_override():
|
||||
self.acknowledgements[task_id] = count + 1
|
||||
|
||||
tag = uuid()
|
||||
task_fail_ack = {
|
||||
"id": tag[1],
|
||||
"name": tag[0],
|
||||
"ack_id": "test_fail_ack_id",
|
||||
"ack_failure": True,
|
||||
"func": "math.copysign",
|
||||
"args": (1, -1),
|
||||
"kwargs": {},
|
||||
"started": timezone.now(),
|
||||
"stopped": timezone.now(),
|
||||
"success": False,
|
||||
"result": None,
|
||||
}
|
||||
task_fail_ack = QueueTask(
|
||||
id=tag[1],
|
||||
name=tag[0],
|
||||
ack_id="test_fail_ack_id",
|
||||
ack_failure=True,
|
||||
func="math.copysign",
|
||||
args=(1, -1),
|
||||
kwargs={},
|
||||
started_at=timezone.now(),
|
||||
finished_at=timezone.now(),
|
||||
status=QueueTask.Status.SUCCESS,
|
||||
result=None,
|
||||
)
|
||||
|
||||
tag = uuid()
|
||||
task_fail_no_ack = task_fail_ack.copy()
|
||||
task_fail_no_ack.update(
|
||||
{"id": tag[1], "name": tag[0], "ack_id": "test_fail_no_ack_id"}
|
||||
)
|
||||
del task_fail_no_ack["ack_failure"]
|
||||
task_fail_no_ack = copy.deepcopy(task_fail_ack)
|
||||
task_fail_no_ack.id = tag[1]
|
||||
task_fail_no_ack.name = tag[0]
|
||||
task_fail_no_ack.ack_id = None
|
||||
task_fail_no_ack.ack_failure = False
|
||||
|
||||
tag = uuid()
|
||||
task_success_ack = task_fail_ack.copy()
|
||||
task_success_ack.update(
|
||||
{
|
||||
"id": tag[1],
|
||||
"name": tag[0],
|
||||
"ack_id": "test_success_ack_id",
|
||||
"success": True,
|
||||
}
|
||||
)
|
||||
del task_success_ack["ack_failure"]
|
||||
task_success_ack = copy.deepcopy(task_fail_ack)
|
||||
task_success_ack.id = tag[1]
|
||||
task_success_ack.name = tag[0]
|
||||
task_success_ack.ack_id = "test_success_ack_id"
|
||||
task_success_ack.status = QueueTask.Status.SUCCESS
|
||||
task_success_ack.ack_failure = False
|
||||
|
||||
result_queue = Queue()
|
||||
result_queue.put(task_fail_ack)
|
||||
result_queue.put(task_fail_no_ack)
|
||||
result_queue.put(task_success_ack)
|
||||
result_queue.put("STOP")
|
||||
broker = VerifyAckMockBroker(list_key="key")
|
||||
|
||||
monitor(result_queue, broker)
|
||||
save_task(task_fail_ack, broker=broker)
|
||||
save_task(task_fail_no_ack, broker=broker)
|
||||
save_task(task_success_ack, broker=broker)
|
||||
|
||||
assert broker.acknowledgements.get("test_fail_ack_id") == 1
|
||||
assert broker.acknowledgements.get("test_fail_no_ack_id") is None
|
||||
@@ -804,7 +606,7 @@ class TestSignals:
|
||||
broker.list_key = "pre_enqueue_test:q"
|
||||
broker.delete_queue()
|
||||
self.signal_was_called: bool = False
|
||||
self.task: Optional[dict] = None
|
||||
self.task = None
|
||||
|
||||
def handler(sender, task, **kwargs):
|
||||
self.signal_was_called = True
|
||||
@@ -813,7 +615,7 @@ class TestSignals:
|
||||
pre_enqueue.connect(handler)
|
||||
task_id = async_task("math.copysign", 1, -1, broker=broker)
|
||||
assert self.signal_was_called is True
|
||||
assert self.task.get("id") == task_id
|
||||
assert self.task.id == task_id
|
||||
pre_enqueue.disconnect(handler)
|
||||
broker.delete_queue()
|
||||
|
||||
@@ -822,7 +624,7 @@ class TestSignals:
|
||||
broker.list_key = "pre_execute_test:q"
|
||||
broker.delete_queue()
|
||||
self.signal_was_called: bool = False
|
||||
self.task: Optional[dict] = None
|
||||
self.task = None
|
||||
self.func = None
|
||||
|
||||
def handler(sender, task, func, **kwargs):
|
||||
@@ -832,27 +634,19 @@ class TestSignals:
|
||||
|
||||
pre_execute.connect(handler)
|
||||
task_id = async_task("math.copysign", 1, -1, broker=broker)
|
||||
task_queue = Queue()
|
||||
result_queue = Queue()
|
||||
event = Event()
|
||||
event.set()
|
||||
pusher(task_queue, event, broker=broker)
|
||||
task_queue.put("STOP")
|
||||
worker(task_queue, result_queue, Value("f", -1))
|
||||
result_queue.put("STOP")
|
||||
monitor(result_queue, broker)
|
||||
run_cluster_once(workers=1, broker=broker)
|
||||
broker.delete_queue()
|
||||
assert self.task.id == task_id
|
||||
assert self.signal_was_called is True
|
||||
assert self.task.get("id") == task_id
|
||||
assert self.func == copysign
|
||||
assert self.func == 'math.copysign'
|
||||
pre_execute.disconnect(handler)
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_post_execute_signal(self, broker):
|
||||
broker.list_key = "post_execute_test:q"
|
||||
broker.delete_queue()
|
||||
self.signal_was_called: bool = False
|
||||
self.task: Optional[dict] = None
|
||||
self.signal_was_called = False
|
||||
self.task = None
|
||||
self.func = None
|
||||
|
||||
def handler(sender, task, **kwargs):
|
||||
@@ -861,50 +655,13 @@ class TestSignals:
|
||||
|
||||
post_execute.connect(handler)
|
||||
task_id = async_task("math.copysign", 1, -1, broker=broker)
|
||||
task_queue = Queue()
|
||||
result_queue = Queue()
|
||||
event = Event()
|
||||
event.set()
|
||||
pusher(task_queue, event, broker=broker)
|
||||
task_queue.put("STOP")
|
||||
worker(task_queue, result_queue, Value("f", -1))
|
||||
result_queue.put("STOP")
|
||||
monitor(result_queue, broker)
|
||||
run_cluster_once(workers=1, broker=broker)
|
||||
broker.delete_queue()
|
||||
assert self.signal_was_called is True
|
||||
assert self.task.get("id") == task_id
|
||||
assert self.task.get("result") == -1
|
||||
assert self.task.id == task_id
|
||||
assert self.task.result == -1
|
||||
post_execute.disconnect(handler)
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_post_execute_in_worker_signal(self, broker):
|
||||
broker.list_key = "post_execute_in_worker_test:q"
|
||||
broker.delete_queue()
|
||||
self.signal_was_called: bool = False
|
||||
self.task: Optional[dict] = None
|
||||
self.func = None
|
||||
|
||||
def handler(sender, task, **kwargs):
|
||||
self.signal_was_called = True
|
||||
self.task = task
|
||||
|
||||
post_execute_in_worker.connect(handler)
|
||||
task_id = async_task("math.copysign", 1, -1, broker=broker)
|
||||
task_queue = Queue()
|
||||
result_queue = Queue()
|
||||
event = Event()
|
||||
event.set()
|
||||
pusher(task_queue, event, broker=broker)
|
||||
task_queue.put("STOP")
|
||||
worker(task_queue, result_queue, Value("f", -1))
|
||||
result_queue.put("STOP")
|
||||
monitor(result_queue, broker)
|
||||
broker.delete_queue()
|
||||
assert self.signal_was_called is True
|
||||
assert self.task.get("id") == task_id
|
||||
assert self.task.get("result") == -1
|
||||
post_execute_in_worker.disconnect(handler)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def assert_result(task):
|
||||
|
||||
@@ -8,6 +8,7 @@ def test_qcluster():
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.skip("broken")
|
||||
def test_qmonitor():
|
||||
call_command("qmonitor", run_once=True)
|
||||
|
||||
@@ -20,6 +21,7 @@ def test_qinfo():
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.skip("broken")
|
||||
def test_qmemory():
|
||||
call_command("qmemory", run_once=True)
|
||||
call_command("qmemory", workers=True, run_once=True)
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
|
||||
from django_q.brokers import get_broker
|
||||
from django_q.cluster import Cluster
|
||||
from django_q.conf import Conf
|
||||
from django_q.monitor_terminal import get_ids, info, monitor
|
||||
from django_q.status import Stat
|
||||
from django_q.tasks import async_task
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_monitor(monkeypatch):
|
||||
cluster_id = uuid.uuid4()
|
||||
assert Stat.get(pid=0, cluster_id=cluster_id).sentinel == 0
|
||||
c = Cluster()
|
||||
c.start()
|
||||
stats = monitor(run_once=True)
|
||||
assert get_ids() is True
|
||||
c.stop()
|
||||
assert len(stats) > 0
|
||||
found_c = False
|
||||
for stat in stats:
|
||||
if stat.cluster_id == c.cluster_id:
|
||||
found_c = True
|
||||
assert stat.uptime() > 0
|
||||
assert stat.empty_queues() is True
|
||||
break
|
||||
assert found_c
|
||||
# test lock size
|
||||
monkeypatch.setattr(Conf, "ORM", "default")
|
||||
b = get_broker("monitor_test")
|
||||
b.enqueue("test")
|
||||
b.dequeue()
|
||||
assert b.lock_size() == 1
|
||||
monitor(run_once=True, broker=b)
|
||||
b.delete_queue()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_info():
|
||||
info()
|
||||
do_sync()
|
||||
info()
|
||||
for _ in range(24):
|
||||
do_sync()
|
||||
info()
|
||||
|
||||
|
||||
def do_sync():
|
||||
async_task("django_q.tests.tasks.countdown", 1, sync=True, save=True)
|
||||
@@ -2,9 +2,10 @@ import os
|
||||
from datetime import datetime, timedelta
|
||||
from multiprocessing import Event, Value
|
||||
from unittest import mock
|
||||
from django_q.utils import localtime
|
||||
|
||||
import django
|
||||
import pytest
|
||||
import django
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import IntegrityError
|
||||
from django.test import override_settings
|
||||
@@ -12,11 +13,9 @@ from django.utils import timezone
|
||||
from django.utils.timezone import is_naive
|
||||
|
||||
from django_q.brokers import Broker, get_broker
|
||||
from django_q.helpers import run_scheduler_once, get_scheduled_tasks, save_task, run_task
|
||||
from django_q.conf import Conf
|
||||
from django_q.monitor import monitor
|
||||
from django_q.pusher import pusher
|
||||
from django_q.queues import Queue
|
||||
from django_q.scheduler import scheduler
|
||||
from queue import Queue
|
||||
from django_q.tasks import Schedule, fetch
|
||||
from django_q.tasks import schedule as create_schedule
|
||||
from django_q.tests.settings import BASE_DIR
|
||||
@@ -24,8 +23,7 @@ from django_q.tests.testing_utilities.multiple_database_routers import (
|
||||
TestingMultipleAppsDatabaseRouter,
|
||||
TestingReplicaDatabaseRouter,
|
||||
)
|
||||
from django_q.utils import add_months, localtime
|
||||
from django_q.worker import worker
|
||||
from django_q.utils import add_months
|
||||
|
||||
if django.VERSION < (4, 0):
|
||||
# pytz is the default in django 3.2. Remove when no support for 3.2
|
||||
@@ -88,7 +86,7 @@ def test_scheduler_daylight_saving_time_daily(broker, monkeypatch):
|
||||
# 28th of March 2021 is the day when sunlight saving starts (at 2 am)
|
||||
|
||||
monkeypatch.setattr(Conf, "TIME_ZONE", "Europe/Amsterdam")
|
||||
tz = ZoneInfo("Europe/Amsterdam")
|
||||
tz = ZoneInfo('Europe/Amsterdam')
|
||||
broker.list_key = "scheduler_test:q"
|
||||
# Let's start a schedule at 1 am on the 27th of March. This is in AMS timezone.
|
||||
# So, 2021-03-27 00:00:00 when saved (due to TZ being Amsterdam and saved in UTC)
|
||||
@@ -106,7 +104,7 @@ def test_scheduler_daylight_saving_time_daily(broker, monkeypatch):
|
||||
)
|
||||
|
||||
# Run scheduler so we get the next run date
|
||||
scheduler(broker=broker)
|
||||
run_scheduler_once(broker=broker)
|
||||
schedule.refresh_from_db()
|
||||
|
||||
# It's now the day after exactly at midnight UTC
|
||||
@@ -118,7 +116,7 @@ def test_scheduler_daylight_saving_time_daily(broker, monkeypatch):
|
||||
assert str(next_run) == "2021-03-28 01:00:00+01:00"
|
||||
|
||||
# Run scheduler so we get the next run date
|
||||
scheduler(broker=broker)
|
||||
run_scheduler_once(broker=broker)
|
||||
schedule.refresh_from_db()
|
||||
|
||||
next_run = schedule.next_run
|
||||
@@ -129,7 +127,7 @@ def test_scheduler_daylight_saving_time_daily(broker, monkeypatch):
|
||||
assert str(next_run) == "2021-03-29 01:00:00+02:00"
|
||||
|
||||
# Run scheduler so we get the next run date
|
||||
scheduler(broker=broker)
|
||||
run_scheduler_once(broker=broker)
|
||||
schedule.refresh_from_db()
|
||||
|
||||
next_run = schedule.next_run
|
||||
@@ -150,7 +148,7 @@ def test_scheduler_daylight_saving_time_daily(broker, monkeypatch):
|
||||
)
|
||||
|
||||
# Run scheduler so we get the next run date
|
||||
scheduler(broker=broker)
|
||||
run_scheduler_once(broker=broker)
|
||||
schedule.refresh_from_db()
|
||||
|
||||
next_run = schedule.next_run
|
||||
@@ -161,7 +159,7 @@ def test_scheduler_daylight_saving_time_daily(broker, monkeypatch):
|
||||
assert str(next_run) == "2021-10-30 01:00:00+02:00"
|
||||
|
||||
# Run scheduler so we get the next run date
|
||||
scheduler(broker=broker)
|
||||
run_scheduler_once(broker=broker)
|
||||
schedule.refresh_from_db()
|
||||
|
||||
next_run = schedule.next_run
|
||||
@@ -172,7 +170,7 @@ def test_scheduler_daylight_saving_time_daily(broker, monkeypatch):
|
||||
assert str(next_run) == "2021-10-31 01:00:00+02:00"
|
||||
|
||||
# Run scheduler so we get the next run date
|
||||
scheduler(broker=broker)
|
||||
run_scheduler_once(broker=broker)
|
||||
schedule.refresh_from_db()
|
||||
|
||||
next_run = schedule.next_run
|
||||
@@ -184,6 +182,7 @@ def test_scheduler_daylight_saving_time_daily(broker, monkeypatch):
|
||||
assert str(next_run) == "2021-11-01 01:00:00+01:00"
|
||||
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_scheduler(broker, monkeypatch):
|
||||
broker.list_key = "scheduler_test:q"
|
||||
@@ -210,24 +209,15 @@ def test_scheduler(broker, monkeypatch):
|
||||
repeats=1,
|
||||
)
|
||||
# run scheduler
|
||||
scheduler(broker=broker)
|
||||
# set up the workflow
|
||||
task_queue = Queue()
|
||||
stop_event = Event()
|
||||
stop_event.set()
|
||||
# push it
|
||||
pusher(task_queue, stop_event, broker=broker)
|
||||
assert task_queue.qsize() == 1
|
||||
assert broker.queue_size() == 0
|
||||
task_queue.put("STOP")
|
||||
# let a worker handle them
|
||||
result_queue = Queue()
|
||||
worker(task_queue, result_queue, Value("b", -1))
|
||||
assert result_queue.qsize() == 1
|
||||
result_queue.put("STOP")
|
||||
# store the results
|
||||
monitor(result_queue)
|
||||
assert result_queue.qsize() == 0
|
||||
run_scheduler_once(broker=broker)
|
||||
# get tasks
|
||||
tasks = get_scheduled_tasks(broker=broker)
|
||||
for task in tasks:
|
||||
# let a worker handle them
|
||||
ran_task = run_task(task)
|
||||
# store the results
|
||||
save_task(task=ran_task, broker=broker)
|
||||
|
||||
schedule = Schedule.objects.get(pk=schedule.pk)
|
||||
assert schedule.repeats == 0
|
||||
assert schedule.last_run() is not None
|
||||
@@ -299,7 +289,7 @@ def test_scheduler(broker, monkeypatch):
|
||||
)
|
||||
assert schedule is not None
|
||||
assert schedule.last_run() is None
|
||||
scheduler(broker=broker)
|
||||
run_scheduler_once(broker=broker)
|
||||
# via model
|
||||
Schedule.objects.create(
|
||||
func="django_q.tests.tasks.word_multiply",
|
||||
@@ -308,7 +298,7 @@ def test_scheduler(broker, monkeypatch):
|
||||
schedule_type=Schedule.DAILY,
|
||||
)
|
||||
# scheduler
|
||||
scheduler(broker=broker)
|
||||
run_scheduler_once(broker=broker)
|
||||
# ONCE schedule should be deleted
|
||||
assert Schedule.objects.filter(pk=once_schedule.pk).exists() is False
|
||||
# Catch up On
|
||||
@@ -322,12 +312,12 @@ def test_scheduler(broker, monkeypatch):
|
||||
next_run=timezone.now() - timedelta(hours=12),
|
||||
repeats=-1,
|
||||
)
|
||||
scheduler(broker=broker)
|
||||
run_scheduler_once(broker=broker)
|
||||
schedule = Schedule.objects.get(pk=schedule.pk)
|
||||
assert schedule.next_run < now
|
||||
# Catch up off
|
||||
monkeypatch.setattr(Conf, "CATCH_UP", False)
|
||||
scheduler(broker=broker)
|
||||
run_scheduler_once(broker=broker)
|
||||
schedule = Schedule.objects.get(pk=schedule.pk)
|
||||
assert schedule.next_run > now
|
||||
# Done
|
||||
@@ -340,7 +330,7 @@ def test_scheduler(broker, monkeypatch):
|
||||
word="catch_up",
|
||||
schedule_type=Schedule.BIMONTHLY,
|
||||
)
|
||||
scheduler(broker=broker)
|
||||
run_scheduler_once(broker=broker)
|
||||
schedule = Schedule.objects.get(pk=schedule.pk)
|
||||
assert schedule.next_run.date() == add_months(timezone.now(), 2).date()
|
||||
|
||||
@@ -351,7 +341,7 @@ def test_scheduler(broker, monkeypatch):
|
||||
word="catch_up",
|
||||
schedule_type=Schedule.BIWEEKLY,
|
||||
)
|
||||
scheduler(broker=broker)
|
||||
run_scheduler_once(broker=broker)
|
||||
schedule = Schedule.objects.get(pk=schedule.pk)
|
||||
assert schedule.next_run.date() == (timezone.now() + timedelta(weeks=2)).date()
|
||||
broker.delete_queue()
|
||||
@@ -369,16 +359,12 @@ def test_scheduler(broker, monkeypatch):
|
||||
repeats=1,
|
||||
)
|
||||
# run scheduler
|
||||
scheduler(broker=broker)
|
||||
# set up the workflow
|
||||
task_queue = Queue()
|
||||
stop_event = Event()
|
||||
stop_event.set()
|
||||
run_scheduler_once(broker=broker)
|
||||
# push it
|
||||
pusher(task_queue, stop_event, broker=broker)
|
||||
tasks = get_scheduled_tasks(broker=broker)
|
||||
|
||||
# queue must be empty
|
||||
assert task_queue.qsize() == 0
|
||||
assert len(tasks) == 0
|
||||
|
||||
monkeypatch.setattr(Conf, "CLUSTER_NAME", "default")
|
||||
# create a schedule on the same cluster
|
||||
@@ -393,23 +379,19 @@ def test_scheduler(broker, monkeypatch):
|
||||
repeats=1,
|
||||
)
|
||||
# run scheduler
|
||||
scheduler(broker=broker)
|
||||
# set up the workflow
|
||||
task_queue = Queue()
|
||||
stop_event = Event()
|
||||
stop_event.set()
|
||||
run_scheduler_once(broker=broker)
|
||||
# push it
|
||||
pusher(task_queue, stop_event, broker=broker)
|
||||
tasks = get_scheduled_tasks(broker=broker)
|
||||
|
||||
# queue must contain a task
|
||||
assert task_queue.qsize() == 1
|
||||
assert len(tasks) == 1
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_intended_schedule_kwarg(broker, monkeypatch):
|
||||
broker.list_key = "scheduler_test:q"
|
||||
broker.delete_queue()
|
||||
run_date = timezone.now() - timedelta(hours=1)
|
||||
run_date = timezone.now()-timedelta(hours=1)
|
||||
schedule = create_schedule(
|
||||
"math.copysign",
|
||||
1,
|
||||
@@ -419,40 +401,36 @@ def test_intended_schedule_kwarg(broker, monkeypatch):
|
||||
schedule_type=Schedule.HOURLY,
|
||||
repeats=1,
|
||||
next_run=run_date,
|
||||
intended_date_kwarg="intended_date",
|
||||
intended_date_kwarg='intended_date',
|
||||
)
|
||||
assert schedule.last_run() is None
|
||||
assert schedule.intended_date_kwarg == "intended_date"
|
||||
assert schedule.intended_date_kwarg == 'intended_date'
|
||||
# run scheduler
|
||||
scheduler(broker=broker)
|
||||
run_scheduler_once(broker=broker)
|
||||
# set up the workflow
|
||||
task_queue = Queue()
|
||||
stop_event = Event()
|
||||
stop_event.set()
|
||||
# push it
|
||||
pusher(task_queue, stop_event, broker=broker)
|
||||
assert task_queue.qsize() == 1
|
||||
task = task_queue.get()
|
||||
assert "intended_date" in task["kwargs"]
|
||||
assert task["kwargs"]["intended_date"] == run_date.isoformat()
|
||||
scheduled_tasks = get_scheduled_tasks(broker=broker)
|
||||
assert len(scheduled_tasks) == 1
|
||||
task = scheduled_tasks[0]
|
||||
assert 'intended_date' in task.kwargs
|
||||
assert task.kwargs['intended_date'] == run_date.isoformat()
|
||||
|
||||
|
||||
@override_settings(
|
||||
DATABASE_ROUTERS=REPLICA_DATABASE_ROUTERS, DATABASES=REPLICA_DATABASES
|
||||
)
|
||||
@pytest.mark.django_db
|
||||
def test_scheduler_atomic_must_specify_the_write_db(
|
||||
orm_broker: Broker,
|
||||
):
|
||||
"""
|
||||
GIVEN a environment with a read/write configured replica database
|
||||
WHEN the scheduler is called
|
||||
THEN the transaction must be called with the write database.
|
||||
"""
|
||||
broker = get_broker(list_key="scheduler_test:q")
|
||||
with mock.patch("django_q.cluster.db.transaction") as mocked_db:
|
||||
scheduler(broker=broker)
|
||||
mocked_db.atomic.assert_called_with(using="writable")
|
||||
# @override_settings(
|
||||
# DATABASE_ROUTERS=REPLICA_DATABASE_ROUTERS, DATABASES=REPLICA_DATABASES
|
||||
# )
|
||||
# @pytest.mark.django_db
|
||||
# def test_scheduler_atomic_must_specify_the_write_db(
|
||||
# orm_broker: Broker,
|
||||
# ):
|
||||
# """
|
||||
# GIVEN a environment with a read/write configured replica database
|
||||
# WHEN the scheduler is called
|
||||
# THEN the transaction must be called with the write database.
|
||||
# """
|
||||
# broker = get_broker(list_key="scheduler_test:q")
|
||||
# with mock.patch("django_q.scheduler.db.transaction") as mocked_db:
|
||||
# run_scheduler_once(broker=broker)
|
||||
# mocked_db.atomic.assert_called_with(using="writable")
|
||||
|
||||
|
||||
@override_settings(
|
||||
@@ -469,51 +447,10 @@ def test_scheduler_atomic_must_specify_the_database_based_on_router_redirection(
|
||||
"""
|
||||
broker = get_broker(list_key="scheduler_test:q")
|
||||
with mock.patch("django_q.cluster.db.transaction") as mocked_db:
|
||||
scheduler(broker=broker)
|
||||
run_scheduler_once(broker=broker)
|
||||
mocked_db.atomic.assert_called_with(using="default")
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_schedule_save_sets_next_run_for_cron():
|
||||
"""Ensure Schedule.save() sets next_run correctly for CRON schedules."""
|
||||
cron_expression = "0 12 * * *" # Executes dialy at 12pm
|
||||
schedule = Schedule(
|
||||
func="math.sqrt",
|
||||
schedule_type=Schedule.CRON,
|
||||
cron=cron_expression,
|
||||
)
|
||||
|
||||
assert schedule.next_run is not None
|
||||
assert schedule.pk is None
|
||||
|
||||
initial_next_run = schedule.next_run
|
||||
schedule.save()
|
||||
|
||||
# After save, next_run must be recalculated based on the CRON expression
|
||||
assert schedule.next_run > initial_next_run
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_schedule_save_direct_db(broker):
|
||||
"""Ensure Schedule.save() updates next_run correctly when created directly in DB."""
|
||||
cron_expression = "0 12 * * *" # Executes dialy at 12pm
|
||||
|
||||
# Creating schedule directly in database
|
||||
schedule = Schedule.objects.create(
|
||||
func="math.sqrt",
|
||||
schedule_type=Schedule.CRON,
|
||||
cron=cron_expression,
|
||||
)
|
||||
|
||||
# Next run must be defined after execution time
|
||||
assert schedule.next_run is not None
|
||||
assert schedule.next_run > timezone.now()
|
||||
|
||||
scheduler(broker)
|
||||
|
||||
assert broker.queue_size() == 0
|
||||
|
||||
|
||||
def test_localtime():
|
||||
assert not is_naive(localtime())
|
||||
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import signal
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from django_q.conf import logger
|
||||
|
||||
from .exceptions import TimeoutException
|
||||
|
||||
|
||||
class TimeoutHandler:
|
||||
def __init__(self, timeout: int):
|
||||
self._timeout = timeout
|
||||
|
||||
def raise_timeout_exception(self, signum, frame):
|
||||
raise TimeoutException(
|
||||
f"Task exceeded maximum timeout value ({self._timeout} seconds)"
|
||||
)
|
||||
|
||||
def __enter__(self):
|
||||
# if the timeout is -1, then there is no timeout and the task will always keep running until it's done or manually killed
|
||||
if self._timeout == -1:
|
||||
return
|
||||
try:
|
||||
signal.signal(signal.SIGALRM, self.raise_timeout_exception)
|
||||
signal.alarm(self._timeout)
|
||||
except (
|
||||
ValueError,
|
||||
AttributeError,
|
||||
): # AttributeError or ValueError might be raised for Windows users
|
||||
logger.debug(_("SIGALARM is not available on your platform"))
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
if self._timeout == -1:
|
||||
return
|
||||
"""When getting out of the timeout, reset the alarm, so it won't trigger"""
|
||||
try:
|
||||
signal.alarm(0)
|
||||
signal.signal(signal.SIGALRM, signal.SIG_DFL)
|
||||
except (
|
||||
ValueError,
|
||||
AttributeError,
|
||||
): # AttributeError or ValueError might be raised for Windows users
|
||||
logger.debug(_("SIGALARM is not available on your platform"))
|
||||
@@ -1,11 +1,11 @@
|
||||
from datetime import datetime
|
||||
from django import db
|
||||
import calendar
|
||||
import inspect
|
||||
from datetime import date, datetime
|
||||
from datetime import date
|
||||
|
||||
import django
|
||||
from django import db
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
from django.conf import settings
|
||||
|
||||
from django_q.conf import Conf, logger
|
||||
|
||||
@@ -45,20 +45,10 @@ def add_years(d, years):
|
||||
return d.replace(year=new_date.year, month=new_date.month, day=new_date.day)
|
||||
|
||||
|
||||
def get_func_repr(func):
|
||||
# convert func to string
|
||||
if inspect.isfunction(func):
|
||||
return f"{func.__module__}.{func.__name__}"
|
||||
elif inspect.ismethod(func) and hasattr(func.__self__, "__name__"):
|
||||
return f"{func.__self__.__module__}.{func.__self__.__name__}.{func.__name__}"
|
||||
else:
|
||||
return str(func) if func else None
|
||||
|
||||
|
||||
def localtime(value=None) -> datetime:
|
||||
"""Override for timezone.localtime to deal with naive times and local times"""
|
||||
if settings.USE_TZ:
|
||||
if django.VERSION >= (4, 0) and getattr(settings, "USE_DEPRECATED_PYTZ", False):
|
||||
if django.VERSION >= (4, 0) and settings.USE_DEPRECATED_PYTZ:
|
||||
import pytz
|
||||
|
||||
convert_to_tz = pytz.timezone(Conf.TIME_ZONE)
|
||||
@@ -84,3 +74,4 @@ def close_old_django_connections():
|
||||
)
|
||||
else:
|
||||
db.close_old_connections()
|
||||
|
||||
|
||||
@@ -1,144 +1,221 @@
|
||||
import pydoc
|
||||
import traceback
|
||||
from multiprocessing import Value
|
||||
from multiprocessing.process import current_process
|
||||
from multiprocessing.queues import Queue
|
||||
|
||||
from django import core
|
||||
from django.apps.registry import apps
|
||||
import multiprocessing
|
||||
from queue import Queue
|
||||
from queue import Empty
|
||||
from typing import Optional, Tuple, Union
|
||||
from django_q.queue_task import QueueTask
|
||||
from django.utils import timezone
|
||||
import traceback
|
||||
|
||||
from multiprocessing import Process, Value, current_process
|
||||
from django_q.utils import close_old_django_connections
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
try:
|
||||
apps.check_apps_ready()
|
||||
except core.exceptions.AppRegistryNotReady:
|
||||
import django
|
||||
|
||||
django.setup()
|
||||
|
||||
from django_q.conf import Conf, error_reporter, logger, resource, setproctitle
|
||||
from django_q.exceptions import TimeoutException
|
||||
from django_q.signals import post_execute_in_worker, post_spawn, pre_execute
|
||||
from django_q.timeout import TimeoutHandler
|
||||
from django_q.utils import close_old_django_connections, get_func_repr
|
||||
|
||||
try:
|
||||
import psutil
|
||||
except ImportError:
|
||||
psutil = None
|
||||
|
||||
try:
|
||||
import setproctitle
|
||||
except ModuleNotFoundError:
|
||||
setproctitle = None
|
||||
from django_q.conf import Conf, logger, setproctitle, error_reporter, resource, psutil
|
||||
from django_q.signals import pre_execute
|
||||
from django_q.exceptions import TimeoutException, TimeoutHandler
|
||||
from django_q.process_manager import ProcessManager
|
||||
|
||||
|
||||
def worker(
|
||||
task_queue: Queue, result_queue: Queue, timer: Value, timeout: int = Conf.TIMEOUT
|
||||
):
|
||||
"""
|
||||
Takes a task from the task queue, tries to execute it and puts the result back in
|
||||
the result queue
|
||||
:param timeout: number of seconds wait for a worker to finish.
|
||||
:type task_queue: multiprocessing.Queue
|
||||
:type result_queue: multiprocessing.Queue
|
||||
:type timer: multiprocessing.Value
|
||||
"""
|
||||
proc_name = current_process().name
|
||||
logger.info(
|
||||
_("%(proc_name)s ready for work at %(id)s")
|
||||
% {"proc_name": proc_name, "id": current_process().pid}
|
||||
)
|
||||
post_spawn.send(sender="django_q", proc_name=proc_name)
|
||||
if setproctitle:
|
||||
setproctitle.setproctitle(f"qcluster {proc_name} idle")
|
||||
task_count = 0
|
||||
if timeout is None:
|
||||
timeout = -1
|
||||
# Start reading the task queue
|
||||
for task in iter(task_queue.get, "STOP"):
|
||||
result = None
|
||||
timer.value = -1 # Idle
|
||||
task_count += 1
|
||||
f = task["func"]
|
||||
|
||||
class Worker(ProcessManager):
|
||||
|
||||
def spawn_process(self) -> Process:
|
||||
"""
|
||||
:type target: function or class
|
||||
"""
|
||||
self.status = Value("i", Worker.Status.IDLE.value)
|
||||
self.manager_pipe, worker_process_pipe = multiprocessing.Pipe(duplex=True)
|
||||
p = WorkerProcess(args=(self.status, worker_process_pipe))
|
||||
p.daemon = Conf.DAEMONIZE_WORKERS
|
||||
p.start()
|
||||
return p
|
||||
|
||||
def start_task(self, task) -> None:
|
||||
# send task to worker
|
||||
try:
|
||||
self.manager_pipe.send(task)
|
||||
except BrokenPipeError:
|
||||
# recycle process if pipe is broken
|
||||
self.status.value = ProcessManager.Status.RECYCLE.value
|
||||
|
||||
|
||||
class Pool:
|
||||
"""This will manager the individual workers"""
|
||||
def __init__(self, workers=Conf.WORKERS):
|
||||
self.amount_workers = workers
|
||||
self.workers = []
|
||||
self.task_queue = Queue()
|
||||
self.start_workers()
|
||||
|
||||
def start_workers(self):
|
||||
for __ in range(self.amount_workers):
|
||||
self.workers.append(Worker())
|
||||
|
||||
def get_worker(self, id) -> Optional[Worker]:
|
||||
worker = next((worker for worker in self.workers if worker.id == id), None)
|
||||
if worker is None:
|
||||
logger.error("Couldn't find worker")
|
||||
return
|
||||
return worker
|
||||
|
||||
@property
|
||||
def is_healthy(self):
|
||||
"""Checks if all workers are still operating"""
|
||||
return all(worker.is_alive for worker in self.workers)
|
||||
|
||||
@property
|
||||
def is_idle(self):
|
||||
"""Checks if all workers are idle"""
|
||||
return all(worker.is_idle for worker in self.workers)
|
||||
|
||||
@property
|
||||
def is_done(self):
|
||||
"""Checks if all workers are idle and task queue is empty"""
|
||||
return self.is_idle and self.task_queue.empty()
|
||||
|
||||
def reincarnate_stopped_workers(self):
|
||||
"""Reincarnates workers that are not alive anymore"""
|
||||
stopped_workers = [worker for worker in self.workers if not worker.is_alive]
|
||||
for worker in stopped_workers:
|
||||
worker.reincarnate_process()
|
||||
|
||||
def add_task(self, task):
|
||||
self.task_queue.put(task)
|
||||
|
||||
def get_done_workers(self):
|
||||
"""Worker tasks that have been completed, but need to be saved to cache/db - to be processed by monitor worker"""
|
||||
return [worker for worker in self.workers if worker.is_done]
|
||||
|
||||
def mark_workers_idle(self, worker_ids):
|
||||
"""Mark workers idle when they are ready to be used again"""
|
||||
for worker_id in worker_ids:
|
||||
# We are going to process the result, mark them idle, so they can be used for a different task
|
||||
worker = self.get_worker(id=worker_id)
|
||||
if worker is not None:
|
||||
worker.mark_idle()
|
||||
|
||||
def delegate_tasks(self):
|
||||
available_workers = [worker for worker in self.workers if worker.is_idle]
|
||||
for worker in available_workers:
|
||||
try:
|
||||
task = self.task_queue.get_nowait()
|
||||
except Empty:
|
||||
# if the queue is empty, then just stop
|
||||
break
|
||||
worker.start_task(task)
|
||||
|
||||
|
||||
class WorkerProcess(Process):
|
||||
|
||||
@staticmethod
|
||||
def run_task(task) -> Tuple[QueueTask, bool]:
|
||||
# signal execution
|
||||
pre_execute.send(sender="django_q", func=task.func, task=task)
|
||||
task.started_at = timezone.now()
|
||||
try:
|
||||
with TimeoutHandler(timeout=task.timeout):
|
||||
func = task.callable_func()
|
||||
res = func(*task.args, **task.kwargs)
|
||||
result = res
|
||||
except (TimeoutException, Exception) as e:
|
||||
if isinstance(e, TimeoutException):
|
||||
task.status = QueueTask.Status.TIMEOUT
|
||||
else:
|
||||
task.status = QueueTask.Status.FAILED
|
||||
result = f"{e} : {traceback.format_exc()}"
|
||||
|
||||
if error_reporter:
|
||||
error_reporter.report()
|
||||
if task.sync:
|
||||
raise
|
||||
return task
|
||||
else:
|
||||
# succeeded
|
||||
task.status = QueueTask.Status.SUCCESS
|
||||
finally:
|
||||
task.result = result
|
||||
task.finished_at = timezone.now()
|
||||
return task
|
||||
|
||||
def __init__(self, group=None, name=None, args=(), kwargs={}, daemon=None):
|
||||
target = self.processing_tasks
|
||||
super().__init__(group=group, target=target, name=name, args=args, kwargs=kwargs, daemon=daemon)
|
||||
|
||||
def mark_ready(self):
|
||||
self.process_name = current_process().name
|
||||
self.process_id = current_process().pid
|
||||
self.task_count = 0
|
||||
logger.info(
|
||||
_("%(proc_name)s ready for work at %(id)s")
|
||||
% {"proc_name": self.process_name, "id": self.process_id}
|
||||
)
|
||||
|
||||
|
||||
def mark_start_task(self, task):
|
||||
# Log task creation and set process name
|
||||
# Get the function from the task
|
||||
func_name = get_func_repr(f)
|
||||
task_name = task["name"]
|
||||
task_desc = _("%(proc_name)s processing %(task_name)s '%(func_name)s'") % {
|
||||
"proc_name": proc_name,
|
||||
"func_name": func_name,
|
||||
"task_name": task_name,
|
||||
}
|
||||
if "group" in task:
|
||||
task_desc += f" [{task['group']}]"
|
||||
task_desc = (
|
||||
_("%(proc_name)s processing %(task_name)s '%(func_name)s'")
|
||||
% {
|
||||
"proc_name": self.process_name,
|
||||
"func_name": task.func_name,
|
||||
"task_name": task.name,
|
||||
}
|
||||
)
|
||||
if task.group is not None:
|
||||
task_desc += f" [{task.group}]"
|
||||
logger.info(task_desc)
|
||||
|
||||
if setproctitle:
|
||||
proc_title = f"qcluster {proc_name} processing {task_name} '{func_name}'"
|
||||
if "group" in task:
|
||||
proc_title += f" [{task['group']}]"
|
||||
proc_title = f"qcluster {self.process_name} processing {task.name} '{task.func_name}'"
|
||||
if task.group is not None:
|
||||
proc_title += f" [{task.group}]"
|
||||
setproctitle.setproctitle(proc_title)
|
||||
|
||||
# if it's not an instance try to get it from the string
|
||||
if not callable(f):
|
||||
# locate() returns None if f cannot be loaded
|
||||
f = pydoc.locate(f)
|
||||
if not task.get("sync", False):
|
||||
def processing_tasks(self, status: Value, pipe):
|
||||
self.mark_ready()
|
||||
while True:
|
||||
task = pipe.recv()
|
||||
if task == "STOP":
|
||||
logger.info(f"Worker {self.process_name} stopped processing")
|
||||
break
|
||||
# got a new task, let's mark it starting
|
||||
self.mark_start_task(task)
|
||||
|
||||
# make sure the function actually exists, before we try to run it
|
||||
try:
|
||||
if not task.is_callable:
|
||||
raise ValueError(f"Function {task.func_name} is not defined")
|
||||
except Exception as e:
|
||||
result = (f"{e} : {traceback.format_exc()}", False)
|
||||
if error_reporter:
|
||||
error_reporter.report()
|
||||
if task.sync:
|
||||
raise
|
||||
# stop here, move on to the next one
|
||||
continue
|
||||
|
||||
close_old_django_connections()
|
||||
timer_value = task.pop("timeout", timeout)
|
||||
# signal execution
|
||||
pre_execute.send(sender="django_q", func=f, task=task)
|
||||
# execute the payload
|
||||
timer.value = timer_value # Busy
|
||||
if timer.value != -1:
|
||||
timer.value += 3 # Add buffer so that guard doesn't kill the process on timeout before it gets processed
|
||||
|
||||
timeout_error = False
|
||||
try:
|
||||
if f is None:
|
||||
# raise a meaningfull error if task["func"] is not a valid function
|
||||
raise ValueError(f"Function {task['func']} is not defined")
|
||||
with TimeoutHandler(timer_value):
|
||||
res = f(*task["args"], **task["kwargs"])
|
||||
result = (res, True)
|
||||
except (Exception, TimeoutException) as e:
|
||||
if isinstance(e, TimeoutException):
|
||||
timeout_error = True
|
||||
result = (f"{e} : {traceback.format_exc()}", False)
|
||||
if error_reporter:
|
||||
error_reporter.report()
|
||||
if task.get("sync", False):
|
||||
post_execute_in_worker.send(sender="django_q", func=f, task=task)
|
||||
raise
|
||||
status.value = ProcessManager.Status.BUSY.value
|
||||
task = WorkerProcess.run_task(task)
|
||||
# Add task towards total
|
||||
self.task_count += 1
|
||||
|
||||
with timer.get_lock():
|
||||
# Process result
|
||||
task["result"] = result[0]
|
||||
task["success"] = result[1]
|
||||
task["stopped"] = timezone.now()
|
||||
post_execute_in_worker.send(sender="django_q", func=f, task=task)
|
||||
result_queue.put(task)
|
||||
if timeout_error:
|
||||
# force destroy process due to timeout
|
||||
timer.value = 0
|
||||
break
|
||||
|
||||
timer.value = -1 # Idle
|
||||
# Set to DONE so main process can pick it up
|
||||
status.value = ProcessManager.Status.DONE.value
|
||||
if setproctitle:
|
||||
setproctitle.setproctitle(f"qcluster {proc_name} idle")
|
||||
# Recycle
|
||||
if task_count == Conf.RECYCLE or rss_check():
|
||||
timer.value = -2 # Recycled
|
||||
setproctitle.setproctitle(f"qcluster {self.process_name} completed with task")
|
||||
|
||||
# Recreate a new process if this task has had the max amount of runs or exceeded resources
|
||||
if self.task_count == Conf.RECYCLE or self.rss_check():
|
||||
status.value = ProcessManager.Status.RECYCLE
|
||||
break
|
||||
logger.info(_("%(proc_name)s stopped doing work") % {"proc_name": proc_name})
|
||||
|
||||
pipe.send(task)
|
||||
|
||||
def rss_check():
|
||||
if Conf.MAX_RSS:
|
||||
if resource:
|
||||
return resource.getrusage(resource.RUSAGE_SELF).ru_maxrss >= Conf.MAX_RSS
|
||||
elif psutil:
|
||||
return psutil.Process().memory_info().rss >= Conf.MAX_RSS * 1024
|
||||
return False
|
||||
def rss_check(self):
|
||||
if Conf.MAX_RSS:
|
||||
if resource:
|
||||
return resource.getrusage(resource.RUSAGE_SELF).ru_maxrss >= Conf.MAX_RSS
|
||||
elif psutil:
|
||||
return psutil.Process().memory_info().rss >= Conf.MAX_RSS * 1024
|
||||
return False
|
||||
|
||||
@@ -49,9 +49,8 @@ Reference
|
||||
:param str group: an optional group name.
|
||||
:param bool cached: run this against the cache backend
|
||||
:param bool sync: execute this inline instead of asynchronous
|
||||
:param broker: an optional broker instance
|
||||
|
||||
.. py:class:: Chain(chain=None, group=None, cached=Conf.CACHED, sync=Conf.SYNC, broker=None)
|
||||
.. py:class:: Chain(chain=None, group=None, cached=Conf.CACHED, sync=Conf.SYNC)
|
||||
|
||||
A sequential chain of tasks. Acts as a convenient wrapper for :func:`async_chain`
|
||||
You can pass the task chain at construction or you can append individual tasks before running them.
|
||||
@@ -60,8 +59,6 @@ Reference
|
||||
:param str group: an optional group name.
|
||||
:param bool cached: run this against the cache backend
|
||||
:param bool sync: execute this inline instead of asynchronous
|
||||
:param bool sync: execute this inline instead of asynchronous
|
||||
:param broker: an optional broker instance
|
||||
|
||||
|
||||
.. py:method:: append(func, *args, **kwargs)
|
||||
|
||||
@@ -11,36 +11,36 @@ Start your cluster using Django's ``manage.py`` command::
|
||||
|
||||
You should see the cluster starting ::
|
||||
|
||||
10:57:40 [Q] INFO Q Cluster freddie-uncle-twenty-ten starting.
|
||||
10:57:40 [Q] INFO Process-ede257774c4444c980ab479f10947acc ready for work at 31784
|
||||
10:57:40 [Q] INFO Process-ed580482da3f42968230baa2e4253e42 ready for work at 31785
|
||||
10:57:40 [Q] INFO Process-8a370dc2bc1d49aa9864e517c9895f74 ready for work at 31786
|
||||
10:57:40 [Q] INFO Process-74912f9844264d1397c6e54476b530c0 ready for work at 31787
|
||||
10:57:40 [Q] INFO Process-b00edb26c6074a6189e5696c60aeb35b ready for work at 31788
|
||||
10:57:40 [Q] INFO Process-b0862965db04479f9784a26639ee51e0 ready for work at 31789
|
||||
10:57:40 [Q] INFO Process-7e8abbb8ca2d4d9bb20a937dd5e2872b ready for work at 31790
|
||||
10:57:40 [Q] INFO Process-b0862965db04479f9784a26639ee51e0 ready for work at 31791
|
||||
10:57:40 [Q] INFO Process-67fa9461ac034736a766cd813f617e62 monitoring at 31792
|
||||
10:57:40 [Q] INFO Process-eac052c646b2459797cee98bdb84c85d guarding cluster at 31783
|
||||
10:57:40 [Q] INFO Process-5d98deb19b1e4b2da2ef1e5bd6824f75 pushing tasks at 31793
|
||||
10:57:40 [Q] INFO Q Cluster freddie-uncle-twenty-ten running.
|
||||
10:57:40 [Q] INFO Q Cluster-31781 starting.
|
||||
10:57:40 [Q] INFO Process-1:1 ready for work at 31784
|
||||
10:57:40 [Q] INFO Process-1:2 ready for work at 31785
|
||||
10:57:40 [Q] INFO Process-1:3 ready for work at 31786
|
||||
10:57:40 [Q] INFO Process-1:4 ready for work at 31787
|
||||
10:57:40 [Q] INFO Process-1:5 ready for work at 31788
|
||||
10:57:40 [Q] INFO Process-1:6 ready for work at 31789
|
||||
10:57:40 [Q] INFO Process-1:7 ready for work at 31790
|
||||
10:57:40 [Q] INFO Process-1:8 ready for work at 31791
|
||||
10:57:40 [Q] INFO Process-1:9 monitoring at 31792
|
||||
10:57:40 [Q] INFO Process-1 guarding cluster at 31783
|
||||
10:57:40 [Q] INFO Process-1:10 pushing tasks at 31793
|
||||
10:57:40 [Q] INFO Q Cluster-31781 running.
|
||||
|
||||
|
||||
Stopping the cluster with ctrl-c or either the ``SIGTERM`` and ``SIGKILL`` signals, will initiate the :ref:`stop_procedure`::
|
||||
|
||||
16:44:12 [Q] INFO Q Cluster freddie-uncle-twenty-ten stopping.
|
||||
16:44:12 [Q] INFO Process-eac052c646b2459797cee98bdb84c85d stopping cluster processes
|
||||
16:44:13 [Q] INFO Process-5d98deb19b1e4b2da2ef1e5bd6824f75 stopped pushing tasks
|
||||
16:44:13 [Q] INFO Process-b0862965db04479f9784a26639ee51e0 stopped doing work
|
||||
16:44:13 [Q] INFO Process-7e8abbb8ca2d4d9bb20a937dd5e2872b stopped doing work
|
||||
16:44:13 [Q] INFO Process-b0862965db04479f9784a26639ee51e0 stopped doing work
|
||||
16:44:13 [Q] INFO Process-b00edb26c6074a6189e5696c60aeb35b stopped doing work
|
||||
16:44:13 [Q] INFO Process-74912f9844264d1397c6e54476b530c0 stopped doing work
|
||||
16:44:13 [Q] INFO Process-8a370dc2bc1d49aa9864e517c9895f74 stopped doing work
|
||||
16:44:13 [Q] INFO Process-ed580482da3f42968230baa2e4253e42 stopped doing work
|
||||
16:44:13 [Q] INFO Process-ede257774c4444c980ab479f10947acc stopped doing work
|
||||
16:44:14 [Q] INFO Process-67fa9461ac034736a766cd813f617e62 stopped monitoring results
|
||||
16:44:15 [Q] INFO Q Cluster freddie-uncle-twenty-ten has stopped.
|
||||
16:44:12 [Q] INFO Q Cluster-31781 stopping.
|
||||
16:44:12 [Q] INFO Process-1 stopping cluster processes
|
||||
16:44:13 [Q] INFO Process-1:10 stopped pushing tasks
|
||||
16:44:13 [Q] INFO Process-1:6 stopped doing work
|
||||
16:44:13 [Q] INFO Process-1:4 stopped doing work
|
||||
16:44:13 [Q] INFO Process-1:1 stopped doing work
|
||||
16:44:13 [Q] INFO Process-1:5 stopped doing work
|
||||
16:44:13 [Q] INFO Process-1:7 stopped doing work
|
||||
16:44:13 [Q] INFO Process-1:3 stopped doing work
|
||||
16:44:13 [Q] INFO Process-1:8 stopped doing work
|
||||
16:44:13 [Q] INFO Process-1:2 stopped doing work
|
||||
16:44:14 [Q] INFO Process-1:9 stopped monitoring results
|
||||
16:44:15 [Q] INFO Q Cluster-31781 has stopped.
|
||||
|
||||
The number of workers, optional timeouts, recycles and cpu_affinity can be controlled via the :doc:`configure` settings.
|
||||
|
||||
|
||||
@@ -73,9 +73,9 @@ author = "Ilan Steemers, Stan Triepels"
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = "1.11"
|
||||
version = "1.5"
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = "1.11.0"
|
||||
release = "1.5.1"
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
|
||||
@@ -79,8 +79,6 @@ timeout
|
||||
The number of seconds a worker is allowed to spend on a task before it's terminated. Defaults to ``None``, meaning it will never time out.
|
||||
Set this to something that makes sense for your project. Can be overridden for individual tasks.
|
||||
|
||||
Note: for systems that don't have `SIGALRM` available (e.g. Windows), it will not raise an error properly. It will kill the task, but it will keep retrying until it finishes within the given time.
|
||||
|
||||
See :ref:`retry` for details how to set values for timeout and retry.
|
||||
|
||||
.. _time_zone:
|
||||
@@ -350,7 +348,7 @@ To use MongoDB as a message broker you simply provide the connection information
|
||||
}
|
||||
}
|
||||
|
||||
The ``mongo`` dictionary can contain any of the parameters exposed by pymongo's `MongoClient <https://pymongo.readthedocs.io/en/stable/api/pymongo/mongo_client.html#pymongo.mongo_client.MongoClient>`__
|
||||
The ``mongo`` dictionary can contain any of the parameters exposed by pymongo's `MongoClient <https://api.mongodb.org/python/current/api/pymongo/mongo_client.html#pymongo.mongo_client.MongoClient>`__
|
||||
If you want to use a mongodb uri, you can supply it as the ``host`` parameter.
|
||||
|
||||
mongo_db
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
version: "3.0"
|
||||
services:
|
||||
docs:
|
||||
container_name: djangoq2-docs
|
||||
|
||||
@@ -328,7 +328,7 @@ Requires cache to be enabled. Save file in your Django project's root directory
|
||||
# All django stuff has to come after the setup:
|
||||
django.setup()
|
||||
|
||||
from django_q.status import Stat
|
||||
from django_q.monitor import Stat
|
||||
from django_q.conf import Conf
|
||||
|
||||
# Set host and port settings
|
||||
|
||||
@@ -27,7 +27,7 @@ Features
|
||||
- Rollbar and Sentry support
|
||||
|
||||
|
||||
Django Q2 is tested with: python 3.10, 3.11, 3.12, 3.13 and 3.14. Works with Django 5.2, 6.0 and 6.1
|
||||
Django Q2 is tested with: Python 3.8, 3.9, 3.10, and 3.11. Works with Django 3.2.x and 4.1.x.
|
||||
|
||||
Currently available in English, German and French.
|
||||
|
||||
|
||||
@@ -24,31 +24,15 @@ Installation
|
||||
|
||||
$ python manage.py qcluster
|
||||
|
||||
|
||||
Migrate from Django-Q to Django-Q2
|
||||
----------------------------------
|
||||
|
||||
If you have an application with django-q running right now, you can simply swap the libraries and you should be good to go.::
|
||||
|
||||
|
||||
$ pip uninstall django-q # you might have to uninstall django-q add-ons as well
|
||||
$ pip install django-q2
|
||||
|
||||
|
||||
Then migrate the database to get the latest tables/fields::
|
||||
|
||||
$ python manage.py migrate
|
||||
|
||||
|
||||
Requirements
|
||||
------------
|
||||
|
||||
Django Q2 is tested for Python 3.8, 3.9, 3.10, 3.11 and 3.12
|
||||
Django Q2 is tested for Python 3.8, 3.9, 3.10 and 3.11
|
||||
|
||||
- `Django <https://www.djangoproject.com>`__
|
||||
|
||||
Django Q2 aims to use as much of Django's standard offerings as possible.
|
||||
The code is tested against Django versions `3.2.x`, `4.1.x`, `4.2.x` and `5.0.x`.
|
||||
The code is tested against Django versions `3.2.x` and `4.1.x`.
|
||||
|
||||
- `Django-picklefield <https://github.com/gintas/django-picklefield>`__
|
||||
|
||||
@@ -57,10 +41,6 @@ Django Q2 is tested for Python 3.8, 3.9, 3.10, 3.11 and 3.12
|
||||
|
||||
Optional
|
||||
~~~~~~~~
|
||||
- `Blessed <https://github.com/jquast/blessed>`__ is used to display the statistics in the terminal::
|
||||
|
||||
$ pip install blessed
|
||||
|
||||
- `Redis-py <https://github.com/andymccurdy/redis-py>`__ client by Andy McCurdy is used to interface with both the Redis::
|
||||
|
||||
$ pip install redis
|
||||
@@ -115,11 +95,11 @@ Add-ons
|
||||
-------
|
||||
- `django-q-rollbar <https://github.com/danielwelch/django-q-rollbar>`__ is a Rollbar error reporter::
|
||||
|
||||
$ pip install django-q2[rollbar]
|
||||
$ pip install django-q[rollbar]
|
||||
|
||||
- `django-q-sentry <https://github.com/danielwelch/django-q-sentry>`__ is a Sentry error reporter::
|
||||
|
||||
$ pip install django-q2[sentry]
|
||||
$ pip install django-q[sentry]
|
||||
|
||||
- `django-q-email <https://github.com/joeyespo/django-q-email>`__ is a compatible Django email backend that will automatically async queue your emails.
|
||||
|
||||
@@ -145,7 +125,7 @@ Other known issues are:
|
||||
|
||||
Python
|
||||
~~~~~~
|
||||
Current tests are performed with 3.8, 3.9, 3.10, 3.11 and 3.12
|
||||
Current tests are performed with 3.8, 3.9, 3.10 and 3.11
|
||||
If you do encounter any regressions with earlier versions, please submit an issue on `github <https://github.com/GDay/django-q2>`__
|
||||
|
||||
Open-source packages
|
||||
@@ -155,8 +135,8 @@ You can reference the `requirements <https://github.com/GDay/django-q2/blob/mast
|
||||
|
||||
Django
|
||||
~~~~~~
|
||||
We strive to be compatible with the last two major version of Django.
|
||||
At the moment this means we support the 3.2.x, 4.1.x, 4.2.x and 5.0.x releases.
|
||||
We strive to be compatible with last two major version of Django.
|
||||
At the moment this means we support the 3.2.x and 4.1.x releases.
|
||||
|
||||
Since we are now no longer supporting Python 2, we can also not support older versions of Django that do not support Python >= 3.8
|
||||
Since we are now no longer supporting Python 2, we can also not support older versions of Django that do not support Python >= 3.6
|
||||
For this you can always use older releases, but they are no longer maintained.
|
||||
|
||||
@@ -3,10 +3,6 @@ Monitor
|
||||
.. py:currentmodule::django_q.monitor
|
||||
|
||||
|
||||
.. warning::
|
||||
Blessed needs to be installed to get this to work! See: https://pypi.org/project/blessed/
|
||||
|
||||
|
||||
The cluster monitor shows live information about all the Q clusters connected to your project.
|
||||
|
||||
Start the monitor with Django's `manage.py` command::
|
||||
@@ -111,7 +107,7 @@ You can check the status of your clusters straight from your code with the :clas
|
||||
|
||||
.. code:: python
|
||||
|
||||
from django_q.status import Stat
|
||||
from django_q.monitor import Stat
|
||||
|
||||
for stat in Stat.get_all():
|
||||
print(stat.cluster_id, stat.status)
|
||||
|
||||
@@ -13,12 +13,6 @@ Before enqueuing a task
|
||||
The ``django_q.signals.pre_enqueue`` signal is emitted before a task is
|
||||
enqueued. The task dictionary is given as the ``task`` argument.
|
||||
|
||||
After spawning a worker process
|
||||
"""""""""""""""""""""""""""""""
|
||||
|
||||
The ``django_q.signals.post_spawn`` signal is emitted after a worker process has
|
||||
spawned. The process name is given as the ``proc_name`` argument (string).
|
||||
|
||||
Before executing a task
|
||||
"""""""""""""""""""""""
|
||||
|
||||
@@ -32,14 +26,8 @@ executed by a worker. This signal provides two arguments:
|
||||
|
||||
After executing a task
|
||||
""""""""""""""""""""""
|
||||
- The ``django_q.signals.post_execute_in_worker`` signal is emitted after a task
|
||||
is executed by a worker and processed by the **worker**. It included the ``task``
|
||||
dictionary with the result. Note that this signal is **emitted from, and handled
|
||||
by, the worker process itself**, not the monitor, unlike the ``post_execute``
|
||||
signal below.
|
||||
- The ``django_q.signals.post_execute`` signal is emitted after a task is
|
||||
executed by a worker and processed by the **monitor**. It included the ``task``
|
||||
dictionary with the result.
|
||||
The ``django_q.signals.post_execute`` signal is emitted after a task is
|
||||
executed by a worker and processed by the monitor. It included the ``task`` dictionary with the result.
|
||||
|
||||
|
||||
Subscribing to a signal
|
||||
@@ -49,7 +37,7 @@ Connecting to a Django Q2 signal is done the same as any other Django
|
||||
signal::
|
||||
|
||||
from django.dispatch import receiver
|
||||
from django_q.signals import pre_enqueue, pre_execute, post_execute, post_spawn
|
||||
from django_q.signals import pre_enqueue, pre_execute, post_execute
|
||||
|
||||
@receiver(pre_enqueue)
|
||||
def my_pre_enqueue_callback(sender, task, **kwargs):
|
||||
@@ -62,13 +50,5 @@ signal::
|
||||
@receiver(post_execute)
|
||||
def my_post_execute_callback(sender, task, **kwargs):
|
||||
print(f"Task {task['name']} was executed with result {task['result']}")
|
||||
|
||||
@receiver(post_execute_in_worker)
|
||||
def my_post_execute_in_worker_callback(sender, func, task, **kwargs):
|
||||
print(f"Task {task['name']} was executed with result {task['result']}")
|
||||
|
||||
@receiver(post_spawn)
|
||||
def my_post_spawn_callback(sender, proc_name, **kwargs):
|
||||
print(f"Process {proc_name} has spawned")
|
||||
|
||||
|
||||
|
||||
@@ -75,10 +75,6 @@ broker
|
||||
""""""
|
||||
A broker instance, in case you want to control your own connections.
|
||||
|
||||
cluster
|
||||
""""""
|
||||
The name of the cluster. Only useful if you are using [alternative queues](https://django-q2.readthedocs.io/en/master/cluster.html#multiple-queues).
|
||||
|
||||
task_name
|
||||
"""""""""
|
||||
|
||||
@@ -125,7 +121,6 @@ Optionally you can use the :class:`AsyncTask` class to instantiate a task and ke
|
||||
a.run()
|
||||
|
||||
# wait indefinitely for the result and print it
|
||||
# don't let the task return `None` or it will wait indefinitely
|
||||
print(a.result(wait=-1))
|
||||
|
||||
# change the args
|
||||
@@ -245,7 +240,7 @@ Reference
|
||||
---------
|
||||
|
||||
.. py:function:: async_task(func, *args, hook=None, group=None, timeout=None,\
|
||||
save=None, sync=False, cached=False, broker=None, cluster=None, q_options=None, **kwargs)
|
||||
save=None, sync=False, cached=False, broker=None, q_options=None, **kwargs)
|
||||
|
||||
Puts a task in the cluster queue
|
||||
|
||||
@@ -259,7 +254,6 @@ Reference
|
||||
:param bool sync: If set to True, async_task will simulate a task execution
|
||||
:param cached: Output the result to the cache backend. Bool or timeout in seconds
|
||||
:param broker: Optional broker connection from :func:`brokers.get_broker`
|
||||
:param cluster: Optional cluster name if using alternative queues
|
||||
:param dict q_options: Options dict, overrides option keywords
|
||||
:param dict kwargs: Keyword arguments for the task function
|
||||
:returns: The uuid of the task
|
||||
@@ -270,7 +264,7 @@ Reference
|
||||
Gets the result of a previously executed task
|
||||
|
||||
:param str task_id: the uuid or name of the task
|
||||
:param int wait: optional milliseconds to wait for a result. -1 for indefinite, but be sure the result will not be `None` otherwise it will wait indefinitely!
|
||||
:param int wait: optional milliseconds to wait for a result. -1 for indefinite
|
||||
:param bool cached: run this against the cache backend.
|
||||
:returns: The result of the executed task
|
||||
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
"""
|
||||
ASGI config for exampleproject project.
|
||||
|
||||
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/5.1/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "exampleproject.settings")
|
||||
|
||||
application = get_asgi_application()
|
||||
@@ -1,134 +0,0 @@
|
||||
"""
|
||||
Django settings for exampleproject project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 5.1.1.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/5.1/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/5.1/ref/settings/
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = "django-insecure-)oouh93bjg+c=b!l5-*w)7et1l+!3nmp223rr^1r4v#jn&ow3f"
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = ["*"]
|
||||
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
"django.contrib.admin",
|
||||
"django.contrib.auth",
|
||||
"django.contrib.contenttypes",
|
||||
"django.contrib.sessions",
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
"django_q",
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
"django.middleware.csrf.CsrfViewMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
]
|
||||
|
||||
ROOT_URLCONF = "exampleproject.urls"
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||
"DIRS": [],
|
||||
"APP_DIRS": True,
|
||||
"OPTIONS": {
|
||||
"context_processors": [
|
||||
"django.template.context_processors.debug",
|
||||
"django.template.context_processors.request",
|
||||
"django.contrib.auth.context_processors.auth",
|
||||
"django.contrib.messages.context_processors.messages",
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = "exampleproject.wsgi.application"
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/5.1/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": BASE_DIR / "db.sqlite3",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
|
||||
},
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
||||
},
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
|
||||
},
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/5.1/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = "en-us"
|
||||
|
||||
TIME_ZONE = "UTC"
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/5.1/howto/static-files/
|
||||
|
||||
STATIC_URL = "static/"
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
|
||||
|
||||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||
|
||||
Q_CLUSTER = {
|
||||
"name": "DjangORM",
|
||||
"workers": 4,
|
||||
"timeout": 90,
|
||||
"retry": 120,
|
||||
"queue_limit": 50,
|
||||
"bulk": 10,
|
||||
"orm": "default",
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
"""
|
||||
URL configuration for exampleproject project.
|
||||
|
||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||
https://docs.djangoproject.com/en/5.1/topics/http/urls/
|
||||
Examples:
|
||||
Function views
|
||||
1. Add an import: from my_app import views
|
||||
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||
Class-based views
|
||||
1. Add an import: from other_app.views import Home
|
||||
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||
Including another URLconf
|
||||
1. Import the include() function: from django.urls import include, path
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
|
||||
from django.contrib import admin
|
||||
from django.urls import path
|
||||
|
||||
from exampleproject import views
|
||||
|
||||
urlpatterns = [
|
||||
path("admin/", admin.site.urls),
|
||||
path("new_task/", views.add_task, name="add_task"),
|
||||
path("result/<slug:task_id>/", views.get_result, name="get_result"),
|
||||
]
|
||||
@@ -1,31 +0,0 @@
|
||||
import time
|
||||
|
||||
from django.http import HttpResponse
|
||||
from django.urls import reverse
|
||||
|
||||
from django_q import tasks
|
||||
|
||||
|
||||
# internal function to be called with django_q
|
||||
def new_task(run_for_minutes):
|
||||
print("Task started")
|
||||
time.sleep(run_for_minutes)
|
||||
print("Task done")
|
||||
return True
|
||||
|
||||
|
||||
def add_task(request):
|
||||
task_id = tasks.async_task(new_task, 5)
|
||||
result_url = reverse("get_result", args=[task_id])
|
||||
return HttpResponse(
|
||||
f"Added async task with <a href='{result_url}'>Go to results</a>"
|
||||
)
|
||||
|
||||
|
||||
def get_result(request, task_id):
|
||||
task = tasks.fetch(task_id)
|
||||
if not task:
|
||||
msg = "Task running... please refresh after some time"
|
||||
else:
|
||||
msg = f"Async task result: {task.result}"
|
||||
return HttpResponse(msg)
|
||||
@@ -1,16 +0,0 @@
|
||||
"""
|
||||
WSGI config for exampleproject project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/5.1/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "exampleproject.settings")
|
||||
|
||||
application = get_wsgi_application()
|
||||
1685
poetry.lock
generated
Normal file
1685
poetry.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
137
pyproject.toml
137
pyproject.toml
@@ -1,86 +1,81 @@
|
||||
[build-system]
|
||||
requires = ["uv_build>=0.10.7,<=0.12.1"]
|
||||
build-backend = "uv_build"
|
||||
|
||||
[project]
|
||||
[tool.poetry]
|
||||
name = "django-q2"
|
||||
version = "1.11.0"
|
||||
version = "1.5.1"
|
||||
packages = [
|
||||
{ include = "django_q" },
|
||||
]
|
||||
description = "A multiprocessing distributed task queue for Django"
|
||||
authors = [
|
||||
{ name = "Stan Triepels", email = "django-q2@stantriepels.com" },
|
||||
{ name = "Ilan Steemers", email = "koed00@gmail.com" },
|
||||
]
|
||||
requires-python = ">=3.10,<4"
|
||||
readme = "README.rst"
|
||||
authors = ["Stan Triepels <django-q2@stantriepels.com>", "Ilan Steemers <koed00@gmail.com>"]
|
||||
maintainers = ["Stan Triepels <django-q2@stantriepels.com>"]
|
||||
license = "MIT"
|
||||
maintainers = [{ name = "Stan Triepels", email = "django-q2@stantriepels.com" }]
|
||||
keywords = [
|
||||
"django",
|
||||
"distributed",
|
||||
"multiprocessing",
|
||||
"queue",
|
||||
"scheduler",
|
||||
]
|
||||
readme = 'README.rst'
|
||||
|
||||
repository = "https://github.com/GDay/django-q2"
|
||||
homepage = "https://django-q2.readthedocs.org"
|
||||
documentation = "https://django-q2.readthedocs.org"
|
||||
|
||||
keywords = ["django", "distributed", "multiprocessing", "queue", "scheduler"]
|
||||
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Environment :: Web Environment",
|
||||
"Framework :: Django",
|
||||
"Intended Audience :: Developers",
|
||||
"Operating System :: POSIX",
|
||||
"Operating System :: MacOS",
|
||||
"Programming Language :: Python",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
"Programming Language :: Python :: 3.14",
|
||||
"Topic :: Internet :: WWW/HTTP",
|
||||
"Topic :: System :: Distributed Computing",
|
||||
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||
"Programming Language :: Python :: 3.14",
|
||||
]
|
||||
dependencies = [
|
||||
"django>=5.2",
|
||||
"django-picklefield>=3.1,<4",
|
||||
"importlib-metadata>=3.6 ; python_full_version < '3.10'",
|
||||
'Development Status :: 5 - Production/Stable',
|
||||
'Environment :: Web Environment',
|
||||
'Framework :: Django',
|
||||
'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: MIT License',
|
||||
'Operating System :: POSIX',
|
||||
'Operating System :: MacOS',
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python :: 3.8',
|
||||
'Programming Language :: Python :: 3.9',
|
||||
'Programming Language :: Python :: 3.10',
|
||||
'Programming Language :: Python :: 3.11',
|
||||
'Topic :: Internet :: WWW/HTTP',
|
||||
'Topic :: System :: Distributed Computing',
|
||||
'Topic :: Software Development :: Libraries :: Python Modules',
|
||||
]
|
||||
include = ['CHANGELOG.md']
|
||||
|
||||
[project.optional-dependencies]
|
||||
testing = [
|
||||
"django-redis",
|
||||
"croniter",
|
||||
"hiredis",
|
||||
"psutil",
|
||||
"iron-mq",
|
||||
"boto3",
|
||||
"pymongo",
|
||||
"blessed",
|
||||
"redis",
|
||||
"setproctitle",
|
||||
"pytest",
|
||||
"pytest-django",
|
||||
"pytest-cov",
|
||||
"ruff",
|
||||
]
|
||||
croniter = ["croniter"]
|
||||
[tool.poetry.plugins] # Optional super table
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://django-q2.readthedocs.org"
|
||||
Repository = "https://github.com/GDay/django-q2"
|
||||
Documentation = "https://django-q2.readthedocs.org"
|
||||
[tool.poetry.plugins."djangoq.errorreporters"]
|
||||
"rollbar" = "django_q_rollbar:Rollbar"
|
||||
"sentry" = "django_q_sentry:Sentry"
|
||||
|
||||
[project.entry-points."djangoq.errorreporters"]
|
||||
rollbar = "django_q_rollbar:Rollbar"
|
||||
sentry = "django_q_sentry:Sentry"
|
||||
|
||||
[tool.uv.build-backend]
|
||||
module-name = ["django_q"]
|
||||
module-root = ""
|
||||
source-include = ["CHANGELOG.md"]
|
||||
[tool.poetry.dependencies]
|
||||
python = ">=3.8, <4"
|
||||
django = ">=3.2"
|
||||
django-picklefield = "^3.1"
|
||||
|
||||
blessed = { version = "^1.19.1", optional = true }
|
||||
hiredis = { version = "^2.0.0", optional = true }
|
||||
psutil = { version = "^5.9.2", optional = true }
|
||||
django-redis = { version = "^5.2.0", optional = true }
|
||||
iron-mq = { version = "^0.9", optional = true }
|
||||
boto3 = { version = "^1.24.92", optional = true }
|
||||
pymongo = { version = "^4.2.0", optional = true }
|
||||
croniter = { version = "^1.3.7", optional = true }
|
||||
django-q-rollbar = {version = ">=0.1", optional = true}
|
||||
django-q-sentry = {version = ">=0.1", optional = true}
|
||||
redis = {version = "^4.3.4", optional = true}
|
||||
setproctitle = {version = "^1.3.2", optional = true}
|
||||
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
pytest = "^7.1.3"
|
||||
pytest-django = "^4.5.2"
|
||||
Sphinx = "^4.0.2"
|
||||
pytest-cov = "^4.0.0"
|
||||
black = "^22.10.0"
|
||||
isort = {extras = ["requirements_deprecated_finder"], version = "^5.10.1"}
|
||||
|
||||
[tool.poetry.extras]
|
||||
requires = ["poetry_core"]
|
||||
build-backend = ["poetry.core.masonry.api"]
|
||||
testing = ["django-redis", "croniter", "hiredis", "psutil", "iron-mq", "boto3", "pymongo", "blessed", "redis", "setproctitle"]
|
||||
rollbar = ["django-q-rollbar"]
|
||||
sentry = ["django-q-sentry"]
|
||||
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
|
||||
@@ -1,41 +1,40 @@
|
||||
asgiref==3.8.1 ; python_version >= "3.8" and python_version < "4" \
|
||||
--hash=sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47 \
|
||||
--hash=sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590
|
||||
backports-zoneinfo==0.2.1 ; python_version >= "3.8" and python_version < "3.9" \
|
||||
--hash=sha256:17746bd546106fa389c51dbea67c8b7c8f0d14b5526a579ca6ccf5ed72c526cf \
|
||||
--hash=sha256:1b13e654a55cd45672cb54ed12148cd33628f672548f373963b0bff67b217328 \
|
||||
--hash=sha256:1c5742112073a563c81f786e77514969acb58649bcdf6cdf0b4ed31a348d4546 \
|
||||
--hash=sha256:4a0f800587060bf8880f954dbef70de6c11bbe59c673c3d818921f042f9954a6 \
|
||||
--hash=sha256:5c144945a7752ca544b4b78c8c41544cdfaf9786f25fe5ffb10e838e19a27570 \
|
||||
--hash=sha256:7b0a64cda4145548fed9efc10322770f929b944ce5cee6c0dfe0c87bf4c0c8c9 \
|
||||
--hash=sha256:8439c030a11780786a2002261569bdf362264f605dfa4d65090b64b05c9f79a7 \
|
||||
--hash=sha256:8961c0f32cd0336fb8e8ead11a1f8cd99ec07145ec2931122faaac1c8f7fd987 \
|
||||
--hash=sha256:89a48c0d158a3cc3f654da4c2de1ceba85263fafb861b98b59040a5086259722 \
|
||||
--hash=sha256:a76b38c52400b762e48131494ba26be363491ac4f9a04c1b7e92483d169f6582 \
|
||||
--hash=sha256:da6013fd84a690242c310d77ddb8441a559e9cb3d3d59ebac9aca1a57b2e18bc \
|
||||
--hash=sha256:e55b384612d93be96506932a786bbcde5a2db7a9e6a4bb4bffe8b733f5b9036b \
|
||||
--hash=sha256:e81b76cace8eda1fca50e345242ba977f9be6ae3945af8d46326d776b4cf78d1 \
|
||||
--hash=sha256:e8236383a20872c0cdf5a62b554b27538db7fa1bbec52429d8d106effbaeca08 \
|
||||
--hash=sha256:f04e857b59d9d1ccc39ce2da1021d196e47234873820cbeaad210724b1ee28ac \
|
||||
--hash=sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2
|
||||
django-picklefield==3.2 ; python_version >= "3.8" and python_version < "4" \
|
||||
--hash=sha256:aa463f5d79d497dbe789f14b45180f00a51d0d670067d0729f352a3941cdfa4d \
|
||||
--hash=sha256:e9a73539d110f69825d9320db18bcb82e5189ff48dbed41821c026a20497764c
|
||||
django==4.2.11 ; python_version >= "3.8" and python_version < "4" \
|
||||
--hash=sha256:6e6ff3db2d8dd0c986b4eec8554c8e4f919b5c1ff62a5b4390c17aff2ed6e5c4 \
|
||||
--hash=sha256:ddc24a0a8280a0430baa37aff11f28574720af05888c62b7cfe71d219f4599d3
|
||||
importlib-metadata==7.1.0 ; python_version >= "3.8" and python_version < "3.10" \
|
||||
--hash=sha256:30962b96c0c223483ed6cc7280e7f0199feb01a0e40cfae4d4450fc6fab1f570 \
|
||||
--hash=sha256:b78938b926ee8d5f020fc4772d487045805a55ddbad2ecf21c6d60938dc7fcd2
|
||||
sqlparse==0.5.0 ; python_version >= "3.8" and python_version < "4" \
|
||||
--hash=sha256:714d0a4932c059d16189f58ef5411ec2287a4360f17cdd0edd2d09d4c5087c93 \
|
||||
--hash=sha256:c204494cd97479d0e39f28c93d46c0b2d5959c7b9ab904762ea6c7af211c8663
|
||||
typing-extensions==4.11.0 ; python_version >= "3.8" and python_version < "3.11" \
|
||||
--hash=sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0 \
|
||||
--hash=sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a
|
||||
tzdata==2024.1 ; python_version >= "3.8" and python_version < "4" and sys_platform == "win32" \
|
||||
--hash=sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd \
|
||||
--hash=sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252
|
||||
zipp==3.18.1 ; python_version >= "3.8" and python_version < "3.10" \
|
||||
--hash=sha256:206f5a15f2af3dbaee80769fb7dc6f249695e940acca08dfb2a4769fe61e538b \
|
||||
--hash=sha256:2884ed22e7d8961de1c9a05142eb69a247f120291bc0206a00a7642f09b5b715
|
||||
ansicon==1.89.0; platform_system == "Windows" and python_version >= "2.7" \
|
||||
--hash=sha256:f1def52d17f65c2c9682cf8370c03f541f410c1752d6a14029f97318e4b9dfec \
|
||||
--hash=sha256:e4d039def5768a47e4afec8e89e83ec3ae5a26bf00ad851f914d1240b444d2b1
|
||||
arrow==1.1.1; python_version >= "3.6" \
|
||||
--hash=sha256:77a60a4db5766d900a2085ce9074c5c7b8e2c99afeaa98ad627637ff6f292510 \
|
||||
--hash=sha256:dee7602f6c60e3ec510095b5e301441bc56288cb8f51def14dcb3079f623823a
|
||||
asgiref==3.3.4; python_version >= "3.6" \
|
||||
--hash=sha256:92906c611ce6c967347bbfea733f13d6313901d54dcca88195eaeb52b2a8e8ee \
|
||||
--hash=sha256:d1216dfbdfb63826470995d31caed36225dcaf34f182e0fa257a4dd9e86f1b78
|
||||
blessed==1.18.1; python_version >= "2.7" \
|
||||
--hash=sha256:dd7c0d33db9a2e7f597b446996484d0ed46e1586239db064fb5025008937dcae \
|
||||
--hash=sha256:8b09936def6bc06583db99b65636b980075733e13550cb6af262ce724a55da23
|
||||
django-picklefield==3.0.1; python_version >= "3" \
|
||||
--hash=sha256:15ccba592ca953b9edf9532e64640329cd47b136b7f8f10f2939caa5f9ce4287 \
|
||||
--hash=sha256:3c702a54fde2d322fe5b2f39b8f78d9f655b8f77944ab26f703be6c0ed335a35
|
||||
django==3.2.4; python_version >= "3.6" \
|
||||
--hash=sha256:ea735cbbbb3b2fba6d4da4784a0043d84c67c92f1fdf15ad6db69900e792c10f \
|
||||
--hash=sha256:66c9d8db8cc6fe938a28b7887c1596e42d522e27618562517cc8929eb7e7f296
|
||||
jinxed==1.1.0; platform_system == "Windows" and python_version >= "2.7" \
|
||||
--hash=sha256:6a61ccf963c16aa885304f27e6e5693783676897cea0c7f223270c8b8e78baf8 \
|
||||
--hash=sha256:d8f1731f134e9e6b04d95095845ae6c10eb15cb223a5f0cabdea87d4a279c305
|
||||
python-dateutil==2.8.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.6" \
|
||||
--hash=sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c \
|
||||
--hash=sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a
|
||||
pytz==2021.1; python_version >= "3.6" \
|
||||
--hash=sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798 \
|
||||
--hash=sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da
|
||||
six==1.16.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.6" \
|
||||
--hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 \
|
||||
--hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926
|
||||
sqlparse==0.4.1; python_version >= "3.6" \
|
||||
--hash=sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0 \
|
||||
--hash=sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8
|
||||
typing-extensions==3.10.0.0; python_version < "3.8" and python_version >= "3.6" \
|
||||
--hash=sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497 \
|
||||
--hash=sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84 \
|
||||
--hash=sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342
|
||||
wcwidth==0.2.5; python_version >= "2.7" \
|
||||
--hash=sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784 \
|
||||
--hash=sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
redis:
|
||||
image: redis:latest
|
||||
@@ -13,41 +15,15 @@ services:
|
||||
networks:
|
||||
- main
|
||||
|
||||
aws:
|
||||
container_name: aws
|
||||
image: localstack/localstack:3.4.0
|
||||
ports:
|
||||
- "127.0.0.1:4566:4566" # LocalStack Gateway
|
||||
- "127.0.0.1:4510-4559:4510-4559" # External services port range
|
||||
environment:
|
||||
AWS_DEFAULT_REGION: ${AWS_DEFAULT_REGION:-us-west-2}
|
||||
DEFAULT_REGION: ${AWS_DEFAULT_REGION:-us-west-2}
|
||||
SQS_ENDPOINT_STRATEGY: path
|
||||
SERVICES: sqs
|
||||
LOCALSTACK_HOST: aws
|
||||
DEBUG: 1
|
||||
LS_LOG: trace
|
||||
volumes:
|
||||
- ./containers/localstack:/etc/localstack/init/ready.d
|
||||
networks:
|
||||
- main
|
||||
|
||||
django-q2:
|
||||
build:
|
||||
dockerfile: ./Dockerfile.dev
|
||||
context: .
|
||||
environment:
|
||||
AWS_ENDPOINT_URL: http://aws:4566
|
||||
AWS_REGION: ${AWS_REGION:-us-west-2}
|
||||
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID:-test}
|
||||
AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY:-test}
|
||||
AWS_DEFAULT_REGION: ${AWS_DEFAULT_REGION:-us-west-2}
|
||||
volumes:
|
||||
- .:/app
|
||||
depends_on:
|
||||
- redis
|
||||
- mongo
|
||||
- aws
|
||||
networks:
|
||||
- main
|
||||
|
||||
|
||||
993
uv.lock
generated
993
uv.lock
generated
@@ -1,993 +0,0 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.10, <4"
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.12'",
|
||||
"python_full_version < '3.12'",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ansicon"
|
||||
version = "1.89.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b6/e2/1c866404ddbd280efedff4a9f15abfe943cb83cde6e895022370f3a61f85/ansicon-1.89.0.tar.gz", hash = "sha256:e4d039def5768a47e4afec8e89e83ec3ae5a26bf00ad851f914d1240b444d2b1", size = 67312, upload-time = "2019-04-29T20:23:57.314Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/75/f9/f1c10e223c7b56a38109a3f2eb4e7fe9a757ea3ed3a166754fb30f65e466/ansicon-1.89.0-py2.py3-none-any.whl", hash = "sha256:f1def52d17f65c2c9682cf8370c03f541f410c1752d6a14029f97318e4b9dfec", size = 63675, upload-time = "2019-04-29T20:23:53.83Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "asgiref"
|
||||
version = "3.11.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/63/40/f03da1264ae8f7cfdbf9146542e5e7e8100a4c66ab48e791df9a03d3f6c0/asgiref-3.11.1.tar.gz", hash = "sha256:5f184dc43b7e763efe848065441eac62229c9f7b0475f41f80e207a114eda4ce", size = 38550, upload-time = "2026-02-03T13:30:14.33Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/0a/a72d10ed65068e115044937873362e6e32fab1b7dce0046aeb224682c989/asgiref-3.11.1-py3-none-any.whl", hash = "sha256:e8667a091e69529631969fd45dc268fa79b99c92c5fcdda727757e52146ec133", size = 24345, upload-time = "2026-02-03T13:30:13.039Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-timeout"
|
||||
version = "5.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274, upload-time = "2024-11-06T16:41:39.6Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blessed"
|
||||
version = "1.30.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "jinxed", marker = "sys_platform == 'win32'" },
|
||||
{ name = "wcwidth" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/dd/19/e926a0dbbf93c7aeb15d4dfff0d0e3de02653b3ba540b687307d0819c1ff/blessed-1.30.0.tar.gz", hash = "sha256:4d547019d7b40fc5420ea2ba2bc180fdccc31d6715298e2b49ffa7b020d44667", size = 13948932, upload-time = "2026-02-06T19:40:23.541Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/64/b0/8d87c7c8015ce8d4b2c5ee7a82a1d955f10138322c4f0cb387d7d2c1b2e7/blessed-1.30.0-py3-none-any.whl", hash = "sha256:4061a9f10dd22798716c2548ba36385af6a29d856c897f367c6ccc927e0b3a5a", size = 98399, upload-time = "2026-02-06T19:40:20.815Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "boto3"
|
||||
version = "1.42.59"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "botocore" },
|
||||
{ name = "jmespath" },
|
||||
{ name = "s3transfer" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b0/4e/499cb52aaee9468c346bcc1158965e24e72b4e2a20052725b680e0ac949b/boto3-1.42.59.tar.gz", hash = "sha256:6c4a14a4eb37b58a9048901bdeefbe1c529638b73e8f55413319a25f010ca211", size = 112725, upload-time = "2026-02-27T20:25:33.228Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/17/c0/22d868b9408dc5a33935a72896ec8d638b2766c459668d1b37c3e5ac2066/boto3-1.42.59-py3-none-any.whl", hash = "sha256:7a66e3e8e2087ea4403e135e9de592e6d63fc9a91080d8dac415bb74df873a72", size = 140557, upload-time = "2026-02-27T20:25:31.774Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "botocore"
|
||||
version = "1.42.59"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "jmespath" },
|
||||
{ name = "python-dateutil" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/45/ae/50fb33bdf1911c216d50f98d989dd032a506f054cf829ebd737c6fa7e3e6/botocore-1.42.59.tar.gz", hash = "sha256:5314f19e1da8fc0ebc41bdb8bbe17c9a7397d87f4d887076ac8bdef972a34138", size = 14950271, upload-time = "2026-02-27T20:25:20.614Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/59/df/9d52819e0d804ead073d53ab1823bc0f0cb172a250fba31107b0b43fbb04/botocore-1.42.59-py3-none-any.whl", hash = "sha256:d2f2ff7ecc31e86ef46b5daee112cfbca052c13801285fb23af909f7bff5b657", size = 14619293, upload-time = "2026-02-27T20:25:17.455Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2026.2.25"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "charset-normalizer"
|
||||
version = "3.4.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" },
|
||||
]
|
||||
|
||||
[[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, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "coverage"
|
||||
version = "7.13.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/24/56/95b7e30fa389756cb56630faa728da46a27b8c6eb46f9d557c68fff12b65/coverage-7.13.4.tar.gz", hash = "sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91", size = 827239, upload-time = "2026-02-09T12:59:03.86Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/44/d4/7827d9ffa34d5d4d752eec907022aa417120936282fc488306f5da08c292/coverage-7.13.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fc31c787a84f8cd6027eba44010517020e0d18487064cd3d8968941856d1415", size = 219152, upload-time = "2026-02-09T12:56:11.974Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/b0/d69df26607c64043292644dbb9dc54b0856fabaa2cbb1eeee3331cc9e280/coverage-7.13.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a32ebc02a1805adf637fc8dec324b5cdacd2e493515424f70ee33799573d661b", size = 219667, upload-time = "2026-02-09T12:56:13.33Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/a4/c1523f7c9e47b2271dbf8c2a097e7a1f89ef0d66f5840bb59b7e8814157b/coverage-7.13.4-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e24f9156097ff9dc286f2f913df3a7f63c0e333dcafa3c196f2c18b4175ca09a", size = 246425, upload-time = "2026-02-09T12:56:14.552Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/02/aa7ec01d1a5023c4b680ab7257f9bfde9defe8fdddfe40be096ac19e8177/coverage-7.13.4-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8041b6c5bfdc03257666e9881d33b1abc88daccaf73f7b6340fb7946655cd10f", size = 248229, upload-time = "2026-02-09T12:56:16.31Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/98/85aba0aed5126d896162087ef3f0e789a225697245256fc6181b95f47207/coverage-7.13.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a09cfa6a5862bc2fc6ca7c3def5b2926194a56b8ab78ffcf617d28911123012", size = 250106, upload-time = "2026-02-09T12:56:18.024Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/72/1db59bd67494bc162e3e4cd5fbc7edba2c7026b22f7c8ef1496d58c2b94c/coverage-7.13.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:296f8b0af861d3970c2a4d8c91d48eb4dd4771bcef9baedec6a9b515d7de3def", size = 252021, upload-time = "2026-02-09T12:56:19.272Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/97/72899c59c7066961de6e3daa142d459d47d104956db43e057e034f015c8a/coverage-7.13.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e101609bcbbfb04605ea1027b10dc3735c094d12d40826a60f897b98b1c30256", size = 247114, upload-time = "2026-02-09T12:56:21.051Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/1f/f1885573b5970235e908da4389176936c8933e86cb316b9620aab1585fa2/coverage-7.13.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:aa3feb8db2e87ff5e6d00d7e1480ae241876286691265657b500886c98f38bda", size = 248143, upload-time = "2026-02-09T12:56:22.585Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/cf/e80390c5b7480b722fa3e994f8202807799b85bc562aa4f1dde209fbb7be/coverage-7.13.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4fc7fa81bbaf5a02801b65346c8b3e657f1d93763e58c0abdf7c992addd81a92", size = 246152, upload-time = "2026-02-09T12:56:23.748Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/bf/f89a8350d85572f95412debb0fb9bb4795b1d5b5232bd652923c759e787b/coverage-7.13.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:33901f604424145c6e9c2398684b92e176c0b12df77d52db81c20abd48c3794c", size = 249959, upload-time = "2026-02-09T12:56:25.209Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/6e/612a02aece8178c818df273e8d1642190c4875402ca2ba74514394b27aba/coverage-7.13.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:bb28c0f2cf2782508a40cec377935829d5fcc3ad9a3681375af4e84eb34b6b58", size = 246416, upload-time = "2026-02-09T12:56:26.475Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/98/b5afc39af67c2fa6786b03c3a7091fc300947387ce8914b096db8a73d67a/coverage-7.13.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9d107aff57a83222ddbd8d9ee705ede2af2cc926608b57abed8ef96b50b7e8f9", size = 247025, upload-time = "2026-02-09T12:56:27.727Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/30/2bba8ef0682d5bd210c38fe497e12a06c9f8d663f7025e9f5c2c31ce847d/coverage-7.13.4-cp310-cp310-win32.whl", hash = "sha256:a6f94a7d00eb18f1b6d403c91a88fd58cfc92d4b16080dfdb774afc8294469bf", size = 221758, upload-time = "2026-02-09T12:56:29.051Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/13/331f94934cf6c092b8ea59ff868eb587bc8fe0893f02c55bc6c0183a192e/coverage-7.13.4-cp310-cp310-win_amd64.whl", hash = "sha256:2cb0f1e000ebc419632bbe04366a8990b6e32c4e0b51543a6484ffe15eaeda95", size = 222693, upload-time = "2026-02-09T12:56:30.366Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/ad/b59e5b451cf7172b8d1043dc0fa718f23aab379bc1521ee13d4bd9bfa960/coverage-7.13.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d490ba50c3f35dd7c17953c68f3270e7ccd1c6642e2d2afe2d8e720b98f5a053", size = 219278, upload-time = "2026-02-09T12:56:31.673Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/17/0cb7ca3de72e5f4ef2ec2fa0089beafbcaaaead1844e8b8a63d35173d77d/coverage-7.13.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:19bc3c88078789f8ef36acb014d7241961dbf883fd2533d18cb1e7a5b4e28b11", size = 219783, upload-time = "2026-02-09T12:56:33.104Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/63/325d8e5b11e0eaf6d0f6a44fad444ae58820929a9b0de943fa377fe73e85/coverage-7.13.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3998e5a32e62fdf410c0dbd3115df86297995d6e3429af80b8798aad894ca7aa", size = 250200, upload-time = "2026-02-09T12:56:34.474Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/53/c16972708cbb79f2942922571a687c52bd109a7bd51175aeb7558dff2236/coverage-7.13.4-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e264226ec98e01a8e1054314af91ee6cde0eacac4f465cc93b03dbe0bce2fd7", size = 252114, upload-time = "2026-02-09T12:56:35.749Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/c2/7ab36d8b8cc412bec9ea2d07c83c48930eb4ba649634ba00cb7e4e0f9017/coverage-7.13.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3aa4e7b9e416774b21797365b358a6e827ffadaaca81b69ee02946852449f00", size = 254220, upload-time = "2026-02-09T12:56:37.796Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/4d/cf52c9a3322c89a0e6febdfbc83bb45c0ed3c64ad14081b9503adee702e7/coverage-7.13.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:71ca20079dd8f27fcf808817e281e90220475cd75115162218d0e27549f95fef", size = 256164, upload-time = "2026-02-09T12:56:39.016Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/e9/eb1dd17bd6de8289df3580e967e78294f352a5df8a57ff4671ee5fc3dcd0/coverage-7.13.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e2f25215f1a359ab17320b47bcdaca3e6e6356652e8256f2441e4ef972052903", size = 250325, upload-time = "2026-02-09T12:56:40.668Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/07/8c1542aa873728f72267c07278c5cc0ec91356daf974df21335ccdb46368/coverage-7.13.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d65b2d373032411e86960604dc4edac91fdfb5dca539461cf2cbe78327d1e64f", size = 251913, upload-time = "2026-02-09T12:56:41.97Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/d7/c62e2c5e4483a748e27868e4c32ad3daa9bdddbba58e1bc7a15e252baa74/coverage-7.13.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94eb63f9b363180aff17de3e7c8760c3ba94664ea2695c52f10111244d16a299", size = 249974, upload-time = "2026-02-09T12:56:43.323Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/9f/4c5c015a6e98ced54efd0f5cf8d31b88e5504ecb6857585fc0161bb1e600/coverage-7.13.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e856bf6616714c3a9fbc270ab54103f4e685ba236fa98c054e8f87f266c93505", size = 253741, upload-time = "2026-02-09T12:56:45.155Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/59/0f4eef89b9f0fcd9633b5d350016f54126ab49426a70ff4c4e87446cabdc/coverage-7.13.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:65dfcbe305c3dfe658492df2d85259e0d79ead4177f9ae724b6fb245198f55d6", size = 249695, upload-time = "2026-02-09T12:56:46.636Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/2c/b7476f938deb07166f3eb281a385c262675d688ff4659ad56c6c6b8e2e70/coverage-7.13.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b507778ae8a4c915436ed5c2e05b4a6cecfa70f734e19c22a005152a11c7b6a9", size = 250599, upload-time = "2026-02-09T12:56:48.13Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/34/c3420709d9846ee3785b9f2831b4d94f276f38884032dca1457fa83f7476/coverage-7.13.4-cp311-cp311-win32.whl", hash = "sha256:784fc3cf8be001197b652d51d3fd259b1e2262888693a4636e18879f613a62a9", size = 221780, upload-time = "2026-02-09T12:56:50.479Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/08/3d9c8613079d2b11c185b865de9a4c1a68850cfda2b357fae365cf609f29/coverage-7.13.4-cp311-cp311-win_amd64.whl", hash = "sha256:2421d591f8ca05b308cf0092807308b2facbefe54af7c02ac22548b88b95c98f", size = 222715, upload-time = "2026-02-09T12:56:51.815Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/1a/54c3c80b2f056164cc0a6cdcb040733760c7c4be9d780fe655f356f433e4/coverage-7.13.4-cp311-cp311-win_arm64.whl", hash = "sha256:79e73a76b854d9c6088fe5d8b2ebe745f8681c55f7397c3c0a016192d681045f", size = 221385, upload-time = "2026-02-09T12:56:53.194Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/81/4ce2fdd909c5a0ed1f6dedb88aa57ab79b6d1fbd9b588c1ac7ef45659566/coverage-7.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02231499b08dabbe2b96612993e5fc34217cdae907a51b906ac7fca8027a4459", size = 219449, upload-time = "2026-02-09T12:56:54.889Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/96/5238b1efc5922ddbdc9b0db9243152c09777804fb7c02ad1741eb18a11c0/coverage-7.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40aa8808140e55dc022b15d8aa7f651b6b3d68b365ea0398f1441e0b04d859c3", size = 219810, upload-time = "2026-02-09T12:56:56.33Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/72/2f372b726d433c9c35e56377cf1d513b4c16fe51841060d826b95caacec1/coverage-7.13.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5b856a8ccf749480024ff3bd7310adaef57bf31fd17e1bfc404b7940b6986634", size = 251308, upload-time = "2026-02-09T12:56:57.858Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/a0/2ea570925524ef4e00bb6c82649f5682a77fac5ab910a65c9284de422600/coverage-7.13.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c048ea43875fbf8b45d476ad79f179809c590ec7b79e2035c662e7afa3192e3", size = 254052, upload-time = "2026-02-09T12:56:59.754Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/ac/45dc2e19a1939098d783c846e130b8f862fbb50d09e0af663988f2f21973/coverage-7.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7b38448866e83176e28086674fe7368ab8590e4610fb662b44e345b86d63ffa", size = 255165, upload-time = "2026-02-09T12:57:01.287Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/4d/26d236ff35abc3b5e63540d3386e4c3b192168c1d96da5cb2f43c640970f/coverage-7.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de6defc1c9badbf8b9e67ae90fd00519186d6ab64e5cc5f3d21359c2a9b2c1d3", size = 257432, upload-time = "2026-02-09T12:57:02.637Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/55/14a966c757d1348b2e19caf699415a2a4c4f7feaa4bbc6326a51f5c7dd1b/coverage-7.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7eda778067ad7ffccd23ecffce537dface96212576a07924cbf0d8799d2ded5a", size = 251716, upload-time = "2026-02-09T12:57:04.056Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/33/50116647905837c66d28b2af1321b845d5f5d19be9655cb84d4a0ea806b4/coverage-7.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e87f6c587c3f34356c3759f0420693e35e7eb0e2e41e4c011cb6ec6ecbbf1db7", size = 253089, upload-time = "2026-02-09T12:57:05.503Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/b4/8efb11a46e3665d92635a56e4f2d4529de6d33f2cb38afd47d779d15fc99/coverage-7.13.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8248977c2e33aecb2ced42fef99f2d319e9904a36e55a8a68b69207fb7e43edc", size = 251232, upload-time = "2026-02-09T12:57:06.879Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/24/8cd73dd399b812cc76bb0ac260e671c4163093441847ffe058ac9fda1e32/coverage-7.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:25381386e80ae727608e662474db537d4df1ecd42379b5ba33c84633a2b36d47", size = 255299, upload-time = "2026-02-09T12:57:08.245Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/94/0a4b12f1d0e029ce1ccc1c800944a9984cbe7d678e470bb6d3c6bc38a0da/coverage-7.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ee756f00726693e5ba94d6df2bdfd64d4852d23b09bb0bc700e3b30e6f333985", size = 250796, upload-time = "2026-02-09T12:57:10.142Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/44/6002fbf88f6698ca034360ce474c406be6d5a985b3fdb3401128031eef6b/coverage-7.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fdfc1e28e7c7cdce44985b3043bc13bbd9c747520f94a4d7164af8260b3d91f0", size = 252673, upload-time = "2026-02-09T12:57:12.197Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/c6/a0279f7c00e786be75a749a5674e6fa267bcbd8209cd10c9a450c655dfa7/coverage-7.13.4-cp312-cp312-win32.whl", hash = "sha256:01d4cbc3c283a17fc1e42d614a119f7f438eabb593391283adca8dc86eff1246", size = 221990, upload-time = "2026-02-09T12:57:14.085Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/4e/c0a25a425fcf5557d9abd18419c95b63922e897bc86c1f327f155ef234a9/coverage-7.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:9401ebc7ef522f01d01d45532c68c5ac40fb27113019b6b7d8b208f6e9baa126", size = 222800, upload-time = "2026-02-09T12:57:15.944Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/ac/92da44ad9a6f4e3a7debd178949d6f3769bedca33830ce9b1dcdab589a37/coverage-7.13.4-cp312-cp312-win_arm64.whl", hash = "sha256:b1ec7b6b6e93255f952e27ab58fbc68dcc468844b16ecbee881aeb29b6ab4d8d", size = 221415, upload-time = "2026-02-09T12:57:17.497Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/23/aad45061a31677d68e47499197a131eea55da4875d16c1f42021ab963503/coverage-7.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b66a2da594b6068b48b2692f043f35d4d3693fb639d5ea8b39533c2ad9ac3ab9", size = 219474, upload-time = "2026-02-09T12:57:19.332Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/70/9b8b67a0945f3dfec1fd896c5cefb7c19d5a3a6d74630b99a895170999ae/coverage-7.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3599eb3992d814d23b35c536c28df1a882caa950f8f507cef23d1cbf334995ac", size = 219844, upload-time = "2026-02-09T12:57:20.66Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/fd/7e859f8fab324cef6c4ad7cff156ca7c489fef9179d5749b0c8d321281c2/coverage-7.13.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:93550784d9281e374fb5a12bf1324cc8a963fd63b2d2f223503ef0fd4aa339ea", size = 250832, upload-time = "2026-02-09T12:57:22.007Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/dc/b2442d10020c2f52617828862d8b6ee337859cd8f3a1f13d607dddda9cf7/coverage-7.13.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b720ce6a88a2755f7c697c23268ddc47a571b88052e6b155224347389fdf6a3b", size = 253434, upload-time = "2026-02-09T12:57:23.339Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/88/6728a7ad17428b18d836540630487231f5470fb82454871149502f5e5aa2/coverage-7.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b322db1284a2ed3aa28ffd8ebe3db91c929b7a333c0820abec3d838ef5b3525", size = 254676, upload-time = "2026-02-09T12:57:24.774Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/bc/21244b1b8cedf0dff0a2b53b208015fe798d5f2a8d5348dbfece04224fff/coverage-7.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4594c67d8a7c89cf922d9df0438c7c7bb022ad506eddb0fdb2863359ff78242", size = 256807, upload-time = "2026-02-09T12:57:26.125Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/a0/ddba7ed3251cff51006737a727d84e05b61517d1784a9988a846ba508877/coverage-7.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:53d133df809c743eb8bce33b24bcababb371f4441340578cd406e084d94a6148", size = 251058, upload-time = "2026-02-09T12:57:27.614Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/55/e289addf7ff54d3a540526f33751951bf0878f3809b47f6dfb3def69c6f7/coverage-7.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76451d1978b95ba6507a039090ba076105c87cc76fc3efd5d35d72093964d49a", size = 252805, upload-time = "2026-02-09T12:57:29.066Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/4e/cc276b1fa4a59be56d96f1dabddbdc30f4ba22e3b1cd42504c37b3313255/coverage-7.13.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7f57b33491e281e962021de110b451ab8a24182589be17e12a22c79047935e23", size = 250766, upload-time = "2026-02-09T12:57:30.522Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/44/1093b8f93018f8b41a8cf29636c9292502f05e4a113d4d107d14a3acd044/coverage-7.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1731dc33dc276dafc410a885cbf5992f1ff171393e48a21453b78727d090de80", size = 254923, upload-time = "2026-02-09T12:57:31.946Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/55/ea2796da2d42257f37dbea1aab239ba9263b31bd91d5527cdd6db5efe174/coverage-7.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:bd60d4fe2f6fa7dff9223ca1bbc9f05d2b6697bc5961072e5d3b952d46e1b1ea", size = 250591, upload-time = "2026-02-09T12:57:33.842Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/fa/7c4bb72aacf8af5020675aa633e59c1fbe296d22aed191b6a5b711eb2bc7/coverage-7.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9181a3ccead280b828fae232df12b16652702b49d41e99d657f46cc7b1f6ec7a", size = 252364, upload-time = "2026-02-09T12:57:35.743Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/38/a8d2ec0146479c20bbaa7181b5b455a0c41101eed57f10dd19a78ab44c80/coverage-7.13.4-cp313-cp313-win32.whl", hash = "sha256:f53d492307962561ac7de4cd1de3e363589b000ab69617c6156a16ba7237998d", size = 222010, upload-time = "2026-02-09T12:57:37.25Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/0c/dbfafbe90a185943dcfbc766fe0e1909f658811492d79b741523a414a6cc/coverage-7.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:e6f70dec1cc557e52df5306d051ef56003f74d56e9c4dd7ddb07e07ef32a84dd", size = 222818, upload-time = "2026-02-09T12:57:38.734Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/d1/934918a138c932c90d78301f45f677fb05c39a3112b96fd2c8e60503cdc7/coverage-7.13.4-cp313-cp313-win_arm64.whl", hash = "sha256:fb07dc5da7e849e2ad31a5d74e9bece81f30ecf5a42909d0a695f8bd1874d6af", size = 221438, upload-time = "2026-02-09T12:57:40.223Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/57/ee93ced533bcb3e6df961c0c6e42da2fc6addae53fb95b94a89b1e33ebd7/coverage-7.13.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40d74da8e6c4b9ac18b15331c4b5ebc35a17069410cad462ad4f40dcd2d50c0d", size = 220165, upload-time = "2026-02-09T12:57:41.639Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/e0/969fc285a6fbdda49d91af278488d904dcd7651b2693872f0ff94e40e84a/coverage-7.13.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4223b4230a376138939a9173f1bdd6521994f2aff8047fae100d6d94d50c5a12", size = 220516, upload-time = "2026-02-09T12:57:44.215Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/b8/9531944e16267e2735a30a9641ff49671f07e8138ecf1ca13db9fd2560c7/coverage-7.13.4-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1d4be36a5114c499f9f1f9195e95ebf979460dbe2d88e6816ea202010ba1c34b", size = 261804, upload-time = "2026-02-09T12:57:45.989Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/f3/e63df6d500314a2a60390d1989240d5f27318a7a68fa30ad3806e2a9323e/coverage-7.13.4-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:200dea7d1e8095cc6e98cdabe3fd1d21ab17d3cee6dab00cadbb2fe35d9c15b9", size = 263885, upload-time = "2026-02-09T12:57:47.42Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/67/7654810de580e14b37670b60a09c599fa348e48312db5b216d730857ffe6/coverage-7.13.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8eb931ee8e6d8243e253e5ed7336deea6904369d2fd8ae6e43f68abbf167092", size = 266308, upload-time = "2026-02-09T12:57:49.345Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/6f/39d41eca0eab3cc82115953ad41c4e77935286c930e8fad15eaed1389d83/coverage-7.13.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75eab1ebe4f2f64d9509b984f9314d4aa788540368218b858dad56dc8f3e5eb9", size = 267452, upload-time = "2026-02-09T12:57:50.811Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/6d/39c0fbb8fc5cd4d2090811e553c2108cf5112e882f82505ee7495349a6bf/coverage-7.13.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c35eb28c1d085eb7d8c9b3296567a1bebe03ce72962e932431b9a61f28facf26", size = 261057, upload-time = "2026-02-09T12:57:52.447Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/a2/60010c669df5fa603bb5a97fb75407e191a846510da70ac657eb696b7fce/coverage-7.13.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb88b316ec33760714a4720feb2816a3a59180fd58c1985012054fa7aebee4c2", size = 263875, upload-time = "2026-02-09T12:57:53.938Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/d9/63b22a6bdbd17f1f96e9ed58604c2a6b0e72a9133e37d663bef185877cf6/coverage-7.13.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7d41eead3cc673cbd38a4417deb7fd0b4ca26954ff7dc6078e33f6ff97bed940", size = 261500, upload-time = "2026-02-09T12:57:56.012Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/bf/69f86ba1ad85bc3ad240e4c0e57a2e620fbc0e1645a47b5c62f0e941ad7f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:fb26a934946a6afe0e326aebe0730cdff393a8bc0bbb65a2f41e30feddca399c", size = 265212, upload-time = "2026-02-09T12:57:57.5Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/f2/5f65a278a8c2148731831574c73e42f57204243d33bedaaf18fa79c5958f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:dae88bc0fc77edaa65c14be099bd57ee140cf507e6bfdeea7938457ab387efb0", size = 260398, upload-time = "2026-02-09T12:57:59.027Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/80/6e8280a350ee9fea92f14b8357448a242dcaa243cb2c72ab0ca591f66c8c/coverage-7.13.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:845f352911777a8e722bfce168958214951e07e47e5d5d9744109fa5fe77f79b", size = 262584, upload-time = "2026-02-09T12:58:01.129Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/63/01ff182fc95f260b539590fb12c11ad3e21332c15f9799cb5e2386f71d9f/coverage-7.13.4-cp313-cp313t-win32.whl", hash = "sha256:2fa8d5f8de70688a28240de9e139fa16b153cc3cbb01c5f16d88d6505ebdadf9", size = 222688, upload-time = "2026-02-09T12:58:02.736Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/43/89de4ef5d3cd53b886afa114065f7e9d3707bdb3e5efae13535b46ae483d/coverage-7.13.4-cp313-cp313t-win_amd64.whl", hash = "sha256:9351229c8c8407645840edcc277f4a2d44814d1bc34a2128c11c2a031d45a5dd", size = 223746, upload-time = "2026-02-09T12:58:05.362Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/39/7cf0aa9a10d470a5309b38b289b9bb07ddeac5d61af9b664fe9775a4cb3e/coverage-7.13.4-cp313-cp313t-win_arm64.whl", hash = "sha256:30b8d0512f2dc8c8747557e8fb459d6176a2c9e5731e2b74d311c03b78451997", size = 222003, upload-time = "2026-02-09T12:58:06.952Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/11/a9cf762bb83386467737d32187756a42094927150c3e107df4cb078e8590/coverage-7.13.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:300deaee342f90696ed186e3a00c71b5b3d27bffe9e827677954f4ee56969601", size = 219522, upload-time = "2026-02-09T12:58:08.623Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/28/56e6d892b7b052236d67c95f1936b6a7cf7c3e2634bf27610b8cbd7f9c60/coverage-7.13.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29e3220258d682b6226a9b0925bc563ed9a1ebcff3cad30f043eceea7eaf2689", size = 219855, upload-time = "2026-02-09T12:58:10.176Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/69/233459ee9eb0c0d10fcc2fe425a029b3fa5ce0f040c966ebce851d030c70/coverage-7.13.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:391ee8f19bef69210978363ca930f7328081c6a0152f1166c91f0b5fdd2a773c", size = 250887, upload-time = "2026-02-09T12:58:12.503Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/90/2cdab0974b9b5bbc1623f7876b73603aecac11b8d95b85b5b86b32de5eab/coverage-7.13.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0dd7ab8278f0d58a0128ba2fca25824321f05d059c1441800e934ff2efa52129", size = 253396, upload-time = "2026-02-09T12:58:14.615Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/15/ea4da0f85bf7d7b27635039e649e99deb8173fe551096ea15017f7053537/coverage-7.13.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78cdf0d578b15148b009ccf18c686aa4f719d887e76e6b40c38ffb61d264a552", size = 254745, upload-time = "2026-02-09T12:58:16.162Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/11/bb356e86920c655ca4d61daee4e2bbc7258f0a37de0be32d233b561134ff/coverage-7.13.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:48685fee12c2eb3b27c62f2658e7ea21e9c3239cba5a8a242801a0a3f6a8c62a", size = 257055, upload-time = "2026-02-09T12:58:17.892Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/0f/9ae1f8cb17029e09da06ca4e28c9e1d5c1c0a511c7074592e37e0836c915/coverage-7.13.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4e83efc079eb39480e6346a15a1bcb3e9b04759c5202d157e1dd4303cd619356", size = 250911, upload-time = "2026-02-09T12:58:19.495Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/3a/adfb68558fa815cbc29747b553bc833d2150228f251b127f1ce97e48547c/coverage-7.13.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecae9737b72408d6a950f7e525f30aca12d4bd8dd95e37342e5beb3a2a8c4f71", size = 252754, upload-time = "2026-02-09T12:58:21.064Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/b1/540d0c27c4e748bd3cd0bd001076ee416eda993c2bae47a73b7cc9357931/coverage-7.13.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ae4578f8528569d3cf303fef2ea569c7f4c4059a38c8667ccef15c6e1f118aa5", size = 250720, upload-time = "2026-02-09T12:58:22.622Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/95/383609462b3ffb1fe133014a7c84fc0dd01ed55ac6140fa1093b5af7ebb1/coverage-7.13.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6fdef321fdfbb30a197efa02d48fcd9981f0d8ad2ae8903ac318adc653f5df98", size = 254994, upload-time = "2026-02-09T12:58:24.548Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/ba/1761138e86c81680bfc3c49579d66312865457f9fe405b033184e5793cb3/coverage-7.13.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b0f6ccf3dbe577170bebfce1318707d0e8c3650003cb4b3a9dd744575daa8b5", size = 250531, upload-time = "2026-02-09T12:58:26.271Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/8e/05900df797a9c11837ab59c4d6fe94094e029582aab75c3309a93e6fb4e3/coverage-7.13.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75fcd519f2a5765db3f0e391eb3b7d150cce1a771bf4c9f861aeab86c767a3c0", size = 252189, upload-time = "2026-02-09T12:58:27.807Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/bd/29c9f2db9ea4ed2738b8a9508c35626eb205d51af4ab7bf56a21a2e49926/coverage-7.13.4-cp314-cp314-win32.whl", hash = "sha256:8e798c266c378da2bd819b0677df41ab46d78065fb2a399558f3f6cae78b2fbb", size = 222258, upload-time = "2026-02-09T12:58:29.441Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/4d/1f8e723f6829977410efeb88f73673d794075091c8c7c18848d273dc9d73/coverage-7.13.4-cp314-cp314-win_amd64.whl", hash = "sha256:245e37f664d89861cf2329c9afa2c1fe9e6d4e1a09d872c947e70718aeeac505", size = 223073, upload-time = "2026-02-09T12:58:31.026Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/5b/84100025be913b44e082ea32abcf1afbf4e872f5120b7a1cab1d331b1e13/coverage-7.13.4-cp314-cp314-win_arm64.whl", hash = "sha256:ad27098a189e5838900ce4c2a99f2fe42a0bf0c2093c17c69b45a71579e8d4a2", size = 221638, upload-time = "2026-02-09T12:58:32.599Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/e4/c884a405d6ead1370433dad1e3720216b4f9fd8ef5b64bfd984a2a60a11a/coverage-7.13.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:85480adfb35ffc32d40918aad81b89c69c9cc5661a9b8a81476d3e645321a056", size = 220246, upload-time = "2026-02-09T12:58:34.181Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/5c/4d7ed8b23b233b0fffbc9dfec53c232be2e695468523242ea9fd30f97ad2/coverage-7.13.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:79be69cf7f3bf9b0deeeb062eab7ac7f36cd4cc4c4dd694bd28921ba4d8596cc", size = 220514, upload-time = "2026-02-09T12:58:35.704Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/6f/3284d4203fd2f28edd73034968398cd2d4cb04ab192abc8cff007ea35679/coverage-7.13.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:caa421e2684e382c5d8973ac55e4f36bed6821a9bad5c953494de960c74595c9", size = 261877, upload-time = "2026-02-09T12:58:37.864Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/aa/b672a647bbe1556a85337dc95bfd40d146e9965ead9cc2fe81bde1e5cbce/coverage-7.13.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14375934243ee05f56c45393fe2ce81fe5cc503c07cee2bdf1725fb8bef3ffaf", size = 264004, upload-time = "2026-02-09T12:58:39.492Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/a1/aa384dbe9181f98bba87dd23dda436f0c6cf2e148aecbb4e50fc51c1a656/coverage-7.13.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25a41c3104d08edb094d9db0d905ca54d0cd41c928bb6be3c4c799a54753af55", size = 266408, upload-time = "2026-02-09T12:58:41.852Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/5e/5150bf17b4019bc600799f376bb9606941e55bd5a775dc1e096b6ffea952/coverage-7.13.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f01afcff62bf9a08fb32b2c1d6e924236c0383c02c790732b6537269e466a72", size = 267544, upload-time = "2026-02-09T12:58:44.093Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/ed/f1de5c675987a4a7a672250d2c5c9d73d289dbf13410f00ed7181d8017dd/coverage-7.13.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eb9078108fbf0bcdde37c3f4779303673c2fa1fe8f7956e68d447d0dd426d38a", size = 260980, upload-time = "2026-02-09T12:58:45.721Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/e3/fe758d01850aa172419a6743fe76ba8b92c29d181d4f676ffe2dae2ba631/coverage-7.13.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e086334e8537ddd17e5f16a344777c1ab8194986ec533711cbe6c41cde841b6", size = 263871, upload-time = "2026-02-09T12:58:47.334Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/76/b829869d464115e22499541def9796b25312b8cf235d3bb00b39f1675395/coverage-7.13.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:725d985c5ab621268b2edb8e50dfe57633dc69bda071abc470fed55a14935fd3", size = 261472, upload-time = "2026-02-09T12:58:48.995Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/9e/caedb1679e73e2f6ad240173f55218488bfe043e38da577c4ec977489915/coverage-7.13.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3c06f0f1337c667b971ca2f975523347e63ec5e500b9aa5882d91931cd3ef750", size = 265210, upload-time = "2026-02-09T12:58:51.178Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/10/0dd02cb009b16ede425b49ec344aba13a6ae1dc39600840ea6abcb085ac4/coverage-7.13.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:590c0ed4bf8e85f745e6b805b2e1c457b2e33d5255dd9729743165253bc9ad39", size = 260319, upload-time = "2026-02-09T12:58:53.081Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/8e/234d2c927af27c6d7a5ffad5bd2cf31634c46a477b4c7adfbfa66baf7ebb/coverage-7.13.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eb30bf180de3f632cd043322dad5751390e5385108b2807368997d1a92a509d0", size = 262638, upload-time = "2026-02-09T12:58:55.258Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/64/e5547c8ff6964e5965c35a480855911b61509cce544f4d442caa759a0702/coverage-7.13.4-cp314-cp314t-win32.whl", hash = "sha256:c4240e7eded42d131a2d2c4dec70374b781b043ddc79a9de4d55ca71f8e98aea", size = 223040, upload-time = "2026-02-09T12:58:56.936Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/96/38086d58a181aac86d503dfa9c47eb20715a79c3e3acbdf786e92e5c09a8/coverage-7.13.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4c7d3cc01e7350f2f0f6f7036caaf5673fb56b6998889ccfe9e1c1fe75a9c932", size = 224148, upload-time = "2026-02-09T12:58:58.645Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/72/8d10abd3740a0beb98c305e0c3faf454366221c0f37a8bcf8f60020bb65a/coverage-7.13.4-cp314-cp314t-win_arm64.whl", hash = "sha256:23e3f687cf945070d1c90f85db66d11e3025665d8dafa831301a0e0038f3db9b", size = 222172, upload-time = "2026-02-09T12:59:00.396Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/4a/331fe2caf6799d591109bb9c08083080f6de90a823695d412a935622abb2/coverage-7.13.4-py3-none-any.whl", hash = "sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0", size = 211242, upload-time = "2026-02-09T12:59:02.032Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
toml = [
|
||||
{ name = "tomli", marker = "python_full_version <= '3.11'" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "croniter"
|
||||
version = "2.0.7"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "python-dateutil" },
|
||||
{ name = "pytz" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3d/70/240ea4efb2e03b513d820e8da48b784258cbc88770b4b061931cf0269cf8/croniter-2.0.7.tar.gz", hash = "sha256:1041b912b4b1e03751a0993531becf77851ae6e8b334c9c76ffeffb8f055f53f", size = 49655, upload-time = "2024-07-16T15:21:23.156Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/99/1b/62e5ef33bb8cd599004b85f1ae648aaaf329ec396a8e210906e3f04df66a/croniter-2.0.7-py2.py3-none-any.whl", hash = "sha256:f15e80828d23920c4bb7f4d9340b932c9dcabecafc7775703c8b36d1253ed526", size = 21201, upload-time = "2024-07-16T15:21:20.433Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "django"
|
||||
version = "5.2.11"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version < '3.12'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "asgiref" },
|
||||
{ name = "sqlparse" },
|
||||
{ name = "tzdata", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/17/f2/3e57ef696b95067e05ae206171e47a8e53b9c84eec56198671ef9eaa51a6/django-5.2.11.tar.gz", hash = "sha256:7f2d292ad8b9ee35e405d965fbbad293758b858c34bbf7f3df551aeeac6f02d3", size = 10885017, upload-time = "2026-02-03T13:52:50.554Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/91/a7/2b112ab430575bf3135b8304ac372248500d99c352f777485f53fdb9537e/django-5.2.11-py3-none-any.whl", hash = "sha256:e7130df33ada9ab5e5e929bc19346a20fe383f5454acb2cc004508f242ee92c0", size = 8291375, upload-time = "2026-02-03T13:52:42.47Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "django"
|
||||
version = "6.0.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.12'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "asgiref" },
|
||||
{ name = "sqlparse" },
|
||||
{ name = "tzdata", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/26/3e/a1c4207c5dea4697b7a3387e26584919ba987d8f9320f59dc0b5c557a4eb/django-6.0.2.tar.gz", hash = "sha256:3046a53b0e40d4b676c3b774c73411d7184ae2745fe8ce5e45c0f33d3ddb71a7", size = 10886874, upload-time = "2026-02-03T13:50:31.596Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/96/ba/a6e2992bc5b8c688249c00ea48cb1b7a9bc09839328c81dc603671460928/django-6.0.2-py3-none-any.whl", hash = "sha256:610dd3b13d15ec3f1e1d257caedd751db8033c5ad8ea0e2d1219a8acf446ecc6", size = 8339381, upload-time = "2026-02-03T13:50:15.501Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "django-picklefield"
|
||||
version = "3.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "django", version = "5.2.11", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" },
|
||||
{ name = "django", version = "6.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/93/03/13114bccbd1ec8c026ac1ff33dae75ae6c6a5632e4769ee9cda283b9f57e/django_picklefield-3.4.0.tar.gz", hash = "sha256:3a1f740536c0e60d0dba43aa89ccdbe86760d4c3f8ec47799eae122baa741d0a", size = 12555, upload-time = "2025-11-27T03:11:53.13Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/79/b7/139eb1419ca7b27fd714925b8d0eed6efb592479dcf2155fed6c0c87c956/django_picklefield-3.4.0-py3-none-any.whl", hash = "sha256:929bcfbae5b48bd22a52bc04521fdfdd152eee36abb9f20228f9480f9df65f45", size = 10031, upload-time = "2025-11-27T03:11:51.937Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "django-q2"
|
||||
version = "1.10.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "django", version = "5.2.11", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" },
|
||||
{ name = "django", version = "6.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
|
||||
{ name = "django-picklefield" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
croniter = [
|
||||
{ name = "croniter" },
|
||||
]
|
||||
testing = [
|
||||
{ name = "blessed" },
|
||||
{ name = "boto3" },
|
||||
{ name = "croniter" },
|
||||
{ name = "django-redis" },
|
||||
{ name = "hiredis" },
|
||||
{ name = "iron-mq" },
|
||||
{ name = "psutil" },
|
||||
{ name = "pymongo" },
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-cov" },
|
||||
{ name = "pytest-django" },
|
||||
{ name = "redis" },
|
||||
{ name = "ruff" },
|
||||
{ name = "setproctitle" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "blessed", marker = "extra == 'testing'" },
|
||||
{ name = "boto3", marker = "extra == 'testing'" },
|
||||
{ name = "croniter", marker = "extra == 'croniter'" },
|
||||
{ name = "croniter", marker = "extra == 'testing'" },
|
||||
{ name = "django", specifier = ">=5.2" },
|
||||
{ name = "django-picklefield", specifier = ">=3.1,<4" },
|
||||
{ name = "django-redis", marker = "extra == 'testing'" },
|
||||
{ name = "hiredis", marker = "extra == 'testing'" },
|
||||
{ name = "importlib-metadata", marker = "python_full_version < '3.10'", specifier = ">=3.6" },
|
||||
{ name = "iron-mq", marker = "extra == 'testing'" },
|
||||
{ name = "psutil", marker = "extra == 'testing'" },
|
||||
{ name = "pymongo", marker = "extra == 'testing'" },
|
||||
{ name = "pytest", marker = "extra == 'testing'" },
|
||||
{ name = "pytest-cov", marker = "extra == 'testing'" },
|
||||
{ name = "pytest-django", marker = "extra == 'testing'" },
|
||||
{ name = "redis", marker = "extra == 'testing'" },
|
||||
{ name = "ruff", marker = "extra == 'testing'" },
|
||||
{ name = "setproctitle", marker = "extra == 'testing'" },
|
||||
]
|
||||
provides-extras = ["testing", "croniter"]
|
||||
|
||||
[[package]]
|
||||
name = "django-redis"
|
||||
version = "5.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "django", version = "5.2.11", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" },
|
||||
{ name = "django", version = "6.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
|
||||
{ name = "redis" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/83/9d/2272742fdd9d0a9f0b28cd995b0539430c9467a2192e4de2cea9ea6ad38c/django-redis-5.4.0.tar.gz", hash = "sha256:6a02abaa34b0fea8bf9b707d2c363ab6adc7409950b2db93602e6cb292818c42", size = 52567, upload-time = "2023-10-01T20:22:01.221Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/f1/63caad7c9222c26a62082f4f777de26389233b7574629996098bf6d25a4d/django_redis-5.4.0-py3-none-any.whl", hash = "sha256:ebc88df7da810732e2af9987f7f426c96204bf89319df4c6da6ca9a2942edd5b", size = 31119, upload-time = "2023-10-01T20:21:33.009Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dnspython"
|
||||
version = "2.8.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "exceptiongroup"
|
||||
version = "1.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hiredis"
|
||||
version = "2.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bc/bd/e1ba6b92634fd0a25d583a0f988c6fae6767adce46756763ada694defd1b/hiredis-2.4.0.tar.gz", hash = "sha256:90d7af678056c7889d86821344d79fec3932a6a1480ebba3d644cb29a3135348", size = 87599, upload-time = "2024-07-19T11:53:39.12Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/99/8a/3d41b452daba0cc44297680d8268b2d4a99c9f692a6332fefaae2b8777f0/hiredis-2.4.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:aee6c4e8f670ea685345ce4ca01c574a52e0a4318af2b8cdd563de9567731056", size = 81332, upload-time = "2024-07-19T11:51:40.356Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/70/3f39ebfb3824578c34400df3b037b268abb5af0abaa789b430ffd17dd74e/hiredis-2.4.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:76503a0edaf3d1557518127511e69e5d9fa37b6ff15598b0d9d9c2db18b08a41", size = 44669, upload-time = "2024-07-19T11:51:42.303Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/b7/26a56a3b991abe7fcf7bcfa8e0a08de3c3766c6caecb1ba46239342792ff/hiredis-2.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b027b53adb1df11923753d85587e3ab611fe70bc69596e9eb3269acab809c376", size = 42344, upload-time = "2024-07-19T11:51:43.407Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/30/18b824e556271fb7ffa33c8c24712b440e5c2d4abe053d06e2dfc533394f/hiredis-2.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5d2776c7cd6a338cd9338fb50f2a38a7ca3e16250b40ab2d0c41eb1697ebc12", size = 166428, upload-time = "2024-07-19T11:51:44.46Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/35/ad968258e526b05189a3a9cd5359eb5a6d5c049d376c964c0a660728b986/hiredis-2.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8767cae1474f8102ec3d362976f80c8dd4eafd4109c6072adee0a15e37ba919c", size = 177320, upload-time = "2024-07-19T11:51:46.198Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/de/78dd25a50c51ace6af9a42665ef0b9439c858493f4c8dd98f55d21780a8c/hiredis-2.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6dac8a5be01d92707409feec61b98721b7b5c3e77fe7e9e5c7cfb9fdd28385af", size = 166809, upload-time = "2024-07-19T11:51:47.726Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/6e/8adaefff7e3e216b0f7bd6cafce6d5d06798f31c3e2852dc3db6a7d758c9/hiredis-2.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87a8ece3e893f45354395c6b9dc0479744c1c8c6ee4471b60945d96c9b5ce6c2", size = 166538, upload-time = "2024-07-19T11:51:49.195Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/5b/e2525aa0141e6b77cd6e5a4969a8a3f7952ce24c017547935614d4ba3445/hiredis-2.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c85110f536e59fe19ea4b002d04228f57f55462add1630a0785cd6ec62e70415", size = 162859, upload-time = "2024-07-19T11:51:50.599Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/e5/bcb02b046a46b475ca6db822833cabf13f307c3cbbdf0b61ea2a4c458bb9/hiredis-2.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cb62c82a2518b8446be1cc5eb4319e282776bf96fdb2964e81ff2c15d632248b", size = 160729, upload-time = "2024-07-19T11:51:51.784Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/66/8be3100839dd367594fa6397e0fd13bc85701c2d9cdaaec9823af888a0fd/hiredis-2.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9ae4b19cab270fae77d7f944d56bbb308c9886d9577891b347a8deea75563995", size = 159832, upload-time = "2024-07-19T11:51:53.068Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/d6/f3ec8d231b3fcf8e3a883c6b935c72ec1d45c78e955b1c65e3cd1b446723/hiredis-2.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:9fc1a6c78197eff8b4d125bb98410b661e732f3ec563c03264d2d7378cf9e613", size = 171704, upload-time = "2024-07-19T11:51:54.156Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/7c/19d5cbdbcff97b2029e65f453aa5eb4f49e1df36430c3b916a7d36b503cf/hiredis-2.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:38dd931f1124bd9781d3027a0cd6fb6f5a75b5c4ba4fe5540584105239b1f901", size = 164062, upload-time = "2024-07-19T11:51:55.236Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/6f/b020cde8b146f1025549a93ed67b19fe49d6e1cbf7647da3cc0fb9abdcf0/hiredis-2.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2a21e2740c33347740dceb106b64b8a384e91da49aac7e8b3f2a25a9b33714b9", size = 161773, upload-time = "2024-07-19T11:51:56.535Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/ab/ac130f162e67ab9350dec362b337ddf224f6567d05b3d95e7dd20ca9849b/hiredis-2.4.0-cp310-cp310-win32.whl", hash = "sha256:b0adbe8f33f57f2b6bfa8a2ea18f3e4ed91676503673f70f796bfbd06a1a2214", size = 20076, upload-time = "2024-07-19T11:51:57.803Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/85/e30d47313524c3ea6e1a1a199fca0d518ef5cce7b26f45552959915b7ab1/hiredis-2.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:168de1672bd73f7f3cdf0097084b4a71651ac35f7d99d0229ea8f223358d3a79", size = 21534, upload-time = "2024-07-19T11:51:59.28Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/78/fff35558ed059bfe0e2e2255314538b3f42ee01cb748ad6441ac3426254d/hiredis-2.4.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:ac9d91b4d9c306e66a1abd224524fada07684a57f7da72a675e4b8bee9302b38", size = 81334, upload-time = "2024-07-19T11:52:00.629Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/01/d63ea43b6d4991f2b7cc142550b45e951e7588415a8c8c200c878fb01aa6/hiredis-2.4.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:f74bfa9f1b91718d6664d4708d092f7d44e2f0f825a5fab82819d43d41e0302d", size = 44669, upload-time = "2024-07-19T11:52:01.641Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/8e/329eccbb7c64bca74619fb93037b0d911f99218e0ebc43ae74bac608a172/hiredis-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:668b02556d12046e7ce94ded5bfe0ad9989d26e6977ecc55941b9a1a4a49d7d5", size = 42349, upload-time = "2024-07-19T11:52:02.906Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/e2/78d431c22abdf26d4ad8a68340e20103518c8bcfa0d43843389688d921ad/hiredis-2.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6033cc6caaf056969af9ce372282a6ef2838559f2eadffe7ddb73bf65dcb27d6", size = 167118, upload-time = "2024-07-19T11:52:03.87Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/09/0dd0e8f877cf9329de40613f24780b686241097a756be1ef1db2beebb194/hiredis-2.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f76fcf2867d19259b53680c08314435b46f632d20a4d7b9f0ccbb5dd3e925e79", size = 177993, upload-time = "2024-07-19T11:52:05.257Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/04/e68bb0f8c4538c743e30d274bcca7493e5b599e4547228f714ace63faa5b/hiredis-2.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c0e706e0c3d1ec54d8243410e0fd5974b1c7b69db5c54cd9ae6a3a4b64fae33", size = 167659, upload-time = "2024-07-19T11:52:07.28Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/aa/32818b9e459281b11758e31290fef367b1a14cc90778413d69a47b711a39/hiredis-2.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06815c3b9bf7225c4dcc9dd9dfb5a9fa91b4f680104443ef3fcd78410d7eb027", size = 167309, upload-time = "2024-07-19T11:52:09.527Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/6a/3acc8ac22fd3e43cc7417423d247ae220c49948e267b2b28f070d27144ac/hiredis-2.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f44715d6a3313d614ff7550e52ecff67a283776909d960f338701b57e6013542", size = 163170, upload-time = "2024-07-19T11:52:10.941Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/d6/c7f68ca3de4e5194823cb7e18704d83987fb7bcdbe4a8d53d64a4db716ac/hiredis-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:39e1c7212dea1bbed0b075574808bc7c3192b324f54ea5d9ee522f6c35014ce7", size = 161323, upload-time = "2024-07-19T11:52:12.518Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/12/4c56451806c66e1d3616856b414b3e6f5357ab24f643b8a6bbb80f362fd1/hiredis-2.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b385fc7fc7b0811c3fcac4b0a35e5606eca693efba0d1446623ef0158a078034", size = 160186, upload-time = "2024-07-19T11:52:13.739Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/7f/382918c9bb3d99ba92effe6e88a47284334ca5b50433834c1dc798c87ff5/hiredis-2.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0a87a249124666db2b795a0eb77cea5b8af8b148566616a681304826b4405869", size = 172406, upload-time = "2024-07-19T11:52:14.851Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/a9/1c7a25ed0a30d0b2cd8998c96beaabeccad659177fe5e1aa97eec6f54ac1/hiredis-2.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9b4039cd40335f66e55a8bee314b6a795f169fb02d70215d482023ec74613371", size = 164644, upload-time = "2024-07-19T11:52:16.161Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/52/07beb17f0dd6ef1079f0ac9d80481dc5dd96abf2ddbd41ff6fb027bb8c31/hiredis-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c77113fbdbd7ca5de72dd3b7d113856609a1b878f6164de09dd95d12e6a51de2", size = 162356, upload-time = "2024-07-19T11:52:17.766Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/26/d1c128faa2a51c122c2b7e29545a9e854f8c81a7581c6d89b11caf701b9a/hiredis-2.4.0-cp311-cp311-win32.whl", hash = "sha256:469c1a85017abf11d854fb16eca9a4093ebe1f2dacf777fed869d726f02b1389", size = 20083, upload-time = "2024-07-19T11:52:18.882Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/84/bfbfeedeb98c49a7977d3995eda554acb0d7a2ed65410f50eac983dd91bd/hiredis-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:a5c3a32af789b0ec413a606c99b55579abbcb6c86220610a5c5041da8688e7ca", size = 21534, upload-time = "2024-07-19T11:52:19.799Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/69/680b4675f578433e8be026229683293110d66d21a87dcae9bae31d611d96/hiredis-2.4.0-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:8b88390a5e31572e05e8eab476ed3176cc3d2f9622ccc059398ffdb02aaefec4", size = 81433, upload-time = "2024-07-19T11:52:20.729Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/36/b926bcc56113b6cfedfeaabcb97f63d90c6809ed63269628272909c099a8/hiredis-2.4.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:fa4842977924209ae653e856238a30b1c68e579ecde5cf1c16c4de471b35cec7", size = 44733, upload-time = "2024-07-19T11:52:22.136Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/b0/506c37793d81d112318b90e2481b8727bbc3c96a09fecc533768872a30a8/hiredis-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a459b7ff3d802792254d6fc6a622e53ca9cf9f002ed79db7e4dee536b2e20e5d", size = 42405, upload-time = "2024-07-19T11:52:23.107Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/5b/7e2aa081a03f92b82e1a4e6700ed4e4707d3950f1a9550e8c655e651c1a5/hiredis-2.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5c711c8ca8d5767ed8ecd5fb5602c12eaf8fb256a5f4308ae36f2dc79e6f853", size = 169078, upload-time = "2024-07-19T11:52:24.051Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/3f/1783b24d1a4b02a63b629c06dfee6463f17144b5f70cac5c97b7b89ab745/hiredis-2.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a4f733882b67407d4b667eafd61fce86e8e204b158258cc1d0cb0843f6bb4708", size = 179911, upload-time = "2024-07-19T11:52:25.468Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/d7/87004b1cb044a1e0736d55a8750514c76fffa4710d2b465486a83010d9c0/hiredis-2.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:605fe35ebb482b7c8d5daadcf3d264dc5edd205a352d89ee3a983861ef73cda8", size = 169403, upload-time = "2024-07-19T11:52:26.552Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/98/160037832cbc702ec92b71a8f94000e3227e427ae7866c44bd8f089be31f/hiredis-2.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a40f1d985047fe4654a1afb4702cbe0daeacde3868d52be9e4652615d387e05b", size = 169884, upload-time = "2024-07-19T11:52:28.189Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/24/c8950993aeaa4af2d7dcaad1966b62cf01bc45633f06e308c9246f7ef58a/hiredis-2.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fc8d3edbc9f32da930da6ea33d43ce0c3239e6b2018a77907fbf4e9836bd6def", size = 165399, upload-time = "2024-07-19T11:52:29.383Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/25/26f291cee841b76dbb2f64fb8874033ad3eed246f8969b96cc922ad417aa/hiredis-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5b0b2463906cc4119187dfaad493c48a7b2e17120946feb3eb7c2328c8cb4bca", size = 163335, upload-time = "2024-07-19T11:52:30.802Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/97/5f77048ce1e0ee8453cc11be919adf8c2c34474acf970dbeae8aea328eee/hiredis-2.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5e45171fd046bbed2ce6ac485071cd0575d18ae98b5bbcf6533356e443ec47ea", size = 161909, upload-time = "2024-07-19T11:52:32.451Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/f1/318c5a99d1c39e2cfe1c563132b18843d0e9f90d82cdb2804e3e860f9316/hiredis-2.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:68e39d2c0beed53e5361caacd0de98f864b3532344edb79e27e62efba2262de5", size = 174283, upload-time = "2024-07-19T11:52:33.537Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/eb/f7febe81159fc19472c2c2ad698f1dafd71d72cd2ce24ffad6a95949bc54/hiredis-2.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2b76a5600047387c73c1b3d950e4ae3feffaefd442b20ba2f5fea773881d9bcd", size = 166667, upload-time = "2024-07-19T11:52:35.112Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/07/28f88471603a5bfd7844df225b06c50169705b8d383684c2f758efdf1011/hiredis-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1537d13eefe4f48cb979362264851ee90d2bb7a221c8c350e9ceeda9f0392228", size = 164525, upload-time = "2024-07-19T11:52:36.21Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/9a/944764cce2a12215b4262c855734bbc34e77857ee761b5e0d71a71883c23/hiredis-2.4.0-cp312-cp312-win32.whl", hash = "sha256:1bfa50491d3222e3c2297b52c14e835ac52702ac8a91ec3fc1ff5201912623bb", size = 20237, upload-time = "2024-07-19T11:52:37.933Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/2c/43be81cd76c806bc96ca3dd0fb26cbbb2e11d86e211bb1fc88b6c1de680c/hiredis-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:9aabc6098ef00e158598489db5a8b9e12d57a55ea5a4ec35ba3b527dfb88d16e", size = 21633, upload-time = "2024-07-19T11:52:38.818Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/b6/2202807078b63c9f7a8f8dcdbe98f1b36bd246b757b261da790728beb1f4/hiredis-2.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:aeb60452d5b6150075974bc36e1cc74a46bd4b125cd5e72a86a04f4d6abf4e67", size = 39903, upload-time = "2024-07-19T11:53:16.673Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/21/4b0abb18e76860b354e77a734c82e18ffbb9153e6f263eaad47d1d154c5e/hiredis-2.4.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:4da6d881033a1bcb31bba152ea0925344127f0a98f86a6cf2ceb01cf6ecd29e2", size = 36991, upload-time = "2024-07-19T11:53:17.648Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/b2/3c9eb8804073afe5cc4ff40760e6f0b15e5234557a8cab194d447189ecf8/hiredis-2.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9d559775a95aee0ff06c0aaac638691619d6342b7cde85c62ad228804f82829", size = 47969, upload-time = "2024-07-19T11:53:18.626Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/6f/7786187814df1ff1c017939725d1374ffbb23d973f56e871cfb5a46b51d8/hiredis-2.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48baae8fbebf3b11660db6e51a55ff51516ed32edcd44a57f51ea9b373aca330", size = 48364, upload-time = "2024-07-19T11:53:19.652Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/3a/85f08eb7f2f0463691cf432cbb4768ee39d556aa2651ce23b20a011178cf/hiredis-2.4.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d851b7ff732ebc9d823de3c7cc95a5ed4261a0226acd46861a18369ac9568f36", size = 55692, upload-time = "2024-07-19T11:53:20.724Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/cc/d88dfd4cdf5fd9f7ca117e8fa0e609eed702db8d1cb3f3652a51fc3ab250/hiredis-2.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:1d16f5023c1d9971f284231eb7036a25d4d123138a5adc4512c92a73d83b9a77", size = 21631, upload-time = "2024-07-19T11:53:21.738Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.11"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iron-core"
|
||||
version = "1.2.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "python-dateutil" },
|
||||
{ name = "requests" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0a/48/9192fb85b687a5bb5d4981aa64edaa13d5fa280576140a43edcfafb89902/iron-core-1.2.1.tar.gz", hash = "sha256:b7190b86afbe5470da8389fbb412c0fa0529c87f85261736a87e5983ab6f1b95", size = 6152, upload-time = "2022-03-15T16:00:13.945Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/16/9a/8661bcbf6291a955e4f4ba6994e6b60cb6779248d83c8339e723d5e8294d/iron_core-1.2.1-py3-none-any.whl", hash = "sha256:fd5ed3f9b441b9256bae1f829ae6f1da73fae62872aef395336f7d0ac2f041f7", size = 5801, upload-time = "2022-03-15T16:00:12.798Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iron-mq"
|
||||
version = "0.9"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "iron-core" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1a/2e/dd6cee26ed27e25b8700f3de79a3dbf2386b4b69a28a7be20b72d7242e7b/iron-mq-0.9.tar.gz", hash = "sha256:c90441d872d9c08968343810a2ad1cca1664d80fd2ad3a3a2dbec57b7b38ecfa", size = 7442, upload-time = "2016-05-31T12:03:24.055Z" }
|
||||
|
||||
[[package]]
|
||||
name = "jinxed"
|
||||
version = "1.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "ansicon" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/20/d0/59b2b80e7a52d255f9e0ad040d2e826342d05580c4b1d7d7747cfb8db731/jinxed-1.3.0.tar.gz", hash = "sha256:1593124b18a41b7a3da3b078471442e51dbad3d77b4d4f2b0c26ab6f7d660dbf", size = 80981, upload-time = "2024-07-31T22:39:18.854Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/27/e3/0e0014d6ab159d48189e92044ace13b1e1fe9aa3024ba9f4e8cf172aa7c2/jinxed-1.3.0-py2.py3-none-any.whl", hash = "sha256:b993189f39dc2d7504d802152671535b06d380b26d78070559551cbf92df4fc5", size = 33085, upload-time = "2024-07-31T22:39:17.426Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jmespath"
|
||||
version = "1.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "26.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
version = "1.6.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "psutil"
|
||||
version = "5.9.8"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/90/c7/6dc0a455d111f68ee43f27793971cf03fe29b6ef972042549db29eec39a2/psutil-5.9.8.tar.gz", hash = "sha256:6be126e3225486dff286a8fb9a06246a5253f4c7c53b475ea5f5ac934e64194c", size = 503247, upload-time = "2024-01-19T20:47:09.517Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/e3/07ae864a636d70a8a6f58da27cb1179192f1140d5d1da10886ade9405797/psutil-5.9.8-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:aee678c8720623dc456fa20659af736241f575d79429a0e5e9cf88ae0605cc81", size = 248702, upload-time = "2024-01-19T20:47:36.303Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/bd/28c5f553667116b2598b9cc55908ec435cb7f77a34f2bff3e3ca765b0f78/psutil-5.9.8-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cb6403ce6d8e047495a701dc7c5bd788add903f8986d523e3e20b98b733e421", size = 285242, upload-time = "2024-01-19T20:47:39.65Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/4f/0e22aaa246f96d6ac87fe5ebb9c5a693fbe8877f537a1022527c47ca43c5/psutil-5.9.8-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d06016f7f8625a1825ba3732081d77c94589dca78b7a3fc072194851e88461a4", size = 288191, upload-time = "2024-01-19T20:47:43.078Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/f5/2aa3a4acdc1e5940b59d421742356f133185667dd190b166dbcfcf5d7b43/psutil-5.9.8-cp37-abi3-win32.whl", hash = "sha256:bc56c2a1b0d15aa3eaa5a60c9f3f8e3e565303b465dbf57a1b730e7a2b9844e0", size = 251252, upload-time = "2024-01-19T20:47:52.88Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/52/3e39d26feae7df0aa0fd510b14012c3678b36ed068f7d78b8d8784d61f0e/psutil-5.9.8-cp37-abi3-win_amd64.whl", hash = "sha256:8db4c1b57507eef143a15a6884ca10f7c73876cdf5d51e713151c1236a0e68cf", size = 255090, upload-time = "2024-01-19T20:47:56.019Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/33/2d74d588408caedd065c2497bdb5ef83ce6082db01289a1e1147f6639802/psutil-5.9.8-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:d16bbddf0693323b8c6123dd804100241da461e41d6e332fb0ba6058f630f8c8", size = 249898, upload-time = "2024-01-19T20:47:59.238Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pymongo"
|
||||
version = "4.16.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "dnspython" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/65/9c/a4895c4b785fc9865a84a56e14b5bd21ca75aadc3dab79c14187cdca189b/pymongo-4.16.0.tar.gz", hash = "sha256:8ba8405065f6e258a6f872fe62d797a28f383a12178c7153c01ed04e845c600c", size = 2495323, upload-time = "2026-01-07T18:05:48.107Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/93/c36c0998dd91ad8b5031d2e77a903d5cd705b5ba05ca92bcc8731a2c3a8d/pymongo-4.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ed162b2227f98d5b270ecbe1d53be56c8c81db08a1a8f5f02d89c7bb4d19591d", size = 807993, upload-time = "2026-01-07T18:03:40.302Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/96/d2117d792fa9fedb2f6ccf0608db31f851e8382706d7c3c88c6ac92cc958/pymongo-4.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4a9390dce61d705a88218f0d7b54d7e1fa1b421da8129fc7c009e029a9a6b81e", size = 808355, upload-time = "2026-01-07T18:03:42.13Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/2e/e79b7b86c0dd6323d0985c201583c7921d67b842b502aae3f3327cbe3935/pymongo-4.16.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:92a232af9927710de08a6c16a9710cc1b175fb9179c0d946cd4e213b92b2a69a", size = 1182337, upload-time = "2026-01-07T18:03:44.126Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/82/07ec9966381c57d941fddc52637e9c9653e63773be410bd8605f74683084/pymongo-4.16.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d79aa147ce86aef03079096d83239580006ffb684eead593917186aee407767", size = 1200928, upload-time = "2026-01-07T18:03:45.52Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/15/9d45e3cc6fa428b0a3600b0c1c86b310f28c91251c41493460695ab40b6b/pymongo-4.16.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:19a1c96e7f39c7a59a9cfd4d17920cf9382f6f684faeff4649bf587dc59f8edc", size = 1239418, upload-time = "2026-01-07T18:03:47.03Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/b3/f35ee51e2a3f05f673ad4f5e803ae1284c42f4413e8d121c4958f1af4eb9/pymongo-4.16.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efe020c46ce3c3a89af6baec6569635812129df6fb6cf76d4943af3ba6ee2069", size = 1229045, upload-time = "2026-01-07T18:03:48.377Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/2d/1688b88d7c0a5c01da8c703dea831419435d9ce67c6ddbb0ac629c9c72d2/pymongo-4.16.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9dc2c00bed568732b89e211b6adca389053d5e6d2d5a8979e80b813c3ec4d1f9", size = 1196517, upload-time = "2026-01-07T18:03:50.205Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/c6/e89db0f23bd20757b627a5d8c73a609ffd6741887b9004ab229208a79764/pymongo-4.16.0-cp310-cp310-win32.whl", hash = "sha256:5b9c6d689bbe5beb156374508133218610e14f8c81e35bc17d7a14e30ab593e6", size = 794911, upload-time = "2026-01-07T18:03:52.701Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/54/e00a5e517153f310a33132375159e42dceb12bee45b51b35aa0df14f1866/pymongo-4.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:2290909275c9b8f637b0a92eb9b89281e18a72922749ebb903403ab6cc7da914", size = 804801, upload-time = "2026-01-07T18:03:57.671Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/0a/2572faf89195a944c99c6d756227019c8c5f4b5658ecc261c303645dfe69/pymongo-4.16.0-cp310-cp310-win_arm64.whl", hash = "sha256:6af1aaa26f0835175d2200e62205b78e7ec3ffa430682e322cc91aaa1a0dbf28", size = 797579, upload-time = "2026-01-07T18:03:59.1Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/3a/907414a763c4270b581ad6d960d0c6221b74a70eda216a1fdd8fa82ba89f/pymongo-4.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6f2077ec24e2f1248f9cac7b9a2dfb894e50cc7939fcebfb1759f99304caabef", size = 862561, upload-time = "2026-01-07T18:04:00.628Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/58/787d8225dd65cb2383c447346ea5e200ecfde89962d531111521e3b53018/pymongo-4.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4d4f7ba040f72a9f43a44059872af5a8c8c660aa5d7f90d5344f2ed1c3c02721", size = 862923, upload-time = "2026-01-07T18:04:02.213Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/a7/cc2865aae32bc77ade7b35f957a58df52680d7f8506f93c6edbf458e5738/pymongo-4.16.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8a0f73af1ea56c422b2dcfc0437459148a799ef4231c6aee189d2d4c59d6728f", size = 1426779, upload-time = "2026-01-07T18:04:03.942Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/25/3e96eb7998eec05382174da2fefc58d28613f46bbdf821045539d0ed60ab/pymongo-4.16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa30cd16ddd2f216d07ba01d9635c873e97ddb041c61cf0847254edc37d1c60e", size = 1454207, upload-time = "2026-01-07T18:04:05.387Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/7b/8e817a7df8c5d565d39dd4ca417a5e0ef46cc5cc19aea9405f403fec6449/pymongo-4.16.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d638b0b1b294d95d0fdc73688a3b61e05cc4188872818cd240d51460ccabcb5", size = 1511654, upload-time = "2026-01-07T18:04:08.458Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/7a/50c4d075ccefcd281cdcfccc5494caa5665b096b85e65a5d6afabb80e09e/pymongo-4.16.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:21d02cc10a158daa20cb040985e280e7e439832fc6b7857bff3d53ef6914ad50", size = 1496794, upload-time = "2026-01-07T18:04:10.355Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/cd/ebdc1aaca5deeaf47310c369ef4083e8550e04e7bf7e3752cfb7d95fcdb8/pymongo-4.16.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4fbb8d3552c2ad99d9e236003c0b5f96d5f05e29386ba7abae73949bfebc13dd", size = 1448371, upload-time = "2026-01-07T18:04:11.76Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/c9/50fdd78c37f68ea49d590c027c96919fbccfd98f3a4cb39f84f79970bd37/pymongo-4.16.0-cp311-cp311-win32.whl", hash = "sha256:be1099a8295b1a722d03fb7b48be895d30f4301419a583dcf50e9045968a041c", size = 841024, upload-time = "2026-01-07T18:04:13.522Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/dd/a3aa1ade0cf9980744db703570afac70a62c85b432c391dea0577f6da7bb/pymongo-4.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:61567f712bda04c7545a037e3284b4367cad8d29b3dec84b4bf3b2147020a75b", size = 855838, upload-time = "2026-01-07T18:04:14.923Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/10/9ad82593ccb895e8722e4884bad4c5ce5e8ff6683b740d7823a6c2bcfacf/pymongo-4.16.0-cp311-cp311-win_arm64.whl", hash = "sha256:c53338613043038005bf2e41a2fafa08d29cdbc0ce80891b5366c819456c1ae9", size = 845007, upload-time = "2026-01-07T18:04:17.099Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/03/6dd7c53cbde98de469a3e6fb893af896dca644c476beb0f0c6342bcc368b/pymongo-4.16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bd4911c40a43a821dfd93038ac824b756b6e703e26e951718522d29f6eb166a8", size = 917619, upload-time = "2026-01-07T18:04:19.173Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/e1/328915f2734ea1f355dc9b0e98505ff670f5fab8be5e951d6ed70971c6aa/pymongo-4.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:25a6b03a68f9907ea6ec8bc7cf4c58a1b51a18e23394f962a6402f8e46d41211", size = 917364, upload-time = "2026-01-07T18:04:20.861Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/fe/4769874dd9812a1bc2880a9785e61eba5340da966af888dd430392790ae0/pymongo-4.16.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:91ac0cb0fe2bf17616c2039dac88d7c9a5088f5cb5829b27c9d250e053664d31", size = 1686901, upload-time = "2026-01-07T18:04:22.219Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/8d/15707b9669fdc517bbc552ac60da7124dafe7ac1552819b51e97ed4038b4/pymongo-4.16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cf0ec79e8ca7077f455d14d915d629385153b6a11abc0b93283ed73a8013e376", size = 1723034, upload-time = "2026-01-07T18:04:24.055Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/af/3d5d16ff11d447d40c1472da1b366a31c7380d7ea2922a449c7f7f495567/pymongo-4.16.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2d0082631a7510318befc2b4fdab140481eb4b9dd62d9245e042157085da2a70", size = 1797161, upload-time = "2026-01-07T18:04:25.964Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/04/725ab8664eeec73ec125b5a873448d80f5d8cf2750aaaf804cbc538a50a5/pymongo-4.16.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:85dc2f3444c346ea019a371e321ac868a4fab513b7a55fe368f0cc78de8177cc", size = 1780938, upload-time = "2026-01-07T18:04:28.745Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/50/dd7e9095e1ca35f93c3c844c92eb6eb0bc491caeb2c9bff3b32fe3c9b18f/pymongo-4.16.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dabbf3c14de75a20cc3c30bf0c6527157224a93dfb605838eabb1a2ee3be008d", size = 1714342, upload-time = "2026-01-07T18:04:30.331Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/c9/542776987d5c31ae8e93e92680ea2b6e5a2295f398b25756234cabf38a39/pymongo-4.16.0-cp312-cp312-win32.whl", hash = "sha256:60307bb91e0ab44e560fe3a211087748b2b5f3e31f403baf41f5b7b0a70bd104", size = 887868, upload-time = "2026-01-07T18:04:32.124Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/d4/b4045a7ccc5680fb496d01edf749c7a9367cc8762fbdf7516cf807ef679b/pymongo-4.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:f513b2c6c0d5c491f478422f6b5b5c27ac1af06a54c93ef8631806f7231bd92e", size = 907554, upload-time = "2026-01-07T18:04:33.685Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/4c/33f75713d50d5247f2258405142c0318ff32c6f8976171c4fcae87a9dbdf/pymongo-4.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:dfc320f08ea9a7ec5b2403dc4e8150636f0d6150f4b9792faaae539c88e7db3b", size = 892971, upload-time = "2026-01-07T18:04:35.594Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/84/148d8b5da8260f4679d6665196ae04ab14ffdf06f5fe670b0ab11942951f/pymongo-4.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d15f060bc6d0964a8bb70aba8f0cb6d11ae99715438f640cff11bbcf172eb0e8", size = 972009, upload-time = "2026-01-07T18:04:38.303Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/5e/9f3a8daf583d0adaaa033a3e3e58194d2282737dc164014ff33c7a081103/pymongo-4.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a19ea46a0fe71248965305a020bc076a163311aefbaa1d83e47d06fa30ac747", size = 971784, upload-time = "2026-01-07T18:04:39.669Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/f2/b6c24361fcde24946198573c0176406bfd5f7b8538335f3d939487055322/pymongo-4.16.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:311d4549d6bf1f8c61d025965aebb5ba29d1481dc6471693ab91610aaffbc0eb", size = 1947174, upload-time = "2026-01-07T18:04:41.368Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/1a/8634192f98cf740b3d174e1018dd0350018607d5bd8ac35a666dc49c732b/pymongo-4.16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:46ffb728d92dd5b09fc034ed91acf5595657c7ca17d4cf3751322cd554153c17", size = 1991727, upload-time = "2026-01-07T18:04:42.965Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/2f/0c47ac84572b28e23028a23a3798a1f725e1c23b0cf1c1424678d16aff42/pymongo-4.16.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:acda193f440dd88c2023cb00aa8bd7b93a9df59978306d14d87a8b12fe426b05", size = 2082497, upload-time = "2026-01-07T18:04:44.652Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/57/9f46ef9c862b2f0cf5ce798f3541c201c574128d31ded407ba4b3918d7b6/pymongo-4.16.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5d9fdb386cf958e6ef6ff537d6149be7edb76c3268cd6833e6c36aa447e4443f", size = 2064947, upload-time = "2026-01-07T18:04:46.228Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/56/5421c0998f38e32288100a07f6cb2f5f9f352522157c901910cb2927e211/pymongo-4.16.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:91899dd7fb9a8c50f09c3c1cf0cb73bfbe2737f511f641f19b9650deb61c00ca", size = 1980478, upload-time = "2026-01-07T18:04:48.017Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/93/bfc448d025e12313a937d6e1e0101b50cc9751636b4b170e600fe3203063/pymongo-4.16.0-cp313-cp313-win32.whl", hash = "sha256:2cd60cd1e05de7f01927f8e25ca26b3ea2c09de8723241e5d3bcfdc70eaff76b", size = 934672, upload-time = "2026-01-07T18:04:49.538Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/10/12710a5e01218d50c3dd165fd72c5ed2699285f77348a3b1a119a191d826/pymongo-4.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3ead8a0050c53eaa55935895d6919d393d0328ec24b2b9115bdbe881aa222673", size = 959237, upload-time = "2026-01-07T18:04:51.382Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/56/d288bcd1d05bc17ec69df1d0b1d67bc710c7c5dbef86033a5a4d2e2b08e6/pymongo-4.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:dbbc5b254c36c37d10abb50e899bc3939bbb7ab1e7c659614409af99bd3e7675", size = 940909, upload-time = "2026-01-07T18:04:52.904Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/9e/4d343f8d0512002fce17915a89477b9f916bda1205729e042d8f23acf194/pymongo-4.16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:8a254d49a9ffe9d7f888e3c677eed3729b14ce85abb08cd74732cead6ccc3c66", size = 1026634, upload-time = "2026-01-07T18:04:54.359Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/e3/341f88c5535df40c0450fda915f582757bb7d988cdfc92990a5e27c4c324/pymongo-4.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a1bf44e13cf2d44d2ea2e928a8140d5d667304abe1a61c4d55b4906f389fbe64", size = 1026252, upload-time = "2026-01-07T18:04:56.642Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/64/9471b22eb98f0a2ca0b8e09393de048502111b2b5b14ab1bd9e39708aab5/pymongo-4.16.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f1c5f1f818b669875d191323a48912d3fcd2e4906410e8297bb09ac50c4d5ccc", size = 2207399, upload-time = "2026-01-07T18:04:58.255Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/ac/47c4d50b25a02f21764f140295a2efaa583ee7f17992a5e5fa542b3a690f/pymongo-4.16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77cfd37a43a53b02b7bd930457c7994c924ad8bbe8dff91817904bcbf291b371", size = 2260595, upload-time = "2026-01-07T18:04:59.788Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/1b/0ce1ce9dd036417646b2fe6f63b58127acff3cf96eeb630c34ec9cd675ff/pymongo-4.16.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:36ef2fee50eee669587d742fb456e349634b4fcf8926208766078b089054b24b", size = 2366958, upload-time = "2026-01-07T18:05:01.942Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/3c/a5a17c0d413aa9d6c17bc35c2b472e9e79cda8068ba8e93433b5f43028e9/pymongo-4.16.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55f8d5a6fe2fa0b823674db2293f92d74cd5f970bc0360f409a1fc21003862d3", size = 2346081, upload-time = "2026-01-07T18:05:03.576Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/19/f815533d1a88fb8a3b6c6e895bb085ffdae68ccb1e6ed7102202a307f8e2/pymongo-4.16.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9caacac0dd105e2555521002e2d17afc08665187017b466b5753e84c016628e6", size = 2246053, upload-time = "2026-01-07T18:05:05.459Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/88/4be3ec78828dc64b212c123114bd6ae8db5b7676085a7b43cc75d0131bd2/pymongo-4.16.0-cp314-cp314-win32.whl", hash = "sha256:c789236366525c3ee3cd6e4e450a9ff629a7d1f4d88b8e18a0aea0615fd7ecf8", size = 989461, upload-time = "2026-01-07T18:05:07.018Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/5a/ab8d5af76421b34db483c9c8ebc3a2199fb80ae63dc7e18f4cf1df46306a/pymongo-4.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:2b0714d7764efb29bf9d3c51c964aed7c4c7237b341f9346f15ceaf8321fdb35", size = 1017803, upload-time = "2026-01-07T18:05:08.499Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/f4/98d68020728ac6423cf02d17cfd8226bf6cce5690b163d30d3f705e8297e/pymongo-4.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:12762e7cc0f8374a8cae3b9f9ed8dabb5d438c7b33329232dd9b7de783454033", size = 997184, upload-time = "2026-01-07T18:05:09.944Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/00/dc3a271daf06401825b9c1f4f76f018182c7738281ea54b9762aea0560c1/pymongo-4.16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1c01e8a7cd0ea66baf64a118005535ab5bf9f9eb63a1b50ac3935dccf9a54abe", size = 1083303, upload-time = "2026-01-07T18:05:11.702Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/4b/b5375ee21d12eababe46215011ebc63801c0d2c5ffdf203849d0d79f9852/pymongo-4.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4c4872299ebe315a79f7f922051061634a64fda95b6b17677ba57ef00b2ba2a4", size = 1083233, upload-time = "2026-01-07T18:05:13.182Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/e3/52efa3ca900622c7dcb56c5e70f15c906816d98905c22d2ee1f84d9a7b60/pymongo-4.16.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:78037d02389745e247fe5ab0bcad5d1ab30726eaac3ad79219c7d6bbb07eec53", size = 2527438, upload-time = "2026-01-07T18:05:14.981Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/96/43b1be151c734e7766c725444bcbfa1de6b60cc66bfb406203746839dd25/pymongo-4.16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c126fb72be2518395cc0465d4bae03125119136462e1945aea19840e45d89cfc", size = 2600399, upload-time = "2026-01-07T18:05:16.794Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/62/fa64a5045dfe3a1cd9217232c848256e7bc0136cffb7da4735c5e0d30e40/pymongo-4.16.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f3867dc225d9423c245a51eaac2cfcd53dde8e0a8d8090bb6aed6e31bd6c2d4f", size = 2720960, upload-time = "2026-01-07T18:05:18.498Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/7b/01577eb97e605502821273a5bc16ce0fb0be5c978fe03acdbff471471202/pymongo-4.16.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f25001a955073b80510c0c3db0e043dbbc36904fd69e511c74e3d8640b8a5111", size = 2699344, upload-time = "2026-01-07T18:05:20.073Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/68/6ef6372d516f703479c3b6cbbc45a5afd307173b1cbaccd724e23919bb1a/pymongo-4.16.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d9885aad05f82fd7ea0c9ca505d60939746b39263fa273d0125170da8f59098", size = 2577133, upload-time = "2026-01-07T18:05:22.052Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/c7/b5337093bb01da852f945802328665f85f8109dbe91d81ea2afe5ff059b9/pymongo-4.16.0-cp314-cp314t-win32.whl", hash = "sha256:948152b30eddeae8355495f9943a3bf66b708295c0b9b6f467de1c620f215487", size = 1040560, upload-time = "2026-01-07T18:05:23.888Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/8c/5b448cd1b103f3889d5713dda37304c81020ff88e38a826e8a75ddff4610/pymongo-4.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f6e42c1bc985d9beee884780ae6048790eb4cd565c46251932906bdb1630034a", size = 1075081, upload-time = "2026-01-07T18:05:26.874Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/cd/ddc794cdc8500f6f28c119c624252fb6dfb19481c6d7ed150f13cf468a6d/pymongo-4.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:6b2a20edb5452ac8daa395890eeb076c570790dfce6b7a44d788af74c2f8cf96", size = 1047725, upload-time = "2026-01-07T18:05:28.47Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "7.4.4"
|
||||
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/80/1f/9d8e98e4133ffb16c90f3b405c43e38d3abb715bb5d7a63a5a684f7e46a3/pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280", size = 1357116, upload-time = "2023-12-31T12:00:18.035Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/51/ff/f6e8b8f39e08547faece4bd80f89d5a8de68a38b2d179cc1c4490ffa3286/pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8", size = 325287, upload-time = "2023-12-31T12:00:13.963Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest-cov"
|
||||
version = "4.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "coverage", extra = ["toml"] },
|
||||
{ name = "pytest" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7a/15/da3df99fd551507694a9b01f512a2f6cf1254f33601605843c3775f39460/pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6", size = 63245, upload-time = "2023-05-24T18:44:56.845Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/4b/8b78d126e275efa2379b1c2e09dc52cf70df16fc3b90613ef82531499d73/pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a", size = 21949, upload-time = "2023-05-24T18:44:54.079Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest-django"
|
||||
version = "4.12.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pytest" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/13/2b/db9a193df89e5660137f5428063bcc2ced7ad790003b26974adf5c5ceb3b/pytest_django-4.12.0.tar.gz", hash = "sha256:df94ec819a83c8979c8f6de13d9cdfbe76e8c21d39473cfe2b40c9fc9be3c758", size = 91156, upload-time = "2026-02-14T18:40:49.235Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/83/a5/41d091f697c09609e7ef1d5d61925494e0454ebf51de7de05f0f0a728f1d/pytest_django-4.12.0-py3-none-any.whl", hash = "sha256:3ff300c49f8350ba2953b90297d23bf5f589db69545f56f1ec5f8cff5da83e85", size = 26123, upload-time = "2026-02-14T18:40:47.381Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-dateutil"
|
||||
version = "2.9.0.post0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "six" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytz"
|
||||
version = "2025.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redis"
|
||||
version = "4.6.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "async-timeout", marker = "python_full_version <= '3.11.2'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/73/88/63d802c2b18dd9eaa5b846cbf18917c6b2882f20efda398cc16a7500b02c/redis-4.6.0.tar.gz", hash = "sha256:585dc516b9eb042a619ef0a39c3d7d55fe81bdb4df09a52c9cdde0d07bf1aa7d", size = 4561721, upload-time = "2023-06-25T13:13:57.139Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/20/2e/409703d645363352a20c944f5d119bdae3eb3034051a53724a7c5fee12b8/redis-4.6.0-py3-none-any.whl", hash = "sha256:e2b03db868160ee4591de3cb90d40ebb50a90dd302138775937f6a42b7ed183c", size = 241149, upload-time = "2023-06-25T13:13:54.563Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.32.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "charset-normalizer" },
|
||||
{ name = "idna" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.4.10"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ea/04/b660bc832ebfa40e1788edf6934388340751cbc6f733d1f807edca9d96e6/ruff-0.4.10.tar.gz", hash = "sha256:3aa4f2bc388a30d346c56524f7cacca85945ba124945fe489952aadb6b5cd804", size = 2577674, upload-time = "2024-06-20T17:42:56.184Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/53/0d/134fdd72f566d37b0c59b6e55f60993c705f93a0fe3c1faa6f8a269057c7/ruff-0.4.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5c2c4d0859305ac5a16310eec40e4e9a9dec5dcdfbe92697acd99624e8638dac", size = 8510271, upload-time = "2024-06-20T17:41:49.591Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/5e/4ac799ffec39ef5012052c1f144a0f7a63a0322ebd328b802d64beb3d091/ruff-0.4.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a79489607d1495685cdd911a323a35871abfb7a95d4f98fc6f85e799227ac46e", size = 8107776, upload-time = "2024-06-20T17:41:55.14Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/6f/37af054d3ced5a6196201f6c248eeaec6b3b844136cf3da510d591dbfd89/ruff-0.4.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1dd1681dfa90a41b8376a61af05cc4dc5ff32c8f14f5fe20dba9ff5deb80cd6", size = 9868358, upload-time = "2024-06-20T17:41:58.162Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/38/070baf0393ba0da9d85409bdd63874776926acfc372e8e9f0ed21957aeee/ruff-0.4.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c75c53bb79d71310dc79fb69eb4902fba804a81f374bc86a9b117a8d077a1784", size = 9172824, upload-time = "2024-06-20T17:42:02.386Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/9d/bad51d81c918e1ce1648b24480a63f5605662efe69b55fad05825b5711ff/ruff-0.4.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18238c80ee3d9100d3535d8eb15a59c4a0753b45cc55f8bf38f38d6a597b9739", size = 9997887, upload-time = "2024-06-20T17:42:06.309Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/a4/1310b3d003cb67f3c86cb8cc5c5e475dab152b1eef88558abd11e55daaad/ruff-0.4.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d8f71885bce242da344989cae08e263de29752f094233f932d4f5cfb4ef36a81", size = 10743762, upload-time = "2024-06-20T17:42:11.13Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/c1/5373bc5a4c3782c0a368ce5ca4ec3a689574daf71f68f55720a6a64321d4/ruff-0.4.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:330421543bd3222cdfec481e8ff3460e8702ed1e58b494cf9d9e4bf90db52b9d", size = 10329524, upload-time = "2024-06-20T17:42:15.294Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/dc/2c057e7717a3eaaa89ea848a26ef085930a2509f9b66ceae55319668c03d/ruff-0.4.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e9b6fb3a37b772628415b00c4fc892f97954275394ed611056a4b8a2631365e", size = 11208593, upload-time = "2024-06-20T17:42:20.077Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/c3/3f89b1e967a869642bd9198f27e2b89b8300862555d3e1e39b4ccaf92e8b/ruff-0.4.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f54c481b39a762d48f64d97351048e842861c6662d63ec599f67d515cb417f6", size = 10041835, upload-time = "2024-06-20T17:42:24.487Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/e6/734aed23112de8df5a2f3bc02e9e45cd3910fe83b0d2bb2456e200c52d98/ruff-0.4.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:67fe086b433b965c22de0b4259ddfe6fa541c95bf418499bedb9ad5fb8d1c631", size = 9842683, upload-time = "2024-06-20T17:42:28.324Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/13/bc788b2e21d3e4db74d1375da22f50f944bc1fef064c4749f307b0c8794f/ruff-0.4.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:acfaaab59543382085f9eb51f8e87bac26bf96b164839955f244d07125a982ef", size = 9283929, upload-time = "2024-06-20T17:42:32.221Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/09/f3c6560f9d81a4c5d800996090c9cc54d794ea14ab8f8af46b7483005963/ruff-0.4.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:3cea07079962b2941244191569cf3a05541477286f5cafea638cd3aa94b56815", size = 9617526, upload-time = "2024-06-20T17:42:36.588Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/9e/11ae4e8587efe40aa083835665d0818626f8f4a10aa4ebc097cdbfae7624/ruff-0.4.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:338a64ef0748f8c3a80d7f05785930f7965d71ca260904a9321d13be24b79695", size = 10114053, upload-time = "2024-06-20T17:42:41.144Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/94/3bb62a0086e9c61d0506e546e7cf68456fd93bf569a8adfa5e324812970d/ruff-0.4.10-py3-none-win32.whl", hash = "sha256:ffe3cd2f89cb54561c62e5fa20e8f182c0a444934bf430515a4b422f1ab7b7ca", size = 7707741, upload-time = "2024-06-20T17:42:45.061Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/4e/6fd32ebd0a09f25ed9911b77c5273b7a6b3b50a78d6ed0508d66a24398b8/ruff-0.4.10-py3-none-win_amd64.whl", hash = "sha256:67f67cef43c55ffc8cc59e8e0b97e9e60b4837c8f21e8ab5ffd5d66e196e25f7", size = 8519153, upload-time = "2024-06-20T17:42:48.907Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/78/5109b7db3b44a64157b025e45eec6591e4beb53732104637d8e0ee0c5570/ruff-0.4.10-py3-none-win_arm64.whl", hash = "sha256:dd1fcee327c20addac7916ca4e2653fbbf2e8388d8a6477ce5b4e986b68ae6c0", size = 7906942, upload-time = "2024-06-20T17:42:52.972Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "s3transfer"
|
||||
version = "0.16.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "botocore" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/05/04/74127fc843314818edfa81b5540e26dd537353b123a4edc563109d8f17dd/s3transfer-0.16.0.tar.gz", hash = "sha256:8e990f13268025792229cd52fa10cb7163744bf56e719e0b9cb925ab79abf920", size = 153827, upload-time = "2025-12-01T02:30:59.114Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/51/727abb13f44c1fcf6d145979e1535a35794db0f6e450a0cb46aa24732fe2/s3transfer-0.16.0-py3-none-any.whl", hash = "sha256:18e25d66fed509e3868dc1572b3f427ff947dd2c56f844a5bf09481ad3f3b2fe", size = 86830, upload-time = "2025-12-01T02:30:57.729Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "setproctitle"
|
||||
version = "1.3.7"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8d/48/49393a96a2eef1ab418b17475fb92b8fcfad83d099e678751b05472e69de/setproctitle-1.3.7.tar.gz", hash = "sha256:bc2bc917691c1537d5b9bca1468437176809c7e11e5694ca79a9ca12345dcb9e", size = 27002, upload-time = "2025-09-05T12:51:25.278Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/48/fb401ec8c4953d519d05c87feca816ad668b8258448ff60579ac7a1c1386/setproctitle-1.3.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cf555b6299f10a6eb44e4f96d2f5a3884c70ce25dc5c8796aaa2f7b40e72cb1b", size = 18079, upload-time = "2025-09-05T12:49:07.732Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/a3/c2b0333c2716fb3b4c9a973dd113366ac51b4f8d56b500f4f8f704b4817a/setproctitle-1.3.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:690b4776f9c15aaf1023bb07d7c5b797681a17af98a4a69e76a1d504e41108b7", size = 13099, upload-time = "2025-09-05T12:49:09.222Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/f8/17bda581c517678260e6541b600eeb67745f53596dc077174141ba2f6702/setproctitle-1.3.7-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:00afa6fc507967d8c9d592a887cdc6c1f5742ceac6a4354d111ca0214847732c", size = 31793, upload-time = "2025-09-05T12:49:10.297Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/d1/76a33ae80d4e788ecab9eb9b53db03e81cfc95367ec7e3fbf4989962fedd/setproctitle-1.3.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9e02667f6b9fc1238ba753c0f4b0a37ae184ce8f3bbbc38e115d99646b3f4cd3", size = 32779, upload-time = "2025-09-05T12:49:12.157Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/27/1a07c38121967061564f5e0884414a5ab11a783260450172d4fc68c15621/setproctitle-1.3.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:83fcd271567d133eb9532d3b067c8a75be175b2b3b271e2812921a05303a693f", size = 34578, upload-time = "2025-09-05T12:49:13.393Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/d4/725e6353935962d8bb12cbf7e7abba1d0d738c7f6935f90239d8e1ccf913/setproctitle-1.3.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:13fe37951dda1a45c35d77d06e3da5d90e4f875c4918a7312b3b4556cfa7ff64", size = 32030, upload-time = "2025-09-05T12:49:15.362Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/24/e4677ae8e1cb0d549ab558b12db10c175a889be0974c589c428fece5433e/setproctitle-1.3.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a05509cfb2059e5d2ddff701d38e474169e9ce2a298cf1b6fd5f3a213a553fe5", size = 33363, upload-time = "2025-09-05T12:49:16.829Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/d4/69ce66e4373a48fdbb37489f3ded476bb393e27f514968c3a69a67343ae0/setproctitle-1.3.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6da835e76ae18574859224a75db6e15c4c2aaa66d300a57efeaa4c97ca4c7381", size = 31508, upload-time = "2025-09-05T12:49:18.032Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/5a/42c1ed0e9665d068146a68326529b5686a1881c8b9197c2664db4baf6aeb/setproctitle-1.3.7-cp310-cp310-win32.whl", hash = "sha256:9e803d1b1e20240a93bac0bc1025363f7f80cb7eab67dfe21efc0686cc59ad7c", size = 12558, upload-time = "2025-09-05T12:49:19.742Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/fe/dd206cc19a25561921456f6cb12b405635319299b6f366e0bebe872abc18/setproctitle-1.3.7-cp310-cp310-win_amd64.whl", hash = "sha256:a97200acc6b64ec4cada52c2ecaf1fba1ef9429ce9c542f8a7db5bcaa9dcbd95", size = 13245, upload-time = "2025-09-05T12:49:21.023Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/cd/1b7ba5cad635510720ce19d7122154df96a2387d2a74217be552887c93e5/setproctitle-1.3.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a600eeb4145fb0ee6c287cb82a2884bd4ec5bbb076921e287039dcc7b7cc6dd0", size = 18085, upload-time = "2025-09-05T12:49:22.183Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/1a/b2da0a620490aae355f9d72072ac13e901a9fec809a6a24fc6493a8f3c35/setproctitle-1.3.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:97a090fed480471bb175689859532709e28c085087e344bca45cf318034f70c4", size = 13097, upload-time = "2025-09-05T12:49:23.322Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/2e/bd03ff02432a181c1787f6fc2a678f53b7dacdd5ded69c318fe1619556e8/setproctitle-1.3.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1607b963e7b53e24ec8a2cb4e0ab3ae591d7c6bf0a160feef0551da63452b37f", size = 32191, upload-time = "2025-09-05T12:49:24.567Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/78/1e62fc0937a8549f2220445ed2175daacee9b6764c7963b16148119b016d/setproctitle-1.3.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a20fb1a3974e2dab857870cf874b325b8705605cb7e7e8bcbb915bca896f52a9", size = 33203, upload-time = "2025-09-05T12:49:25.871Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/3c/65edc65db3fa3df400cf13b05e9d41a3c77517b4839ce873aa6b4043184f/setproctitle-1.3.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f8d961bba676e07d77665204f36cffaa260f526e7b32d07ab3df6a2c1dfb44ba", size = 34963, upload-time = "2025-09-05T12:49:27.044Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/32/89157e3de997973e306e44152522385f428e16f92f3cf113461489e1e2ee/setproctitle-1.3.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:db0fd964fbd3a9f8999b502f65bd2e20883fdb5b1fae3a424e66db9a793ed307", size = 32398, upload-time = "2025-09-05T12:49:28.909Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/18/77a765a339ddf046844cb4513353d8e9dcd8183da9cdba6e078713e6b0b2/setproctitle-1.3.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:db116850fcf7cca19492030f8d3b4b6e231278e8fe097a043957d22ce1bdf3ee", size = 33657, upload-time = "2025-09-05T12:49:30.323Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/63/f0b6205c64d74d2a24a58644a38ec77bdbaa6afc13747e75973bf8904932/setproctitle-1.3.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:316664d8b24a5c91ee244460bdaf7a74a707adaa9e14fbe0dc0a53168bb9aba1", size = 31836, upload-time = "2025-09-05T12:49:32.309Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/51/e1277f9ba302f1a250bbd3eedbbee747a244b3cc682eb58fb9733968f6d8/setproctitle-1.3.7-cp311-cp311-win32.whl", hash = "sha256:b74774ca471c86c09b9d5037c8451fff06bb82cd320d26ae5a01c758088c0d5d", size = 12556, upload-time = "2025-09-05T12:49:33.529Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/7b/822a23f17e9003dfdee92cd72758441ca2a3680388da813a371b716fb07f/setproctitle-1.3.7-cp311-cp311-win_amd64.whl", hash = "sha256:acb9097213a8dd3410ed9f0dc147840e45ca9797785272928d4be3f0e69e3be4", size = 13243, upload-time = "2025-09-05T12:49:34.553Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/f0/2dc88e842077719d7384d86cc47403e5102810492b33680e7dadcee64cd8/setproctitle-1.3.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2dc99aec591ab6126e636b11035a70991bc1ab7a261da428491a40b84376654e", size = 18049, upload-time = "2025-09-05T12:49:36.241Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/b4/50940504466689cda65680c9e9a1e518e5750c10490639fa687489ac7013/setproctitle-1.3.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cdd8aa571b7aa39840fdbea620e308a19691ff595c3a10231e9ee830339dd798", size = 13079, upload-time = "2025-09-05T12:49:38.088Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/99/71630546b9395b095f4082be41165d1078204d1696c2d9baade3de3202d0/setproctitle-1.3.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2906b6c7959cdb75f46159bf0acd8cc9906cf1361c9e1ded0d065fe8f9039629", size = 32932, upload-time = "2025-09-05T12:49:39.271Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/22/cee06af4ffcfb0e8aba047bd44f5262e644199ae7527ae2c1f672b86495c/setproctitle-1.3.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6915964a6dda07920a1159321dcd6d94fc7fc526f815ca08a8063aeca3c204f1", size = 33736, upload-time = "2025-09-05T12:49:40.565Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/00/a5949a8bb06ef5e7df214fc393bb2fb6aedf0479b17214e57750dfdd0f24/setproctitle-1.3.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cff72899861c765bd4021d1ff1c68d60edc129711a2fdba77f9cb69ef726a8b6", size = 35605, upload-time = "2025-09-05T12:49:42.362Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/3a/50caca532a9343828e3bf5778c7a84d6c737a249b1796d50dd680290594d/setproctitle-1.3.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b7cb05bd446687ff816a3aaaf831047fc4c364feff7ada94a66024f1367b448c", size = 33143, upload-time = "2025-09-05T12:49:43.515Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/14/b843a251296ce55e2e17c017d6b9f11ce0d3d070e9265de4ecad948b913d/setproctitle-1.3.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3a57b9a00de8cae7e2a1f7b9f0c2ac7b69372159e16a7708aa2f38f9e5cc987a", size = 34434, upload-time = "2025-09-05T12:49:45.31Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/b7/06145c238c0a6d2c4bc881f8be230bb9f36d2bf51aff7bddcb796d5eed67/setproctitle-1.3.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d8828b356114f6b308b04afe398ed93803d7fca4a955dd3abe84430e28d33739", size = 32795, upload-time = "2025-09-05T12:49:46.419Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/dc/ef76a81fac9bf27b84ed23df19c1f67391a753eed6e3c2254ebcb5133f56/setproctitle-1.3.7-cp312-cp312-win32.whl", hash = "sha256:b0304f905efc845829ac2bc791ddebb976db2885f6171f4a3de678d7ee3f7c9f", size = 12552, upload-time = "2025-09-05T12:49:47.635Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/5b/a9fe517912cd6e28cf43a212b80cb679ff179a91b623138a99796d7d18a0/setproctitle-1.3.7-cp312-cp312-win_amd64.whl", hash = "sha256:9888ceb4faea3116cf02a920ff00bfbc8cc899743e4b4ac914b03625bdc3c300", size = 13247, upload-time = "2025-09-05T12:49:49.16Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/2f/fcedcade3b307a391b6e17c774c6261a7166aed641aee00ed2aad96c63ce/setproctitle-1.3.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c3736b2a423146b5e62230502e47e08e68282ff3b69bcfe08a322bee73407922", size = 18047, upload-time = "2025-09-05T12:49:50.271Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/ae/afc141ca9631350d0a80b8f287aac79a76f26b6af28fd8bf92dae70dc2c5/setproctitle-1.3.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3384e682b158d569e85a51cfbde2afd1ab57ecf93ea6651fe198d0ba451196ee", size = 13073, upload-time = "2025-09-05T12:49:51.46Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/ed/0a4f00315bc02510395b95eec3d4aa77c07192ee79f0baae77ea7b9603d8/setproctitle-1.3.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0564a936ea687cd24dffcea35903e2a20962aa6ac20e61dd3a207652401492dd", size = 33284, upload-time = "2025-09-05T12:49:52.741Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/e4/adf3c4c0a2173cb7920dc9df710bcc67e9bcdbf377e243b7a962dc31a51a/setproctitle-1.3.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a5d1cb3f81531f0eb40e13246b679a1bdb58762b170303463cb06ecc296f26d0", size = 34104, upload-time = "2025-09-05T12:49:54.416Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/4f/6daf66394152756664257180439d37047aa9a1cfaa5e4f5ed35e93d1dc06/setproctitle-1.3.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a7d159e7345f343b44330cbba9194169b8590cb13dae940da47aa36a72aa9929", size = 35982, upload-time = "2025-09-05T12:49:56.295Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/62/f2c0595403cf915db031f346b0e3b2c0096050e90e0be658a64f44f4278a/setproctitle-1.3.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0b5074649797fd07c72ca1f6bff0406f4a42e1194faac03ecaab765ce605866f", size = 33150, upload-time = "2025-09-05T12:49:58.025Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/29/10dd41cde849fb2f9b626c846b7ea30c99c81a18a5037a45cc4ba33c19a7/setproctitle-1.3.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:61e96febced3f61b766115381d97a21a6265a0f29188a791f6df7ed777aef698", size = 34463, upload-time = "2025-09-05T12:49:59.424Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/3c/cedd8eccfaf15fb73a2c20525b68c9477518917c9437737fa0fda91e378f/setproctitle-1.3.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:047138279f9463f06b858e579cc79580fbf7a04554d24e6bddf8fe5dddbe3d4c", size = 32848, upload-time = "2025-09-05T12:50:01.107Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/3e/0a0e27d1c9926fecccfd1f91796c244416c70bf6bca448d988638faea81d/setproctitle-1.3.7-cp313-cp313-win32.whl", hash = "sha256:7f47accafac7fe6535ba8ba9efd59df9d84a6214565108d0ebb1199119c9cbbd", size = 12544, upload-time = "2025-09-05T12:50:15.81Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/1b/6bf4cb7acbbd5c846ede1c3f4d6b4ee52744d402e43546826da065ff2ab7/setproctitle-1.3.7-cp313-cp313-win_amd64.whl", hash = "sha256:fe5ca35aeec6dc50cabab9bf2d12fbc9067eede7ff4fe92b8f5b99d92e21263f", size = 13235, upload-time = "2025-09-05T12:50:16.89Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/a4/d588d3497d4714750e3eaf269e9e8985449203d82b16b933c39bd3fc52a1/setproctitle-1.3.7-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:10e92915c4b3086b1586933a36faf4f92f903c5554f3c34102d18c7d3f5378e9", size = 18058, upload-time = "2025-09-05T12:50:02.501Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/77/7637f7682322a7244e07c373881c7e982567e2cb1dd2f31bd31481e45500/setproctitle-1.3.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:de879e9c2eab637f34b1a14c4da1e030c12658cdc69ee1b3e5be81b380163ce5", size = 13072, upload-time = "2025-09-05T12:50:03.601Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/09/f366eca0973cfbac1470068d1313fa3fe3de4a594683385204ec7f1c4101/setproctitle-1.3.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c18246d88e227a5b16248687514f95642505000442165f4b7db354d39d0e4c29", size = 34490, upload-time = "2025-09-05T12:50:04.948Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/36/611fc2ed149fdea17c3677e1d0df30d8186eef9562acc248682b91312706/setproctitle-1.3.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7081f193dab22df2c36f9fc6d113f3793f83c27891af8fe30c64d89d9a37e152", size = 35267, upload-time = "2025-09-05T12:50:06.015Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/a4/64e77d0671446bd5a5554387b69e1efd915274686844bea733714c828813/setproctitle-1.3.7-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9cc9b901ce129350637426a89cfd650066a4adc6899e47822e2478a74023ff7c", size = 37376, upload-time = "2025-09-05T12:50:07.484Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/bc/ad9c664fe524fb4a4b2d3663661a5c63453ce851736171e454fa2cdec35c/setproctitle-1.3.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:80e177eff2d1ec172188d0d7fd9694f8e43d3aab76a6f5f929bee7bf7894e98b", size = 33963, upload-time = "2025-09-05T12:50:09.056Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/01/a36de7caf2d90c4c28678da1466b47495cbbad43badb4e982d8db8167ed4/setproctitle-1.3.7-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:23e520776c445478a67ee71b2a3c1ffdafbe1f9f677239e03d7e2cc635954e18", size = 35550, upload-time = "2025-09-05T12:50:10.791Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/68/17e8aea0ed5ebc17fbf03ed2562bfab277c280e3625850c38d92a7b5fcd9/setproctitle-1.3.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5fa1953126a3b9bd47049d58c51b9dac72e78ed120459bd3aceb1bacee72357c", size = 33727, upload-time = "2025-09-05T12:50:12.032Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/33/90a3bf43fe3a2242b4618aa799c672270250b5780667898f30663fd94993/setproctitle-1.3.7-cp313-cp313t-win32.whl", hash = "sha256:4a5e212bf438a4dbeece763f4962ad472c6008ff6702e230b4f16a037e2f6f29", size = 12549, upload-time = "2025-09-05T12:50:13.074Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/0e/50d1f07f3032e1f23d814ad6462bc0a138f369967c72494286b8a5228e40/setproctitle-1.3.7-cp313-cp313t-win_amd64.whl", hash = "sha256:cf2727b733e90b4f874bac53e3092aa0413fe1ea6d4f153f01207e6ce65034d9", size = 13243, upload-time = "2025-09-05T12:50:14.146Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/c7/43ac3a98414f91d1b86a276bc2f799ad0b4b010e08497a95750d5bc42803/setproctitle-1.3.7-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:80c36c6a87ff72eabf621d0c79b66f3bdd0ecc79e873c1e9f0651ee8bf215c63", size = 18052, upload-time = "2025-09-05T12:50:17.928Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/2c/dc258600a25e1a1f04948073826bebc55e18dbd99dc65a576277a82146fa/setproctitle-1.3.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b53602371a52b91c80aaf578b5ada29d311d12b8a69c0c17fbc35b76a1fd4f2e", size = 13071, upload-time = "2025-09-05T12:50:19.061Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/26/8e3bb082992f19823d831f3d62a89409deb6092e72fc6940962983ffc94f/setproctitle-1.3.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fcb966a6c57cf07cc9448321a08f3be6b11b7635be502669bc1d8745115d7e7f", size = 33180, upload-time = "2025-09-05T12:50:20.395Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/af/ae692a20276d1159dd0cf77b0bcf92cbb954b965655eb4a69672099bb214/setproctitle-1.3.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:46178672599b940368d769474fe13ecef1b587d58bb438ea72b9987f74c56ea5", size = 34043, upload-time = "2025-09-05T12:50:22.454Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/b2/6a092076324dd4dac1a6d38482bedebbff5cf34ef29f58585ec76e47bc9d/setproctitle-1.3.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7f9e9e3ff135cbcc3edd2f4cf29b139f4aca040d931573102742db70ff428c17", size = 35892, upload-time = "2025-09-05T12:50:23.937Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/1a/8836b9f28cee32859ac36c3df85aa03e1ff4598d23ea17ca2e96b5845a8f/setproctitle-1.3.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:14c7eba8d90c93b0e79c01f0bd92a37b61983c27d6d7d5a3b5defd599113d60e", size = 32898, upload-time = "2025-09-05T12:50:25.617Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/22/8fabdc24baf42defb599714799d8445fe3ae987ec425a26ec8e80ea38f8e/setproctitle-1.3.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9e64e98077fb30b6cf98073d6c439cd91deb8ebbf8fc62d9dbf52bd38b0c6ac0", size = 34308, upload-time = "2025-09-05T12:50:26.827Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/1b/b9bee9de6c8cdcb3b3a6cb0b3e773afdb86bbbc1665a3bfa424a4294fda2/setproctitle-1.3.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b91387cc0f02a00ac95dcd93f066242d3cca10ff9e6153de7ee07069c6f0f7c8", size = 32536, upload-time = "2025-09-05T12:50:28.5Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/0c/75e5f2685a5e3eda0b39a8b158d6d8895d6daf3ba86dec9e3ba021510272/setproctitle-1.3.7-cp314-cp314-win32.whl", hash = "sha256:52b054a61c99d1b72fba58b7f5486e04b20fefc6961cd76722b424c187f362ed", size = 12731, upload-time = "2025-09-05T12:50:43.955Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/ae/acddbce90d1361e1786e1fb421bc25baeb0c22ef244ee5d0176511769ec8/setproctitle-1.3.7-cp314-cp314-win_amd64.whl", hash = "sha256:5818e4080ac04da1851b3ec71e8a0f64e3748bf9849045180566d8b736702416", size = 13464, upload-time = "2025-09-05T12:50:45.057Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/6d/20886c8ff2e6d85e3cabadab6aab9bb90acaf1a5cfcb04d633f8d61b2626/setproctitle-1.3.7-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:6fc87caf9e323ac426910306c3e5d3205cd9f8dcac06d233fcafe9337f0928a3", size = 18062, upload-time = "2025-09-05T12:50:29.78Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/60/26dfc5f198715f1343b95c2f7a1c16ae9ffa45bd89ffd45a60ed258d24ea/setproctitle-1.3.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6134c63853d87a4897ba7d5cc0e16abfa687f6c66fc09f262bb70d67718f2309", size = 13075, upload-time = "2025-09-05T12:50:31.604Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/9c/980b01f50d51345dd513047e3ba9e96468134b9181319093e61db1c47188/setproctitle-1.3.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1403d2abfd32790b6369916e2313dffbe87d6b11dca5bbd898981bcde48e7a2b", size = 34744, upload-time = "2025-09-05T12:50:32.777Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/b4/82cd0c86e6d1c4538e1a7eb908c7517721513b801dff4ba3f98ef816a240/setproctitle-1.3.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e7c5bfe4228ea22373e3025965d1a4116097e555ee3436044f5c954a5e63ac45", size = 35589, upload-time = "2025-09-05T12:50:34.13Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/4f/9f6b2a7417fd45673037554021c888b31247f7594ff4bd2239918c5cd6d0/setproctitle-1.3.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:585edf25e54e21a94ccb0fe81ad32b9196b69ebc4fc25f81da81fb8a50cca9e4", size = 37698, upload-time = "2025-09-05T12:50:35.524Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/92/927b7d4744aac214d149c892cb5fa6dc6f49cfa040cb2b0a844acd63dcaf/setproctitle-1.3.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:96c38cdeef9036eb2724c2210e8d0b93224e709af68c435d46a4733a3675fee1", size = 34201, upload-time = "2025-09-05T12:50:36.697Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/0c/fd4901db5ba4b9d9013e62f61d9c18d52290497f956745cd3e91b0d80f90/setproctitle-1.3.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:45e3ef48350abb49cf937d0a8ba15e42cee1e5ae13ca41a77c66d1abc27a5070", size = 35801, upload-time = "2025-09-05T12:50:38.314Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/e3/54b496ac724e60e61cc3447f02690105901ca6d90da0377dffe49ff99fc7/setproctitle-1.3.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1fae595d032b30dab4d659bece20debd202229fce12b55abab978b7f30783d73", size = 33958, upload-time = "2025-09-05T12:50:39.841Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/a8/c84bb045ebf8c6fdc7f7532319e86f8380d14bbd3084e6348df56bdfe6fd/setproctitle-1.3.7-cp314-cp314t-win32.whl", hash = "sha256:02432f26f5d1329ab22279ff863c83589894977063f59e6c4b4845804a08f8c2", size = 12745, upload-time = "2025-09-05T12:50:41.377Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/b6/3a5a4f9952972791a9114ac01dfc123f0df79903577a3e0a7a404a695586/setproctitle-1.3.7-cp314-cp314t-win_amd64.whl", hash = "sha256:cbc388e3d86da1f766d8fc2e12682e446064c01cea9f88a88647cfe7c011de6a", size = 13469, upload-time = "2025-09-05T12:50:42.67Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/8a/aff5506ce89bc3168cb492b18ba45573158d528184e8a9759a05a09088a9/setproctitle-1.3.7-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:eb440c5644a448e6203935ed60466ec8d0df7278cd22dc6cf782d07911bcbea6", size = 12654, upload-time = "2025-09-05T12:51:17.141Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/89/5b6f2faedd6ced3d3c085a5efbd91380fb1f61f4c12bc42acad37932f4e9/setproctitle-1.3.7-pp310-pypy310_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:502b902a0e4c69031b87870ff4986c290ebbb12d6038a70639f09c331b18efb2", size = 14284, upload-time = "2025-09-05T12:51:18.393Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/c0/4312fed3ca393a29589603fd48f17937b4ed0638b923bac75a728382e730/setproctitle-1.3.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f6f268caeabb37ccd824d749e7ce0ec6337c4ed954adba33ec0d90cc46b0ab78", size = 13282, upload-time = "2025-09-05T12:51:19.703Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/5b/5e1c117ac84e3cefcf8d7a7f6b2461795a87e20869da065a5c087149060b/setproctitle-1.3.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b1cac6a4b0252b8811d60b6d8d0f157c0fdfed379ac89c25a914e6346cf355a1", size = 12587, upload-time = "2025-09-05T12:51:21.195Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/02/b9eadc226195dcfa90eed37afe56b5dd6fa2f0e5220ab8b7867b8862b926/setproctitle-1.3.7-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f1704c9e041f2b1dc38f5be4552e141e1432fba3dd52c72eeffd5bc2db04dc65", size = 14286, upload-time = "2025-09-05T12:51:22.61Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/26/1be1d2a53c2a91ec48fa2ff4a409b395f836798adf194d99de9c059419ea/setproctitle-1.3.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b08b61976ffa548bd5349ce54404bf6b2d51bd74d4f1b241ed1b0f25bce09c3a", size = 13282, upload-time = "2025-09-05T12:51:24.094Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "six"
|
||||
version = "1.17.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlparse"
|
||||
version = "0.5.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/90/76/437d71068094df0726366574cf3432a4ed754217b436eb7429415cf2d480/sqlparse-0.5.5.tar.gz", hash = "sha256:e20d4a9b0b8585fdf63b10d30066c7c94c5d7a7ec47c889a2d83a3caa93ff28e", size = 120815, upload-time = "2025-12-19T07:17:45.073Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/49/4b/359f28a903c13438ef59ebeee215fb25da53066db67b305c125f1c6d2a25/sqlparse-0.5.5-py3-none-any.whl", hash = "sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba", size = 46138, upload-time = "2025-12-19T07:17:46.573Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tomli"
|
||||
version = "2.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.15.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tzdata"
|
||||
version = "2025.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.6.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wcwidth"
|
||||
version = "0.6.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/35/a2/8e3becb46433538a38726c948d3399905a4c7cabd0df578ede5dc51f0ec2/wcwidth-0.6.0.tar.gz", hash = "sha256:cdc4e4262d6ef9a1a57e018384cbeb1208d8abbc64176027e2c2455c81313159", size = 159684, upload-time = "2026-02-06T19:19:40.919Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", size = 94189, upload-time = "2026-02-06T19:19:39.646Z" },
|
||||
]
|
||||
@@ -1,16 +1,11 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
web:
|
||||
restart: always
|
||||
command: bash -c "python manage.py migrate && python manage.py runserver 0.0.0.0:8000"
|
||||
command: python manage.py runserver 0.0.0.0:8000
|
||||
ports:
|
||||
- "127.0.0.1:8000:8000"
|
||||
build: .
|
||||
volumes:
|
||||
- .:/app
|
||||
|
||||
django-q:
|
||||
restart: always
|
||||
command: bash -c "python manage.py migrate && python manage.py qcluster"
|
||||
build: .
|
||||
volumes:
|
||||
- .:/app
|
||||
|
||||
Reference in New Issue
Block a user