diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..eb92025
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,51 @@
+name: Run tests and upload coverage
+
+on:
+ push:
+ branches: ["main"]
+ pull_request:
+ branches: ["main"]
+
+permissions:
+ contents: read
+
+jobs:
+ test:
+ name: Run tests and collect coverage
+ runs-on: ubuntu-latest
+
+ services:
+ redis:
+ image: redis:7-alpine
+ ports:
+ - 6379:6379
+ options: >-
+ --health-cmd "redis-cli ping"
+ --health-interval 5s
+ --health-timeout 5s
+ --health-retries 5
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Install uv (with Python 3.12)
+ uses: astral-sh/setup-uv@v4
+ with:
+ python-version: "3.12"
+
+ - name: Install dependencies via uv
+ run: |
+ # installs main deps from [project.dependencies]
+ # plus dev group from [dependency-groups]
+ uv sync --group dev
+
+ - name: Run tests
+ run: |
+ uv run pytest --cov=fq_server --cov-branch --cov-report=xml
+
+ - name: Upload results to Codecov
+ uses: codecov/codecov-action@v5
+ with:
+ token: ${{ secrets.CODECOV_TOKEN }}
+ slug: flowdacity/flowdacity-queue-server
diff --git a/.gitignore b/.gitignore
index d752e61..41cb5a5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -54,7 +54,7 @@ docs/_build/
Dockerfile0
Dockerfile1
Dockerfile_supervisor
-test_sharq.py
+test_fq.py
.idea/*
venv/*
.vscode/
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
deleted file mode 100644
index d88d706..0000000
--- a/.pre-commit-config.yaml
+++ /dev/null
@@ -1,11 +0,0 @@
-repos:
-- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v1.2.3
- hooks:
- - id: trailing-whitespace
-
-- repo: git@github.com:Yelp/detect-secrets
- rev: v0.12.3
- hooks:
- - id: detect-secrets
- args: ['--baseline', '.secrets.baseline']
diff --git a/.python-version b/.python-version
new file mode 100644
index 0000000..e4fba21
--- /dev/null
+++ b/.python-version
@@ -0,0 +1 @@
+3.12
diff --git a/.secrets.baseline b/.secrets.baseline
deleted file mode 100644
index d95ce11..0000000
--- a/.secrets.baseline
+++ /dev/null
@@ -1,40 +0,0 @@
-{
- "exclude": {
- "files": null,
- "lines": null
- },
- "generated_at": "2019-05-21T19:45:43Z",
- "plugins_used": [
- {
- "name": "AWSKeyDetector"
- },
- {
- "name": "ArtifactoryDetector"
- },
- {
- "base64_limit": 4.5,
- "name": "Base64HighEntropyString"
- },
- {
- "name": "BasicAuthDetector"
- },
- {
- "hex_limit": 3,
- "name": "HexHighEntropyString"
- },
- {
- "name": "KeywordDetector"
- },
- {
- "name": "PrivateKeyDetector"
- },
- {
- "name": "SlackDetector"
- },
- {
- "name": "StripeDetector"
- }
- ],
- "results": {},
- "version": "0.12.4"
-}
diff --git a/CHANGELOG b/CHANGELOG
deleted file mode 100644
index 8233cb7..0000000
--- a/CHANGELOG
+++ /dev/null
@@ -1,11 +0,0 @@
-MAJOR RELEASE v0.2.0
- - Supports the requeue limit feature
- of SHARQ which bounds the number of
- job retries in SHARQ.
-
-MAJOR RELEASE v0.1.0
- - Adds the interval API which lets users
- change dequeue in real time.
-
-MINOR RELEASE v0.0.2
- - returns 404 on finish to a non existent job.
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..176d430
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,28 @@
+# --- Build arguments with defaults ---
+ARG PYTHON_VERSION=3.12
+ARG PORT=8080
+
+# --- Base image ---
+FROM python:${PYTHON_VERSION}-slim
+
+# --- Re-declare build args as env for access after FROM ---
+ARG PORT
+ENV PYTHONDONTWRITEBYTECODE=1 \
+ PYTHONUNBUFFERED=1 \
+ FQ_CONFIG=/app/docker.conf \
+ UV_LINK_MODE=copy \
+ PORT=${PORT}
+
+WORKDIR /app
+
+RUN pip install --no-cache-dir --upgrade uv
+
+COPY pyproject.toml uv.lock* ./
+
+RUN uv pip install --system --no-cache .
+
+COPY . .
+
+EXPOSE ${PORT}
+
+CMD ["sh", "-c", "uvicorn asgi:app --host 0.0.0.0 --port ${PORT}"]
diff --git a/Jenkinsfile b/Jenkinsfile
deleted file mode 100644
index 929109d..0000000
--- a/Jenkinsfile
+++ /dev/null
@@ -1,9 +0,0 @@
-#!groovy
-
-@Library('plivo_standard_libs@production') _
-
-deliveryPipeline ([
- buildContainer: 'plivo/jenkins-ci/python/2.7.14/ci-base/ubuntu/trusty:18.02.01.139',
- disableQAStages: true,
- trivyBypass: true
-])
diff --git a/MANIFEST.in b/MANIFEST.in
index cf7f81d..cb2158f 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1 +1 @@
-include LICENSE.txt README.md sharq.conf
+include LICENSE.txt README.md fq.conf
diff --git a/Makefile b/Makefile
index eb26cbc..6f28f2a 100644
--- a/Makefile
+++ b/Makefile
@@ -1,24 +1,47 @@
-.PHONY: clean build install uninstall test run
+.PHONY: all clean build install uninstall test publish redis redis-down
-all: clean
+# Default target
+all: clean build
+# Remove Python + build artifacts
clean:
- find . -name \*.pyc -delete
- find . -name \*.pyo -delete
- find . -name \*~ -delete
- rm -rf build dist SharQServer.egg-info
+ find . -name "*.pyc" -delete
+ find . -name "*.pyo" -delete
+ find . -name "*~" -delete
+ rm -rf dist build *.egg-info
+# Build package (requires: pip install build)
build:
- python setup.py sdist
+ python -m build
+# Install locally built package
install:
- pip install dist/SharQServer-*.tar.gz
+ pip install --force-reinstall dist/*.whl
+# Uninstall FQ completely
uninstall:
- yes | pip uninstall sharqserver
+ pip uninstall -y flowdacity-queue
+# Run tests — prefers pytest, falls back to python modules
test:
- python -m tests
+ @if python -c "import pytest" 2>/dev/null; then \
+ python -m pytest -q; \
+ else \
+ echo 'pytest not installed — running direct test modules'; \
+ python -m tests.test_routes; \
+ fi
-run:
- sharq-server --config sharq.conf
+publish: clean
+ uv sync --group dev
+ uv run python -m build
+# @if [ -z "$$PYPI_API_TOKEN" ]; then echo "PYPI_API_TOKEN must be set"; exit 1; fi
+# uv run python -m twine upload dist/* -u __token__ -p "$$PYPI_API_TOKEN"
+ uv run python -m twine upload dist/*
+
+# Start Redis container
+redis:
+ docker compose up -d redis
+
+# Stop Redis container
+redis-down:
+ docker compose down
\ No newline at end of file
diff --git a/README.md b/README.md
index 24811cb..fc06a10 100644
--- a/README.md
+++ b/README.md
@@ -1,28 +1,28 @@
SHARQ Server
============
-SHARQ Server is an flexible, rate limited queuing system based on the [SHARQ Core library](https://github.com/plivo/sharq) and [Redis](https://redis.io).
+SHARQ Server is an flexible, rate limited queuing system based on the [SHARQ Core library](https://github.com/plivo/fq) and [Redis](https://redis.io).
## Overview
SHARQ Server is a flexible, open source, rate limited queuing system. Based on the [Leaky Bucket Algorithm](http://en.wikipedia.org/wiki/Leaky_bucket#The_Leaky_Bucket_Algorithm_as_a_Queue), SHARQ lets you create queues dynamically and update their rate limits in real time.
-SHARQ consists of two components - the core component and the server component. The [SHARQ core](https://github.com/plivo/sharq) is built on [Redis](https://redis.io), using Python and Lua, and the SHARQ Server is built using [Flask](http://flask.pocoo.org/) and [Gevent](http://www.gevent.org/) and talks HTTP.
+SHARQ consists of two components - the core component and the server component. The [SHARQ core](https://github.com/plivo/fq) is built on [Redis](https://redis.io), using Python and Lua, and the SHARQ Server is built using [Flask](http://flask.pocoo.org/) and [Gevent](http://www.gevent.org/) and talks HTTP.
## Installation
SHARQ Server can be installed using [pip](http://pip.readthedocs.org/en/latest/installing.html) as follows:
```
-pip install sharqserver
+pip install fqserver
```
## Running the server
-SHARQ server can be started with the following command. A simple SHARQ config file can be [found here](https://github.com/plivo/sharq-server/blob/master/sharq.conf).
+SHARQ server can be started with the following command. A simple SHARQ config file can be [found here](https://github.com/plivo/fq-server/blob/master/fq.conf).
```
-$sharq-server --config sharq.conf
+$fq-server --config fq.conf
```
Ensure the SHARQ server is up by making a HTTP request.
@@ -30,13 +30,13 @@ Ensure the SHARQ server is up by making a HTTP request.
```
$curl http://127.0.0.1:8080/
{
- "message": "Hello, SharQ!"
+ "message": "Hello, FQ!"
}
```
## Documentation
-Check out [sharq.io](http://sharq.io) for documentation.
+Check out [fq.io](http://fq.io) for documentation.
## License
diff --git a/asgi.py b/asgi.py
new file mode 100644
index 0000000..28d659e
--- /dev/null
+++ b/asgi.py
@@ -0,0 +1,14 @@
+# Copyright (c) 2025 Flowdacity Team. See LICENSE.txt for details.
+# ASGI application entrypoint for Flowdacity Queue (FQ) Server
+
+import os
+from fq_server import setup_server
+
+# read config path from env variable, fallback to ./default.conf
+fq_config_path = os.environ.get("FQ_CONFIG", "./default.conf")
+fq_config_path = os.path.abspath(fq_config_path)
+
+server = setup_server(fq_config_path)
+
+# ASGI app exposed for Uvicorn/Hypercorn
+app = server.app
diff --git a/catalog-mms-mpssharq.yaml b/catalog-mms-mpssharq.yaml
deleted file mode 100644
index 2ede79f..0000000
--- a/catalog-mms-mpssharq.yaml
+++ /dev/null
@@ -1,18 +0,0 @@
-apiVersion: backstage.io/v1alpha1
-kind: Component
-metadata:
- name: mms-mpssharq
- annotations:
- github.com/project-slug: plivo/mms-mpssharq
- backstage.io/kubernetes-label-selector: 'app.kubernetes.io/name=mms-mpssharq'
- backstage.io/kubernetes-namespace: 'messaging'
- plivo/config-app-name: 'mms-mpssharq'
- plivo/team: 'messaging'
- plivo/subteam: 'mms'
- plivo/language: 'python'
- plivo/secret-app-name: 'mms-mpssharq'
- jenkins.io/job-full-name: 'plivo/mms-mpssharq'
-spec:
- type: service
- lifecycle: unknown
- owner: messaging
\ No newline at end of file
diff --git a/catalog-mms-npssharq.yaml b/catalog-mms-npssharq.yaml
deleted file mode 100644
index 3ed0e1c..0000000
--- a/catalog-mms-npssharq.yaml
+++ /dev/null
@@ -1,18 +0,0 @@
-apiVersion: backstage.io/v1alpha1
-kind: Component
-metadata:
- name: mms-npssharq
- annotations:
- github.com/project-slug: plivo/mms-npssharq
- backstage.io/kubernetes-label-selector: 'app.kubernetes.io/name=mms-npssharq'
- backstage.io/kubernetes-namespace: 'messaging'
- plivo/config-app-name: 'mms-npssharq'
- plivo/team: 'messaging'
- plivo/subteam: 'mms'
- plivo/language: 'python'
- plivo/secret-app-name: 'mms-npssharq'
- jenkins.io/job-full-name: 'plivo/mms-npssharq'
-spec:
- type: service
- lifecycle: unknown
- owner: messaging
\ No newline at end of file
diff --git a/catalog-npssharq-clustered.yaml b/catalog-npssharq-clustered.yaml
deleted file mode 100644
index 14e3c4b..0000000
--- a/catalog-npssharq-clustered.yaml
+++ /dev/null
@@ -1,18 +0,0 @@
-apiVersion: backstage.io/v1alpha1
-kind: Component
-metadata:
- name: npssharq-clustered
- annotations:
- github.com/project-slug: plivo/npssharq-clustered
- backstage.io/kubernetes-label-selector: 'app.kubernetes.io/name=npssharq-clustered'
- backstage.io/kubernetes-namespace: 'messaging'
- plivo/config-app-name: 'npssharq-clustered'
- plivo/team: 'messaging'
- plivo/subteam: 'sms'
- plivo/language: 'python'
- plivo/secret-app-name: 'npssharq-clustered'
- jenkins.io/job-full-name: 'plivo/npssharq-clustered'
-spec:
- type: service
- lifecycle: unknown
- owner: messaging
\ No newline at end of file
diff --git a/catalog-rcs-mpssharq.yaml b/catalog-rcs-mpssharq.yaml
deleted file mode 100644
index 1f34f46..0000000
--- a/catalog-rcs-mpssharq.yaml
+++ /dev/null
@@ -1,18 +0,0 @@
-apiVersion: backstage.io/v1alpha1
-kind: Component
-metadata:
- name: rcs-mpssharq
- annotations:
- github.com/project-slug: plivo/rcs-mpssharq
- backstage.io/kubernetes-label-selector: 'app.kubernetes.io/name=rcs-mpssharq'
- backstage.io/kubernetes-namespace: 'messaging'
- plivo/config-app-name: 'rcs-mpssharq'
- plivo/team: 'messaging'
- plivo/subteam: 'sms'
- plivo/language: 'python'
- plivo/secret-app-name: 'rcs-mpssharq'
- jenkins.io/job-full-name: 'plivo/rcs-mpssharq'
-spec:
- type: service
- lifecycle: unknown
- owner: messaging
\ No newline at end of file
diff --git a/catalog-rcs-npssharq.yaml b/catalog-rcs-npssharq.yaml
deleted file mode 100644
index 185fdc8..0000000
--- a/catalog-rcs-npssharq.yaml
+++ /dev/null
@@ -1,18 +0,0 @@
-apiVersion: backstage.io/v1alpha1
-kind: Component
-metadata:
- name: rcs-npssharq
- annotations:
- github.com/project-slug: plivo/rcs-npssharq
- backstage.io/kubernetes-label-selector: 'app.kubernetes.io/name=rcs-npssharq'
- backstage.io/kubernetes-namespace: 'messaging'
- plivo/config-app-name: 'rcs-npssharq'
- plivo/team: 'messaging'
- plivo/subteam: 'sms'
- plivo/language: 'python'
- plivo/secret-app-name: 'rcs-npssharq'
- jenkins.io/job-full-name: 'plivo/rcs-npssharq'
-spec:
- type: service
- lifecycle: unknown
- owner: messaging
\ No newline at end of file
diff --git a/catalog-sharq-clustered.yaml b/catalog-sharq-clustered.yaml
deleted file mode 100644
index 7f3b229..0000000
--- a/catalog-sharq-clustered.yaml
+++ /dev/null
@@ -1,18 +0,0 @@
-apiVersion: backstage.io/v1alpha1
-kind: Component
-metadata:
- name: sharq-clustered
- annotations:
- github.com/project-slug: plivo/sharq-clustered
- backstage.io/kubernetes-label-selector: 'app.kubernetes.io/name=sharq-clustered'
- backstage.io/kubernetes-namespace: 'messaging'
- plivo/config-app-name: 'sharq-clustered'
- plivo/team: 'messaging'
- plivo/subteam: 'sms'
- plivo/language: 'python'
- plivo/secret-app-name: 'sharq-clustered'
- jenkins.io/job-full-name: 'plivo/sharq-clustered'
-spec:
- type: service
- lifecycle: unknown
- owner: messaging
\ No newline at end of file
diff --git a/catalog-washarq-clustered.yaml b/catalog-washarq-clustered.yaml
deleted file mode 100644
index ee02569..0000000
--- a/catalog-washarq-clustered.yaml
+++ /dev/null
@@ -1,18 +0,0 @@
-apiVersion: backstage.io/v1alpha1
-kind: Component
-metadata:
- name: washarq-clustered
- annotations:
- github.com/project-slug: plivo/washarq-clustered
- backstage.io/kubernetes-label-selector: 'app.kubernetes.io/name=washarq-clustered'
- backstage.io/kubernetes-namespace: 'messaging'
- plivo/config-app-name: 'washarq-clustered'
- plivo/team: 'messaging'
- plivo/subteam: 'sms'
- plivo/language: 'python'
- plivo/secret-app-name: 'washarq-clustered'
- jenkins.io/job-full-name: 'plivo/washarq-clustered'
-spec:
- type: service
- lifecycle: unknown
- owner: messaging
\ No newline at end of file
diff --git a/ci/Dockerfile b/ci/Dockerfile
deleted file mode 100644
index 6d99c52..0000000
--- a/ci/Dockerfile
+++ /dev/null
@@ -1,27 +0,0 @@
-FROM 857556598075.dkr.ecr.us-west-1.amazonaws.com/plivo/app/base/python:3.9-bullseye
-USER root
-RUN mkdir -p /opt/sharq-server
-WORKDIR /opt/sharq-server
-COPY . /opt/sharq-server
-RUN mkdir /etc/supervisord && mkdir /etc/supervisord/conf.d && mkdir /var/log/supervisord && pip install supervisor
-RUN apt-get update && apt-get install -y nginx g++ git curl && pip install virtualenv envtpl
-
-RUN virtualenv /opt/sharq-server
-RUN . /opt/sharq-server/bin/activate && /opt/sharq-server/bin/pip install --no-cache-dir -r /opt/sharq-server/requirements.txt && /opt/sharq-server/bin/python setup.py install -f
-
-ADD src/config /etc/sharq-server/config
-ADD src/config/nginx.conf /etc/nginx/nginx.conf
-ADD src/config/nginx-sharq.conf /etc/nginx/conf.d/sharq.conf
-ADD src/config/sharq-server-basicauth /etc/nginx/conf.d/sharq-server-basicauth
-
-COPY src/config/sharq.conf.ctmpl /etc/sharq-server/config/sharq.conf.ctmpl
-COPY src/config/sharq.ini.ctmpl /etc/sharq-server/config/sharq.ini.ctmpl
-COPY src/config/sharq.ini.ctmpl /etc/sharq-server/config/sharq.ini
-COPY src/config/supervisord.conf /etc/supervisord.conf
-RUN mkdir /var/run/sharq/
-
-COPY ci/entrypoint.sh /entrypoint.sh
-RUN chmod 755 /entrypoint.sh && \
- chown root:root /entrypoint.sh
-
-ENTRYPOINT ["/entrypoint.sh"]
diff --git a/ci/config.yml b/ci/config.yml
deleted file mode 100644
index 83fbadb..0000000
--- a/ci/config.yml
+++ /dev/null
@@ -1,8 +0,0 @@
-parent: common
-serviceName: sharq
-hipchatRoom: cicd-tests-dev-notifications
-language: python
-dockerOnly: true
-build:
- platform: "linux/amd64,linux/arm64"
- ecrRegistries: plivo-dev-sms,zentrunk,plivo-dev-voice,contacto-prod
diff --git a/ci/entrypoint.sh b/ci/entrypoint.sh
deleted file mode 100644
index a1e7165..0000000
--- a/ci/entrypoint.sh
+++ /dev/null
@@ -1,34 +0,0 @@
-#!/usr/bin/env bash
-
-set -ex
-
-
-CONSUL=$CONSUL
-if [[ -z "$REGION" && -n "$AWS_REGION" ]]; then
- export REGION="$AWS_REGION"
-fi
-
-if [[ -z "$ENVIRONMENT" && -n "$APP_ENV" ]]; then
- export ENVIRONMENT="$APP_ENV"
-fi
-export TEAM=$TEAM
-export SHARQ_TYPE=$SHARQ_TYPE
-
-echo "
- ___ _ ___ ___
- / __| |_ __ _ _ _ / _ \ / __| ___ _ ___ _____ _ _
- \__ \ ' \/ _' | '_| (_) | \__ \/ -_) '_\ V / -_) '_|
- |___/_||_\__,_|_| \__\_\ |___/\___|_| \_/\___|_|
-
- "
-
-/usr/sbin/consul-template \
- -consul-addr "$CONSUL" \
- -template "/etc/sharq-server/config/sharq.conf.ctmpl:/etc/sharq-server/config/sharq.conf" \
- -template "/etc/sharq-server/config/sharq.ini.ctmpl:/etc/sharq-server/config/sharq.ini" \
- -consul-retry-attempts=0 -once \
- -log-level debug
-
-echo "All templates are rendered. Starting sharq-server..."
-
-supervisord -c /etc/supervisord.conf
diff --git a/default.conf b/default.conf
new file mode 100644
index 0000000..9911a70
--- /dev/null
+++ b/default.conf
@@ -0,0 +1,23 @@
+[fq]
+job_expire_interval : 1000
+job_requeue_interval : 1000
+default_job_requeue_limit : -1
+enable_requeue_script : true
+
+[fq-server]
+host : 127.0.0.1
+port : 8080
+workers : 1
+accesslog : /tmp/fq.log
+
+[redis]
+db : 0
+key_prefix : fq_server
+conn_type : tcp_sock
+;; unix connection settings
+unix_socket_path : /tmp/redis.sock
+;; tcp connection settings
+port : 6379
+host : 127.0.0.1
+password :
+clustered : false
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..4c53a43
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,29 @@
+version: "3.9"
+
+services:
+ app:
+ build: .
+ environment:
+ - FQ_CONFIG=/app/docker.conf
+ - PORT=8080
+ ports:
+ - "8080:8080"
+ depends_on:
+ redis:
+ condition: service_healthy
+
+ redis:
+ image: redis:7-alpine
+ command: ["redis-server", "--save", "60", "1", "--loglevel", "warning"]
+ healthcheck:
+ test: ["CMD", "redis-cli", "ping"]
+ interval: 5s
+ timeout: 5s
+ retries: 5
+ ports:
+ - "6399:6379"
+ volumes:
+ - redis-data:/data
+
+volumes:
+ redis-data:
diff --git a/docker.conf b/docker.conf
new file mode 100644
index 0000000..a57dda3
--- /dev/null
+++ b/docker.conf
@@ -0,0 +1,23 @@
+[fq]
+job_expire_interval : 1000
+job_requeue_interval : 1000
+default_job_requeue_limit : -1
+enable_requeue_script : true
+
+[fq-server]
+host : 0.0.0.0
+port : 8080
+workers : 1
+accesslog : /tmp/fq.log
+
+[redis]
+db : 0
+key_prefix : fq_server
+conn_type : tcp_sock
+;; unix connection settings
+unix_socket_path : /tmp/redis.sock
+;; tcp connection settings
+port : 6399
+host : redis
+password :
+clustered : false
diff --git a/docs/Makefile b/docs/Makefile
index 20799b9..d17e2da 100644
--- a/docs/Makefile
+++ b/docs/Makefile
@@ -85,17 +85,17 @@ qthelp:
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
- @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/sharqserver.qhcp"
+ @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/fqserver.qhcp"
@echo "To view the help file:"
- @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/sharqserver.qhc"
+ @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/fqserver.qhc"
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
- @echo "# mkdir -p $$HOME/.local/share/devhelp/sharqserver"
- @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/sharqserver"
+ @echo "# mkdir -p $$HOME/.local/share/devhelp/fqserver"
+ @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/fqserver"
@echo "# devhelp"
epub:
diff --git a/docs/_templates/layout.html b/docs/_templates/layout.html
index 8009f22..e6f69ff 100644
--- a/docs/_templates/layout.html
+++ b/docs/_templates/layout.html
@@ -1,5 +1,5 @@
{% extends "!layout.html" %}
{%- block extrahead %}
-
+
{% endblock %}
diff --git a/docs/conf.py b/docs/conf.py
index 164bc47..745603d 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
-# sharq server documentation build configuration file, created by
+# fq server documentation build configuration file, created by
# sphinx-quickstart on Mon Sep 22 17:57:32 2014.
#
# This file is execfile()d with the current directory set to its
@@ -116,7 +116,7 @@
html_theme_options = {
'logo': 'logo.png',
'github_user': 'plivo',
- 'github_repo': 'sharq-server',
+ 'github_repo': 'fq-server',
}
# Add any paths that contain custom themes here, relative to this directory.
@@ -197,7 +197,7 @@
#html_file_suffix = None
# Output file base name for HTML help builder.
-htmlhelp_basename = 'sharqserverdoc'
+htmlhelp_basename = 'fqserverdoc'
# -- Options for LaTeX output ---------------------------------------------
@@ -217,7 +217,7 @@
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
- ('index', 'sharqserver.tex', u'sharq server Documentation',
+ ('index', 'fqserver.tex', u'fq server Documentation',
u'Plivo Team', 'manual'),
]
@@ -247,7 +247,7 @@
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
- ('index', 'sharqserver', u'sharq server Documentation',
+ ('index', 'fqserver', u'fq server Documentation',
[u'Plivo Team'], 1)
]
@@ -261,8 +261,8 @@
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
- ('index', 'sharqserver', u'sharq server Documentation',
- u'Plivo Team', 'sharqserver', 'One line description of project.',
+ ('index', 'fqserver', u'fq server Documentation',
+ u'Plivo Team', 'fqserver', 'One line description of project.',
'Miscellaneous'),
]
diff --git a/docs/configuration.rst b/docs/configuration.rst
index 5f9ff7b..53831b1 100644
--- a/docs/configuration.rst
+++ b/docs/configuration.rst
@@ -9,7 +9,7 @@ The SHARQ configuration file is minimal and has three sections.
* `Redis Section <#id3>`_
-sharq section
+fq section
~~~~~~~~~~~~~
This section contains the configurations specific to the SHARQ core.
@@ -27,7 +27,7 @@ job\_requeue\_interval
two clean up processes. A clean up re-queues all the expired jobs back into their
respective queues.
-sharq-server section
+fq-server section
~~~~~~~~~~~~~~~~~~~~
This section contains the configurations specific to the SHARQ Server.
@@ -96,23 +96,23 @@ IP address or FQDN of Redis.
A Sample Configuration File
~~~~~~~~~~~~~~~~~~~~~~~~~~~
-A sample configuration file looks like this. You can also get this configuration file from the `Github repository `_.
+A sample configuration file looks like this. You can also get this configuration file from the `Github repository `_.
.. code-block:: ini
- [sharq]
+ [fq]
job_expire_interval : 1000 ; in milliseconds
job_requeue_interval : 1000 ; in milliseconds
- [sharq-server]
+ [fq-server]
host : 127.0.0.1
port : 8080
workers : 1 ; optional
- accesslog : /tmp/sharq.log ; optional
+ accesslog : /tmp/fq.log ; optional
[redis]
db : 0
- key_prefix : sharq_server
+ key_prefix : fq_server
conn_type : tcp_sock ; or unix_sock
;; unix connection settings
unix_socket_path : /tmp/redis.sock
diff --git a/docs/contributing.rst b/docs/contributing.rst
index 951ce49..ba46218 100644
--- a/docs/contributing.rst
+++ b/docs/contributing.rst
@@ -6,13 +6,13 @@ SHARQ is open source and released under the permissive `MIT License `_ which implements the core functionality of SHARQ which is rate limiting.
-2. The `SHARQ Server `_ which exposes an HTTP interface via `Flask `_ & `Gevent `_.
+1. The `SHARQ Core `_ which implements the core functionality of SHARQ which is rate limiting.
+2. The `SHARQ Server `_ which exposes an HTTP interface via `Flask `_ & `Gevent `_.
The core rate limiting algorithm is implemented in Lua. The detailed explanation of the algorithm with the implementation details and the `Redis `_ data structures can be found in `The Internals `_ section.
**Github Repository Links:**
-* https://github.com/plivo/sharq-server
-* https://github.com/plivo/sharq
+* https://github.com/plivo/fq-server
+* https://github.com/plivo/fq
diff --git a/docs/faqs.rst b/docs/faqs.rst
index d2128ee..496c74e 100644
--- a/docs/faqs.rst
+++ b/docs/faqs.rst
@@ -66,7 +66,7 @@ All expired jobs in the SHARQ Server will be re-queued back into their respectiv
Is there a way to run the SHARQ Server using uWSGI?
===================================================
-Yes! By default the SHARQ Server uses `Gunicorn `_ internally. If you want to use `uWSGI `_ or any other server based on WSGI, you can do so by running ``wsgi.py`` provided in the source files `available on Github `_. For optimal performance, it is recommended to use uWSGI with `Nginx `_. More details can be found in the `uWSGI documentation `_.
+Yes! By default the SHARQ Server uses `Gunicorn `_ internally. If you want to use `uWSGI `_ or any other server based on WSGI, you can do so by running ``wsgi.py`` provided in the source files `available on Github `_. For optimal performance, it is recommended to use uWSGI with `Nginx `_. More details can be found in the `uWSGI documentation `_.
How do I know the number of jobs in any queue in real time?
===========================================================
@@ -90,8 +90,8 @@ The SHARQ code base is split into two components - the core component and the se
**Github Repository Links:**
-* The SHARQ Core - https://github.com/plivo/sharq
-* The SHARQ Server - https://github.com/plivo/sharq-server
+* The SHARQ Core - https://github.com/plivo/fq
+* The SHARQ Server - https://github.com/plivo/fq-server
Read the `Contributing `_ section for more details.
diff --git a/docs/gettingstarted.rst b/docs/gettingstarted.rst
index 9aff453..d8c9d29 100644
--- a/docs/gettingstarted.rst
+++ b/docs/gettingstarted.rst
@@ -2,9 +2,9 @@
Getting Started
===============
-Once the SHARQ Server is installed, it will expose a **sharq-server** command. If you have not yet installed the SHARQ Server, refer `here `_ for instructions.
+Once the SHARQ Server is installed, it will expose a **fq-server** command. If you have not yet installed the SHARQ Server, refer `here `_ for instructions.
-The **sharq-server** command is minimal and accepts a SHARQ configuration file. To get started quickly, fetch the `SHARQ sample configuration file `_. Refer to the `configuration section `_ for more details.
+The **fq-server** command is minimal and accepts a SHARQ configuration file. To get started quickly, fetch the `SHARQ sample configuration file `_. Refer to the `configuration section `_ for more details.
Running the SHARQ Server
------------------------
@@ -13,7 +13,7 @@ The SHARQ Server can be started with the following command.
::
- sharq-server --config sharq.conf
+ fq-server --config fq.conf
This will run the SHARQ Server in the foreground with the following output.
@@ -37,7 +37,7 @@ Ensure the SHARQ Server has started up correctly by making an HTTP GET request t
curl http://127.0.0.1:8080/
{
- "message": "Hello, SharQ!"
+ "message": "Hello, FQ!"
}
diff --git a/docs/index.rst b/docs/index.rst
index f8ebfef..fe76a00 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -1,11 +1,11 @@
Welcome to SHARQ!
=================
-SHARQ is a flexible, open source, rate limited queuing system. Based on the `Leaky Bucket Algorithm `_, `SHARQ `_ lets you create queues dynamically and update their rate limits in real time.
+SHARQ is a flexible, open source, rate limited queuing system. Based on the `Leaky Bucket Algorithm `_, `SHARQ `_ lets you create queues dynamically and update their rate limits in real time.
SHARQ consists of two components - the core component and the server component. The SHARQ core is built on `Redis `_, using Python and Lua, and the SHARQ Server is built using `Flask `_ and `Gevent `_ and talks HTTP.
-SHARQ is released under the permissive `MIT License `_ and is `available on Github `_!
+SHARQ is released under the permissive `MIT License `_ and is `available on Github `_!
To learn more about SHARQ, check out the `getting started section `_.
diff --git a/docs/installation.rst b/docs/installation.rst
index e7b99e9..534800f 100644
--- a/docs/installation.rst
+++ b/docs/installation.rst
@@ -5,11 +5,11 @@ Installation
Installing from PyPI
--------------------
-SHARQ requires `Redis `_ which can be downloaded `here `_. SHARQ can be installed from `PyPI `_ using `pip `_.
+SHARQ requires `Redis `_ which can be downloaded `here `_. SHARQ can be installed from `PyPI `_ using `pip `_.
::
- pip install sharqserver
+ pip install fqserver
Once the SHARQ Server is installed, head over to the `getting started section `_ to try out the API.
@@ -17,11 +17,11 @@ Once the SHARQ Server is installed, head over to the `getting started section `_.
+Get the source code from the `SHARQ Github repository `_.
::
- git clone https://github.com/plivo/sharq-server.git
+ git clone https://github.com/plivo/fq-server.git
Build the package from the source.
diff --git a/docs/internals.rst b/docs/internals.rst
index 929b9b4..c7a6676 100644
--- a/docs/internals.rst
+++ b/docs/internals.rst
@@ -2,4 +2,4 @@
The Internals
=============
-Coming soon... In the mean time, you can go through `the source code on Github `_
+Coming soon... In the mean time, you can go through `the source code on Github `_
diff --git a/fq_server/__init__.py b/fq_server/__init__.py
new file mode 100644
index 0000000..1cf1a62
--- /dev/null
+++ b/fq_server/__init__.py
@@ -0,0 +1,4 @@
+from .server import FQServer, setup_server
+
+__version__ = '0.1.0'
+__all__ = ['FQServer', 'setup_server']
\ No newline at end of file
diff --git a/fq_server/server.py b/fq_server/server.py
new file mode 100644
index 0000000..1d43b04
--- /dev/null
+++ b/fq_server/server.py
@@ -0,0 +1,400 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2014 Plivo Team. See LICENSE.txt for details.
+# Copyright (c) 2025 Flowdacity Development Team. See LICENSE.txt for details.
+
+import asyncio
+import configparser
+import traceback
+import ujson as json
+from redis.exceptions import LockError
+from contextlib import asynccontextmanager, suppress
+from fq import FQ
+
+from starlette.applications import Starlette
+from starlette.requests import Request
+from starlette.responses import JSONResponse
+from starlette.routing import Route
+
+
+class FQServer(object):
+ """Defines a HTTP based API on top of FQ and
+ exposes the app to run the server (Starlette).
+ """
+
+ def __init__(self, config_path: str):
+ """Load the FQ config and define the routes."""
+ # read the configs required by fq-server.
+ self.config = configparser.ConfigParser()
+ self.config.read(config_path)
+ # pass the config file to configure the FQ core.
+ self.queue = FQ(config_path)
+
+ # Starlette app with routes and startup hook
+ self.app = Starlette(
+ routes=self._build_routes(),
+ lifespan=self._lifespan,
+ )
+
+ # ------------------------------------------------------------------
+ # Background requeue loops (async version of gevent ones)
+ # ------------------------------------------------------------------
+ async def requeue(self):
+ """Loop endlessly and requeue expired jobs (no lock)."""
+ job_requeue_interval = float(self.config.get("fq", "job_requeue_interval"))
+ while True:
+ try:
+ await self.queue.requeue()
+ except Exception:
+ traceback.print_exc()
+ # in seconds
+ await asyncio.sleep(job_requeue_interval / 1000.0)
+
+ async def requeue_with_lock(self):
+ """Loop endlessly and requeue expired jobs, but with a distributed lock."""
+ enable_requeue_script = self.config.get("fq", "enable_requeue_script")
+ if enable_requeue_script == "false":
+ print("requeue script disabled")
+ return
+
+ job_requeue_interval = float(self.config.get("fq", "job_requeue_interval"))
+
+ print("start requeue loop: job_requeue_interval = %f" % (job_requeue_interval))
+
+ while True:
+ try:
+ redis = self.queue.redis_client()
+ # assumes async lock
+ async with redis.lock("fq-requeue-lock-key", timeout=15):
+ try:
+ await self.queue.requeue()
+ except Exception:
+ traceback.print_exc()
+ except LockError:
+ # the lock wasn't acquired within specified time
+ pass
+ finally:
+ await asyncio.sleep(job_requeue_interval / 1000.0)
+
+ # ------------------------------------------------------------------
+ # Lifespan handler
+ # ------------------------------------------------------------------
+ @asynccontextmanager
+ async def _lifespan(self, app: Starlette):
+ # --- startup ---
+ await self.queue._initialize()
+ # mimic original behavior: use requeue_with_lock loop
+ self._requeue_task = asyncio.create_task(self.requeue_with_lock())
+
+ try:
+ yield
+ finally:
+ # --- shutdown ---
+ if self._requeue_task is not None:
+ self._requeue_task.cancel()
+ with suppress(asyncio.CancelledError):
+ await self._requeue_task
+
+ # ------------------------------------------------------------------
+ # Routes definition
+ # ------------------------------------------------------------------
+ def _build_routes(self):
+ return [
+ # '/'
+ Route("/", self._view_index, methods=["GET"]),
+ # '/enqueue///'
+ Route(
+ "/enqueue/{queue_type}/{queue_id}/",
+ self._view_enqueue,
+ methods=["POST"],
+ ),
+ # '/dequeue/' defaults={'queue_type': 'default'}
+ Route(
+ "/dequeue/",
+ self._view_dequeue_default,
+ methods=["GET"],
+ ),
+ # '/dequeue//'
+ Route(
+ "/dequeue/{queue_type}/",
+ self._view_dequeue,
+ methods=["GET"],
+ ),
+ # '/finish////'
+ Route(
+ "/finish/{queue_type}/{queue_id}/{job_id}/",
+ self._view_finish,
+ methods=["POST"],
+ ),
+ # '/interval///'
+ Route(
+ "/interval/{queue_type}/{queue_id}/",
+ self._view_interval,
+ methods=["POST"],
+ ),
+ # '/metrics/' defaults={'queue_type': None, 'queue_id': None}
+ Route(
+ "/metrics/",
+ self._view_metrics,
+ methods=["GET"],
+ ),
+ # '/metrics//' defaults={'queue_id': None}
+ Route(
+ "/metrics/{queue_type}/",
+ self._view_metrics,
+ methods=["GET"],
+ ),
+ # '/metrics///'
+ Route(
+ "/metrics/{queue_type}/{queue_id}/",
+ self._view_metrics,
+ methods=["GET"],
+ ),
+ # '/deletequeue///'
+ Route(
+ "/deletequeue/{queue_type}/{queue_id}/",
+ self._view_clear_queue,
+ methods=["DELETE"],
+ ),
+ # '/deepstatus/'
+ Route(
+ "/deepstatus/",
+ self._view_deep_status,
+ methods=["GET"],
+ ),
+ ]
+
+ # ------------------------------------------------------------------
+ # Views (handlers) – re-implemented as async, keeping behavior & status codes
+ # ------------------------------------------------------------------
+ async def _view_index(self, request: Request):
+ """Greetings at the index."""
+ return JSONResponse({"message": "Hello, FQS!"})
+
+ async def _view_enqueue(self, request: Request):
+ """Enqueues a job into FQ."""
+ queue_type = request.path_params["queue_type"]
+ queue_id = request.path_params["queue_id"]
+
+ response = {"status": "failure"}
+ try:
+ raw_body = await request.body()
+ request_data = json.loads(raw_body or b"{}")
+ except Exception as e:
+ response["message"] = str(e)
+ return JSONResponse(response, status_code=400)
+
+ request_data.update(
+ {
+ "queue_type": queue_type,
+ "queue_id": queue_id,
+ }
+ )
+
+ # if max_queued_length is present in request param,
+ # then only queue length will limit to this value
+ max_queued_length = request_data.get("payload", {}).get(
+ "max_queued_length", None
+ )
+ if max_queued_length is not None:
+ current_queue_length = 0
+ try:
+ current_queue_length = await self.queue.get_queue_length(
+ queue_type, queue_id
+ )
+ except Exception as e:
+ print(
+ "Error occurred while fetching redis key length as {} for auth_id {}".format(
+ e, queue_id
+ )
+ )
+
+ if current_queue_length < max_queued_length:
+ try:
+ response = await self.queue.enqueue(**request_data)
+ response["current_queue_length"] = current_queue_length
+ except Exception as e:
+ traceback.print_exc()
+ response["message"] = str(e)
+ return JSONResponse(response, status_code=400)
+
+ return JSONResponse(response, status_code=201)
+ else:
+ response["message"] = "Max queue length reached"
+ response["current_queue_length"] = current_queue_length
+ return JSONResponse(response, status_code=429)
+ else:
+ try:
+ response = await self.queue.enqueue(**request_data)
+ except Exception as e:
+ traceback.print_exc()
+ response["message"] = str(e)
+ return JSONResponse(response, status_code=400)
+
+ return JSONResponse(response, status_code=201)
+
+ async def _view_dequeue_default(self, request: Request):
+ """Dequeues from default queue_type ('default')."""
+ return await self._dequeue_with_type("default")
+
+ async def _view_dequeue(self, request: Request):
+ """Dequeues a job from FQ."""
+ queue_type = request.path_params["queue_type"]
+ return await self._dequeue_with_type(queue_type)
+
+ async def _dequeue_with_type(self, queue_type: str):
+ response = {"status": "failure"}
+ request_data = {"queue_type": queue_type}
+
+ try:
+ response = await self.queue.dequeue(**request_data)
+ if response["status"] == "failure":
+ return JSONResponse(response, status_code=404)
+
+ current_queue_length = 0
+ try:
+ current_queue_length = await self.queue.get_queue_length(
+ queue_type, response["queue_id"]
+ )
+ except Exception as e:
+ print(
+ "DEQUEUE::Error occurred while fetching redis key length {} for queue_id {}".format(
+ e, response["queue_id"]
+ )
+ )
+ response["current_queue_length"] = current_queue_length
+ except Exception as e:
+ for line in traceback.format_exc().splitlines():
+ print(line)
+ response["message"] = str(e)
+ return JSONResponse(response, status_code=400)
+
+ return JSONResponse(response)
+
+ async def _view_finish(self, request: Request):
+ """Marks a job as finished in FQ."""
+ queue_type = request.path_params["queue_type"]
+ queue_id = request.path_params["queue_id"]
+ job_id = request.path_params["job_id"]
+
+ response = {"status": "failure"}
+ request_data = {
+ "queue_type": queue_type,
+ "queue_id": queue_id,
+ "job_id": job_id,
+ }
+
+ try:
+ response = await self.queue.finish(**request_data)
+ if response["status"] == "failure":
+ return JSONResponse(response, status_code=404)
+ except Exception as e:
+ traceback.print_exc()
+ response["message"] = str(e)
+ return JSONResponse(response, status_code=400)
+
+ return JSONResponse(response)
+
+ async def _view_interval(self, request: Request):
+ """Updates the queue interval in FQ."""
+ queue_type = request.path_params["queue_type"]
+ queue_id = request.path_params["queue_id"]
+
+ response = {"status": "failure"}
+ try:
+ raw_body = await request.body()
+ body = json.loads(raw_body or b"{}")
+ interval = body["interval"]
+ except Exception as e:
+ response["message"] = str(e)
+ return JSONResponse(response, status_code=400)
+
+ request_data = {
+ "queue_type": queue_type,
+ "queue_id": queue_id,
+ "interval": interval,
+ }
+
+ try:
+ response = await self.queue.interval(**request_data)
+ if response["status"] == "failure":
+ return JSONResponse(response, status_code=404)
+ except Exception as e:
+ traceback.print_exc()
+ response["message"] = str(e)
+ return JSONResponse(response, status_code=400)
+
+ return JSONResponse(response)
+
+ async def _view_metrics(self, request: Request):
+ """Gets FQ metrics based on the params."""
+ response = {"status": "failure"}
+ request_data = {}
+
+ # matches Flask defaults: queue_type and/or queue_id may be absent
+ queue_type = request.path_params.get("queue_type")
+ queue_id = request.path_params.get("queue_id")
+
+ if queue_type is not None:
+ request_data["queue_type"] = queue_type
+ if queue_id is not None:
+ request_data["queue_id"] = queue_id
+
+ try:
+ response = await self.queue.metrics(**request_data)
+ except Exception as e:
+ traceback.print_exc()
+ response["message"] = str(e)
+ return JSONResponse(response, status_code=400)
+
+ return JSONResponse(response)
+
+ async def _view_deep_status(self, request: Request):
+ """Checks underlying data store health."""
+ try:
+ await self.queue.deep_status()
+ response = {"status": "success"}
+ return JSONResponse(response)
+ except Exception as e:
+ print(e)
+ for line in traceback.format_exc().splitlines():
+ print(line)
+ # preserve original behavior: raise generic exception -> 500
+ raise Exception from e
+
+ async def _view_clear_queue(self, request: Request):
+ """Remove queue from FQ based on the queue_type and queue_id."""
+ queue_type = request.path_params["queue_type"]
+ queue_id = request.path_params["queue_id"]
+
+ response = {"status": "failure"}
+ try:
+ raw_body = await request.body()
+ request_data = json.loads(raw_body or b"{}")
+ except Exception as e:
+ response["message"] = str(e)
+ return JSONResponse(response, status_code=400)
+
+ request_data.update(
+ {
+ "queue_type": queue_type,
+ "queue_id": queue_id,
+ }
+ )
+
+ try:
+ response = await self.queue.clear_queue(**request_data)
+ except Exception as e:
+ traceback.print_exc()
+ response["message"] = str(e)
+ return JSONResponse(response, status_code=400)
+
+ return JSONResponse(response)
+
+
+# ----------------------------------------------------------------------
+# Setup helpers to create and configure the server
+# ----------------------------------------------------------------------
+def setup_server(config_path: str) -> FQServer:
+ """Configure FQ server and return the server instance."""
+ server = FQServer(config_path)
+ return server
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..fed8980
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,40 @@
+[project]
+name = "flowdacity-queue-server"
+version = "0.1.0"
+description = "An API queuing server based on the Flowdacity Queue (FQ) library."
+readme = "README.md"
+requires-python = ">=3.12"
+dependencies = [
+ "flowdacity-queue>=0.1.0",
+ "httpx>=0.28.1",
+ "msgpack>=1.1.2",
+ "redis[hiredis]>=7.1.0",
+ "starlette>=0.50.0",
+ "ujson>=5.11.0",
+ "uvicorn>=0.38.0",
+]
+
+[dependency-groups]
+dev = [
+ "pytest>=9.0.1",
+ "pytest-cov>=7.0.0",
+]
+
+[tool.pytest.ini_options]
+pythonpath = ["."]
+testpaths = ["tests"]
+
+[tool.hatch.build.targets.wheel]
+# package is directly in the project root
+packages = ["fq_server"]
+
+[tool.hatch.build.targets.sdist]
+include = [
+ "fq_server",
+ "README.md",
+ "LICENSE.txt",
+ "default.conf",
+ "asgi.py",
+ "docs",
+ "Makefile",
+]
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644
index ca9d57a..0000000
--- a/requirements.txt
+++ /dev/null
@@ -1,13 +0,0 @@
-Flask==2.2.5
-Jinja2==3.1.6
-MarkupSafe==0.23
-Werkzeug==3.0.6
-argparse==1.2.1
-gevent==23.9.0
-greenlet==2.0.2
-gunicorn==22.0.0
-itsdangerous==0.24
-msgpack==0.5.6
-ujson==5.4.0
-uWSGI==2.0.21
-SharQ==1.3.0
diff --git a/runner.py b/runner.py
deleted file mode 100644
index 0b8ab75..0000000
--- a/runner.py
+++ /dev/null
@@ -1,97 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2014 Plivo Team. See LICENSE.txt for details.
-import os
-import argparse
-import multiprocessing
-import configparser
-
-import gunicorn.app.base
-from gunicorn.six import iteritems
-
-from sharq_server import setup_server, __version__
-
-def number_of_workers():
- return (multiprocessing.cpu_count() * 2) + 1
-
-
-class SharQServerApplicationRunner(gunicorn.app.base.BaseApplication):
-
- """A simple SharQ Gunicorn wrapper which is used to load
- config and run the application.
- """
-
- def __init__(self, app, options=None):
- self.options = options or {}
- self.application = app
- super(SharQServerApplicationRunner, self).__init__()
-
- def load_config(self):
- config = dict([(key, value) for key, value in iteritems(self.options)
- if key in self.cfg.settings and value is not None])
- for key, value in iteritems(config):
- self.cfg.set(key.lower(), value)
-
- def load(self):
- return self.application
-
-
-def run():
- """Exposes a CLI to configure the SharQ Server and runs the server."""
- # create a arg parser and configure it.
- parser = argparse.ArgumentParser(description='SharQ Server.')
- parser.add_argument('-c', '--config', action='store', required=True,
- help='Absolute path of the SharQ configuration file.',
- dest='sharq_config')
- parser.add_argument('-gc', '--gunicorn-config', action='store', required=False,
- help='Gunicorn configuration file.',
- dest='gunicorn_config')
- parser.add_argument('--version', action='version', version='SharQ Server %s' % __version__)
- args = parser.parse_args()
-
- # read the configuration file and set gunicorn options.
- config_parser = configparser.SafeConfigParser()
- # get the full path of the config file.
- sharq_config = os.path.abspath(args.sharq_config)
- config_parser.read(sharq_config)
-
- host = config_parser.get('sharq-server', 'host')
- port = config_parser.get('sharq-server', 'port')
- bind = '%s:%s' % (host, port)
- try:
- workers = config_parser.get('sharq-server', 'workers')
- except configparser.NoOptionError:
- workers = number_of_workers()
-
- try:
- accesslog = config_parser.get('sharq-server', 'accesslog')
- except configparser.NoOptionError:
- accesslog = None
-
- options = {
- 'bind': bind,
- 'workers': workers,
- 'worker_class': 'gevent' # required for sharq to function.
- }
- if accesslog:
- options.update({
- 'accesslog': accesslog
- })
-
- if args.gunicorn_config:
- gunicorn_config = os.path.abspath(args.gunicorn_config)
- options.update({
- 'config': gunicorn_config
- })
-
- print("""
- ___ _ ___ ___
- / __| |_ __ _ _ _ / _ \ / __| ___ _ ___ _____ _ _
- \__ \ ' \/ _` | '_| (_) | \__ \/ -_) '_\ V / -_) '_|
- |___/_||_\__,_|_| \__\_\ |___/\___|_| \_/\___|_|
-
- Version: %s
-
- Listening on: %s
- """ % (__version__, bind))
- server = setup_server(sharq_config)
- SharQServerApplicationRunner(server.app, options).run()
diff --git a/setup.py b/setup.py
deleted file mode 100644
index 2e41235..0000000
--- a/setup.py
+++ /dev/null
@@ -1,39 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2014 Plivo Team. See LICENSE.txt for details.
-from setuptools import setup
-
-setup(
- name='SharQServer',
- version='0.2.0',
- url='https://github.com/plivo/sharq-server',
- author='Plivo Team',
- author_email='hello@plivo.com',
- license=open('LICENSE.txt').read(),
- description='An API queuing server based on the SharQ library.',
- long_description=open('README.md').read(),
- packages=['sharq_server'],
- py_modules=['runner'],
- install_requires=[
- 'Flask==2.2.5',
- 'Jinja2==3.1.6',
- 'MarkupSafe==0.23',
- 'Werkzeug==3.0.6',
- 'gevent==23.9.0',
- 'greenlet==2.0.2',
- 'itsdangerous==0.24',
- 'gunicorn==22.0.0',
- 'ujson==5.4.0'
- ],
- classifiers = [
- 'Development Status :: 5 - Production/Stable',
- 'Intended Audience :: Developers',
- 'License :: OSI Approved :: MIT License',
- 'Operating System :: OS Independent',
- 'Programming Language :: Python',
- 'Topic :: Software Development :: Libraries :: Python Modules'
- ],
- entry_points="""
- [console_scripts]
- sharq-server=runner:run
- """
-)
diff --git a/sharq.conf b/sharq.conf
deleted file mode 100644
index 9588f84..0000000
--- a/sharq.conf
+++ /dev/null
@@ -1,21 +0,0 @@
-[sharq]
-job_expire_interval : 1000 ; in milliseconds
-job_requeue_interval : 1000 ; in milliseconds
-default_job_requeue_limit : 0 ; value of -1 retries infinitely
-
-[sharq-server]
-host : 127.0.0.1
-port : 8080
-workers : 1 ; optional
-accesslog : /tmp/sharq.log ; optional
-
-[redis]
-db : 0
-key_prefix : sharq_server
-conn_type : tcp_sock ; or unix_sock
-;; unix connection settings
-unix_socket_path : /tmp/redis.sock
-;; tcp connection settings
-port : 6379
-host : 127.0.0.1
-clustered : true
diff --git a/sharq_server/__init__.py b/sharq_server/__init__.py
deleted file mode 100644
index 6432b9f..0000000
--- a/sharq_server/__init__.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from .server import SharQServer, setup_server
-
-__version__ = '0.2.0'
\ No newline at end of file
diff --git a/sharq_server/server.py b/sharq_server/server.py
deleted file mode 100644
index 3a2e825..0000000
--- a/sharq_server/server.py
+++ /dev/null
@@ -1,300 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2014 Plivo Team. See LICENSE.txt for details.
-import os
-import gevent
-import configparser
-import ujson as json
-from flask import Flask, request, jsonify
-from redis.exceptions import LockError
-import traceback
-from sharq import SharQ
-
-
-class SharQServer(object):
- """Defines a HTTP based API on top of SharQ and
- exposed the app to run the server.
- """
-
- def __init__(self, config_path):
- """Load the SharQ config and define the routes."""
- # read the configs required by sharq-server.
- self.config = configparser.SafeConfigParser()
- self.config.read(config_path)
- # pass the config file to configure the SharQ core.
- self.sq = SharQ(config_path)
- self.app = Flask(__name__)
- # set the routes
- self.app.add_url_rule(
- '/', view_func=self._view_index, methods=['GET'])
- self.app.add_url_rule(
- '/enqueue///',
- view_func=self._view_enqueue, methods=['POST'])
- self.app.add_url_rule(
- '/dequeue/', defaults={'queue_type': 'default'},
- view_func=self._view_dequeue, methods=['GET'])
- self.app.add_url_rule(
- '/dequeue//',
- view_func=self._view_dequeue, methods=['GET'])
- self.app.add_url_rule(
- '/finish////',
- view_func=self._view_finish, methods=['POST'])
- self.app.add_url_rule(
- '/interval///',
- view_func=self._view_interval, methods=['POST'])
- self.app.add_url_rule(
- '/metrics/', defaults={'queue_type': None, 'queue_id': None},
- view_func=self._view_metrics, methods=['GET'])
- self.app.add_url_rule(
- '/metrics//', defaults={'queue_id': None},
- view_func=self._view_metrics, methods=['GET'])
- self.app.add_url_rule(
- '/metrics///',
- view_func=self._view_metrics, methods=['GET'])
- self.app.add_url_rule(
- '/deletequeue///',
- view_func=self._view_clear_queue, methods=['DELETE'])
- self.app.add_url_rule(
- '/deepstatus/',
- view_func=self._view_deep_status, methods=['GET'])
-
- def requeue(self):
- """Loop endlessly and requeue expired jobs."""
- job_requeue_interval = float(
- self.config.get('sharq', 'job_requeue_interval'))
- while True:
- try:
- self.sq.requeue()
- except Exception as e:
- traceback.print_exc()
- gevent.sleep(job_requeue_interval / 1000.00) # in seconds
-
- def requeue_with_lock(self):
- """Loop endlessly and requeue expired jobs, but with a distributed lock"""
- enable_requeue_script = self.config.get('sharq', 'enable_requeue_script')
- if enable_requeue_script == "false":
- print("requeue script disabled")
- return
-
- job_requeue_interval = float(
- self.config.get('sharq', 'job_requeue_interval'))
-
- print("start requeue loop: job_requeue_interval = %f" % (job_requeue_interval))
- while True:
- try:
- with self.sq.redis_client().lock('sharq-requeue-lock-key', timeout=15):
- try:
- self.sq.requeue()
- except Exception as e:
- traceback.print_exc()
- except LockError:
- # the lock wasn't acquired within specified time
- pass
- finally:
- gevent.sleep(job_requeue_interval / 1000.00) # in seconds
-
- def _view_index(self):
- """Greetings at the index."""
- return jsonify(**{'message': 'Hello, SharQ!'})
-
- def _view_enqueue(self, queue_type, queue_id):
- """Enqueues a job into SharQ."""
- response = {
- 'status': 'failure'
- }
- try:
- request_data = json.loads(request.data)
- except Exception as e:
- response['message'] = e.message
- return jsonify(**response), 400
-
- request_data.update({
- 'queue_type': queue_type,
- 'queue_id': queue_id
- })
- """
- if max_queued_length is present in request param,
- then only queue length will limit to this value
- otherwise client can queue as much calls as he wants
- """
- max_queued_length = request_data['payload'].get('max_queued_length', None)
- if max_queued_length is not None:
- current_queue_length = 0
- try:
- current_queue_length = self.sq.get_queue_length(queue_type, queue_id)
- except Exception as e:
- print("Error occurred while fetching redis key length as {} for auth_id {}".format(e, queue_id))
-
- if current_queue_length < max_queued_length:
- try:
- response = self.sq.enqueue(**request_data)
- response['current_queue_length'] = current_queue_length
- except Exception as e:
- traceback.print_exc()
- response['message'] = e.message
- return jsonify(**response), 400
-
- return jsonify(**response), 201
- else:
- response['message'] = 'Max queue length reached'
- response['current_queue_length'] = current_queue_length
- return jsonify(**response), 429
- else:
- try:
- response = self.sq.enqueue(**request_data)
- except Exception as e:
- traceback.print_exc()
- response['message'] = e.message
- return jsonify(**response), 400
-
- return jsonify(**response), 201
-
- def _view_dequeue(self, queue_type):
- """Dequeues a job from SharQ."""
- response = {
- 'status': 'failure'
- }
-
- request_data = {
- 'queue_type': queue_type
- }
- try:
- response = self.sq.dequeue(**request_data)
- if response['status'] == 'failure':
- return jsonify(**response), 404
- current_queue_length = 0
- try:
- current_queue_length = self.sq.get_queue_length(queue_type, response['queue_id'])
- except Exception as e:
- print("DEQUEUE::Error occurred while fetching redis key length {} for queue_id {}".format(e, response[
- 'queue_id']))
- response['current_queue_length'] = current_queue_length
- except Exception as e:
- import traceback
- for line in traceback.format_exc().splitlines():
- print(line)
- response['message'] = e.message
- return jsonify(**response), 400
-
- return jsonify(**response)
-
- def _view_finish(self, queue_type, queue_id, job_id):
- """Marks a job as finished in SharQ."""
- response = {
- 'status': 'failure'
- }
- request_data = {
- 'queue_type': queue_type,
- 'queue_id': queue_id,
- 'job_id': job_id
- }
-
- try:
- response = self.sq.finish(**request_data)
- if response['status'] == 'failure':
- return jsonify(**response), 404
- except Exception as e:
- traceback.print_exc()
- response['message'] = e.message
- return jsonify(**response), 400
-
- return jsonify(**response)
-
- def _view_interval(self, queue_type, queue_id):
- """Updates the queue interval in SharQ."""
- response = {
- 'status': 'failure'
- }
- try:
- request_data = json.loads(request.data)
- interval = request_data['interval']
- except Exception as e:
- response['message'] = e.message
- return jsonify(**response), 400
-
- request_data = {
- 'queue_type': queue_type,
- 'queue_id': queue_id,
- 'interval': interval
- }
-
- try:
- response = self.sq.interval(**request_data)
- if response['status'] == 'failure':
- return jsonify(**response), 404
- except Exception as e:
- traceback.print_exc()
- response['message'] = e.message
- return jsonify(**response), 400
-
- return jsonify(**response)
-
- def _view_metrics(self, queue_type, queue_id):
- """Gets SharQ metrics based on the params."""
- response = {
- 'status': 'failure'
- }
- request_data = {}
- if queue_type:
- request_data['queue_type'] = queue_type
- if queue_id:
- request_data['queue_id'] = queue_id
-
- try:
- response = self.sq.metrics(**request_data)
- except Exception as e:
- traceback.print_exc()
- response['message'] = e.message
- return jsonify(**response), 400
-
- return jsonify(**response)
-
- def _view_deep_status(self):
- """Checks underlying data store health"""
- try:
-
- self.sq.deep_status()
- response = {
- 'status': "success"
- }
- return jsonify(**response)
- except Exception as e:
- print(e)
- import traceback
- for line in traceback.format_exc().splitlines():
- print(line)
- raise Exception
-
- def _view_clear_queue(self, queue_type, queue_id):
- """remove queue from SharQ based on the queue_type and queue_id."""
- response = {
- 'status': 'failure'
- }
- try:
- request_data = json.loads(request.data)
- except Exception as e:
- response['message'] = e.message
- return jsonify(**response), 400
-
- request_data.update({
- 'queue_type': queue_type,
- 'queue_id': queue_id
- })
- try:
- response = self.sq.clear_queue(**request_data)
- except Exception as e:
- traceback.print_exc()
- response['message'] = e.message
- return jsonify(**response), 400
-
- return jsonify(**response)
-
-
-def setup_server(config_path):
- """Configure SharQ server, start the requeue loop
- and return the server."""
- # configure the SharQ server
- server = SharQServer(config_path)
- # start the requeue loop
- gevent.spawn(server.requeue_with_lock)
-
- return server
\ No newline at end of file
diff --git a/src/config/nginx-sharq.conf b/src/config/nginx-sharq.conf
deleted file mode 100644
index f4d81dd..0000000
--- a/src/config/nginx-sharq.conf
+++ /dev/null
@@ -1,47 +0,0 @@
-server {
- listen *:8000;
- server_name _;
-
- keepalive_timeout 120;
-
- location /status/ {
- rewrite ^/status/$ / break;
- uwsgi_pass unix:///var/run/sharq/sharq.sock;
- include uwsgi_params;
- }
-
- location /deepstatus/ {
- log_not_found off;
- uwsgi_pass unix:///var/run/sharq/sharq.sock;
- include uwsgi_params;
- }
-
- location / {
- # Not needed because it's all in the VPC
- log_not_found off;
- auth_basic "gO AwAy!";
- auth_basic_user_file /etc/nginx/conf.d/sharq-server-basicauth;
- uwsgi_pass unix:///var/run/sharq/sharq.sock;
- include uwsgi_params;
- }
-}
-
-server {
- listen 127.0.0.1:1234;
-
- location / {
- stub_status on;
- access_log off;
- }
-}
-
-server {
- listen 8001;
-
- location /nginx_status {
- stub_status on;
- access_log off;
- allow 127.0.0.1;
- deny all;
- }
-}
diff --git a/src/config/nginx.conf b/src/config/nginx.conf
deleted file mode 100644
index eab9cf8..0000000
--- a/src/config/nginx.conf
+++ /dev/null
@@ -1,53 +0,0 @@
-user www-data;
-worker_processes 4;
-
-error_log /var/log/nginx/error.log warn;
-pid /var/run/nginx.pid;
-
-
-events {
- worker_connections 10000;
-}
-
-worker_rlimit_nofile 20000;
-
-http {
- sendfile on;
- tcp_nopush on;
- tcp_nodelay on;
- keepalive_timeout 65;
- keepalive_requests 2000;
- types_hash_max_size 2048;
- # server_tokens off;
-
- # server_names_hash_bucket_size 64;
- # server_name_in_redirect off;
-
- include /etc/nginx/mime.types;
- default_type application/octet-stream;
-
- ##
- # Logging Settings
- ##
- log_format timed_combined '$remote_addr - $remote_user [$time_local] "$request" '
- '$status $body_bytes_sent "$http_referer" '
- '"$http_user_agent" "$http_x_forwarded_for" '
- '$request_time $upstream_response_time $pipe';
-
- #access_log /var/log/nginx/access.log timed_combined buffer=16K;
- access_log off;
-
- ##
- # Gzip Settings
- ##
- gzip on;
- gzip_disable "MSIE [1-6]\.";
- gzip_vary on;
- gzip_proxied any;
- gzip_comp_level 6;
- gzip_buffers 16 8k;
- gzip_http_version 1.1;
- gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;
-
- include /etc/nginx/conf.d/*.conf;
-}
diff --git a/src/config/sharq-server-basicauth b/src/config/sharq-server-basicauth
deleted file mode 100644
index 36b8cf2..0000000
--- a/src/config/sharq-server-basicauth
+++ /dev/null
@@ -1,3 +0,0 @@
-alan_sms:$apr1$GNoPTSEB$6jp6VL0t9/CvlHcC5CbHv/
-alan_turing:$apr1$uKJe.MIc$JaNp54WrpXIlPy4wCXRW81
-benedict_sms:$apr1$LkVwBNUi$W8n.Ong1pG3MTHzYkd.v50
diff --git a/src/config/sharq.conf b/src/config/sharq.conf
deleted file mode 100644
index 22a0ed3..0000000
--- a/src/config/sharq.conf
+++ /dev/null
@@ -1,14 +0,0 @@
-[sharq]
-job_expire_interval = 1000
-job_requeue_interval = 1000
-default_job_requeue_limit = -1
-
-[redis]
-db = 0
-key_prefix = call
-conn_type = tcp_sock
-unix_socket_path = /tmp/redis.sock
-port = 6379
-host = 127.0.0.1
-clustered = false
-password = hello
\ No newline at end of file
diff --git a/src/config/sharq.conf.ctmpl b/src/config/sharq.conf.ctmpl
deleted file mode 100644
index 2d39fc5..0000000
--- a/src/config/sharq.conf.ctmpl
+++ /dev/null
@@ -1,24 +0,0 @@
-{{$region := env "REGION"}}
-{{$appenv := env "ENVIRONMENT"}}
-{{$team := env "TEAM"}}
-{{$sharq_type := env "SHARQ_TYPE"}}
-
-[sharq]
-job_expire_interval : {{ parseInt (keyOrDefault (printf "%s/%s/%s/%s/config/sharq/job_requeue_interval" $team $appenv $sharq_type $region) "120000" ) }}
-job_requeue_interval : {{ parseInt (keyOrDefault (printf "%s/%s/%s/%s/config/sharq/job_requeue_interval" $team $appenv $sharq_type $region) "3000" ) }}
-default_job_requeue_limit : {{ parseInt (keyOrDefault (printf "%s/%s/%s/%s/config/sharq/default_job_requeue_limit" $team $appenv $sharq_type $region) "0" ) }}
-enable_requeue_script : {{ parseBool (keyOrDefault (printf "%s/%s/%s/%s/config/sharq/enable_requeue_script" $team $appenv $sharq_type $region) "false" ) }}
-
-[sharq-server]
-host : 127.0.0.1
-port : 8080
-accesslog : /tmp/sharq.log
-
-[redis]
-db : 0
-key_prefix : {{ printf "%s/%s/%s/%s/config/redis/key_prefix" $team $appenv $sharq_type $region | key }}
-conn_type : tcp_sock
-port : {{ printf "%s/%s/%s/%s/config/redis/port" $team $appenv $sharq_type $region | key | parseInt }}
-host : {{ printf "%s/%s/%s/%s/config/redis/host" $team $appenv $sharq_type $region | key }}
-clustered : {{ printf "%s/%s/%s/%s/config/redis/clustered" $team $appenv $sharq_type $region | key }}
-password : {{ printf "%s/%s/%s/%s/config/redis/password" $team $appenv $sharq_type $region | key }}
diff --git a/src/config/sharq.ini.ctmpl b/src/config/sharq.ini.ctmpl
deleted file mode 100644
index baf4039..0000000
--- a/src/config/sharq.ini.ctmpl
+++ /dev/null
@@ -1,39 +0,0 @@
-[uwsgi]
-# automatically start master process
-master = true
-
-# try to autoload appropriate plugin if "unknown" option has been specified
-autoload = true
-
-# spawn n uWSGI worker processes
-workers = {{ printf "%s/%s/%s/%s/config/uwsgi/num_workers" (env "TEAM") (env "ENVIRONMENT") (env "SHARQ_TYPE") (env "REGION") | key | parseInt }}
-
-# automatically kill workers on master's death
-no-orphans = true
-
-# write master's pid in file /run/uwsgi///pid
-pidfile = /var/run/sharq/uwsgi.pid
-
-# bind to UNIX socket at /run/uwsgi///socket
-socket = /var/run/sharq/sharq.sock
-
-# set mode of created UNIX socket
-chmod-socket = 666
-
-{{$loggingKeyName := printf "%s/%s/%s/%s/config/logging/disable" (env "TEAM") (env "ENVIRONMENT") (env "SHARQ_TYPE") (env "REGION")}}
-{{ if key $loggingKeyName | parseBool }}
-disable-logging=True
-{{ end }}
-
-# daemonize
-#daemonize=False
-
-# sharq related
-chdir = /opt/sharq-server
-virtualenv = /opt/sharq-server
-module = wsgi:app
-gevent = 1024
-
-# configure sharq config path
-env = SHARQ_CONFIG=/etc/sharq-server/config/sharq.conf
-log-format = {"client_addr":"%(addr)","request_method":"%(method)","request_uri":"%(uri)","response_size":%(rsize),"response_time":%(msecs),"status":%(status),"protocol":"%(proto)","timestamp":%(time),"level":"info"}
\ No newline at end of file
diff --git a/src/config/supervisord.conf b/src/config/supervisord.conf
deleted file mode 100644
index d24d00f..0000000
--- a/src/config/supervisord.conf
+++ /dev/null
@@ -1,18 +0,0 @@
-[supervisord]
-nodaemon=true
-user=root
-
-[program:uwsgi]
-command=/opt/sharq-server/bin/uwsgi --ini /etc/sharq-server/config/sharq.ini --die-on-term
-stdout_logfile=/dev/stdout
-stdout_logfile_maxbytes=0
-stderr_logfile=/dev/stderr
-stderr_logfile_maxbytes=0
-stopsignal=INT
-
-[program:nginx]
-command=nginx -g 'daemon off;'
-stdout_logfile=/dev/stdout
-stdout_logfile_maxbytes=0
-stderr_logfile=/dev/stderr
-stderr_logfile_maxbytes=0
diff --git a/tests.py b/tests.py
deleted file mode 100644
index d7af7db..0000000
--- a/tests.py
+++ /dev/null
@@ -1,173 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2014 Plivo Team. See LICENSE.txt for details.
-import unittest
-import ujson as json
-from sharq_server import setup_server
-
-
-class SharQServerTestCase(unittest.TestCase):
-
- def setUp(self):
- # get test client & redis connection
- server = setup_server('./sharq.conf')
- self.app = server.app.test_client()
- self.r = server.sq._r
-
- # flush redis
- self.r.flushdb()
-
- def test_root(self):
- response = self.app.get('/')
- self.assertEqual(response.status_code, 200)
- self.assertEqual(
- json.loads(response.data), {'message': 'Hello, SharQ!'})
-
- def test_enqueue(self):
- request_params = {
- 'job_id': 'ef022088-d2b3-44ad-bf0d-a93d6d93b82c',
- 'payload': {'message': 'Hello, world.'},
- 'interval': 1000
- }
- response = self.app.post(
- '/enqueue/sms/johdoe/', data=json.dumps(request_params),
- content_type='application/json')
- self.assertEqual(response.status_code, 201)
- response_data = json.loads(response.data)
- self.assertEqual(response_data['status'], 'queued')
-
- request_params = {
- 'job_id': 'ef022088-d2b3-44ad-bf1d-a93d6d93b82c',
- 'payload': {'message': 'Hello, world.'},
- 'interval': 1000,
- 'requeue_limit': 10
- }
- response = self.app.post(
- '/enqueue/sms/johdoe/', data=json.dumps(request_params),
- content_type='application/json')
- self.assertEqual(response.status_code, 201)
- response_data = json.loads(response.data)
- self.assertEqual(response_data['status'], 'queued')
-
- def test_dequeue_fail(self):
- response = self.app.get('/dequeue/')
- self.assertEqual(response.status_code, 404)
- response_data = json.loads(response.data)
- self.assertEqual(response_data['status'], 'failure')
-
- response = self.app.get('/dequeue/sms/')
- self.assertEqual(response.status_code, 404)
- response_data = json.loads(response.data)
- self.assertEqual(response_data['status'], 'failure')
-
- def test_dequeue(self):
- # enqueue a job
- request_params = {
- 'job_id': 'ef022088-d2b3-44ad-bf0d-a93d6d93b82c',
- 'payload': {'message': 'Hello, world.'},
- 'interval': 1000
- }
- self.app.post(
- '/enqueue/sms/johndoe/', data=json.dumps(request_params),
- content_type='application/json')
-
- # dequeue a job
- response = self.app.get('/dequeue/sms/')
- self.assertEqual(response.status_code, 200)
- response_data = json.loads(response.data)
- self.assertEqual(response_data['status'], 'success')
- self.assertTrue(
- response_data['job_id'], 'ef022088-d2b3-44ad-bf0d-a93d6d93b82c')
- self.assertEqual(
- response_data['payload'], {'message': 'Hello, world.'})
- self.assertEqual(response_data['queue_id'], 'johndoe')
- self.assertEqual(response_data['requeues_remaining'], -1) # from the config
-
- def test_finish_fail(self):
- # mark a non existent job as finished
- response = self.app.post(
- '/finish/sms/johndoe/ef022088-d2b3-44ad-bf0d-a93d6d93b82c/')
- self.assertEqual(response.status_code, 404)
- response_data = json.loads(response.data)
- self.assertEqual(response_data['status'], 'failure')
-
- def test_finish(self):
- # enqueue a job
- request_params = {
- 'job_id': 'ef022088-d2b3-44ad-bf0d-a93d6d93b82c',
- 'payload': {'message': 'Hello, world.'},
- 'interval': 1000
- }
- self.app.post(
- '/enqueue/sms/johndoe/', data=json.dumps(request_params),
- content_type='application/json')
-
- # dequeue a job
- self.app.get('/dequeue/sms/')
-
- # mark the job as finished
- response = self.app.post(
- '/finish/sms/johndoe/ef022088-d2b3-44ad-bf0d-a93d6d93b82c/')
- self.assertEqual(response.status_code, 200)
- response_data = json.loads(response.data)
- self.assertEqual(response_data['status'], 'success')
-
- def test_interval(self):
- # enqueue a job
- request_params = {
- 'job_id': 'ef022088-d2b3-44ad-bf0d-a93d6d93b82c',
- 'payload': {'message': 'Hello, world.'},
- 'interval': 1000
- }
- self.app.post(
- '/enqueue/sms/johndoe/', data=json.dumps(request_params),
- content_type='application/json')
-
- # change the interval
- request_params = {
- 'interval': 5000
- }
- response = self.app.post(
- '/interval/sms/johndoe/', data=json.dumps(request_params),
- content_type='application/json')
- response_data = json.loads(response.data)
- self.assertEqual(response_data['status'], 'success')
-
- def test_interval_fail(self):
- # change the interval
- request_params = {
- 'interval': 5000
- }
- response = self.app.post(
- '/interval/sms/johndoe/', data=json.dumps(request_params),
- content_type='application/json')
- response_data = json.loads(response.data)
- self.assertEqual(response_data['status'], 'failure')
-
- def test_metrics(self):
- response = self.app.get('/metrics/')
- response_data = json.loads(response.data)
- self.assertEqual(response_data['status'], 'success')
- self.assertIn('queue_types', response_data)
- self.assertIn('enqueue_counts', response_data)
- self.assertIn('dequeue_counts', response_data)
-
- def test_metrics_with_queue_type(self):
- response = self.app.get('/metrics/sms/')
- response_data = json.loads(response.data)
- self.assertEqual(response_data['status'], 'success')
- self.assertIn('queue_ids', response_data)
-
- def test_metrics_with_queue_type_and_queue_id(self):
- response = self.app.get('/metrics/sms/johndoe/')
- response_data = json.loads(response.data)
- self.assertEqual(response_data['status'], 'success')
- self.assertIn('queue_length', response_data)
- self.assertIn('enqueue_counts', response_data)
- self.assertIn('dequeue_counts', response_data)
-
- def tearDown(self):
- # flush redis
- self.r.flushdb()
-
-if __name__ == '__main__':
- unittest.main()
diff --git a/tests/test_routes.py b/tests/test_routes.py
new file mode 100644
index 0000000..94038fb
--- /dev/null
+++ b/tests/test_routes.py
@@ -0,0 +1,190 @@
+# tests/test_routes.py
+
+# -*- coding: utf-8 -*-
+# Copyright ...
+
+import unittest
+import ujson as json
+from httpx import AsyncClient, ASGITransport
+from starlette.types import ASGIApp
+from fq_server import setup_server
+
+
+class FQServerTestCase(unittest.IsolatedAsyncioTestCase):
+ async def asyncSetUp(self):
+ # build server and Starlette app
+ server = setup_server("./default.conf")
+ self.server = server
+ self.app: ASGIApp = server.app
+
+ # queue + redis client (async)
+ self.queue = server.queue
+ await self.queue._initialize() # important: same loop as tests
+ self.r = self.queue._r
+
+ # flush redis before each test
+ await self.r.flushdb()
+
+ # async HTTP client bound to this ASGI app & this loop
+ transport = ASGITransport(app=self.app)
+ self.client = AsyncClient(transport=transport, base_url="http://test")
+
+ async def asyncTearDown(self):
+ # flush redis after each test
+ await self.r.flushdb()
+ await self.client.aclose()
+
+ async def test_root(self):
+ response = await self.client.get("/")
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.json(), {"message": "Hello, FQS!"})
+
+ async def test_enqueue(self):
+ request_params = {
+ "job_id": "ef022088-d2b3-44ad-bf0d-a93d6d93b82c",
+ "payload": {"message": "Hello, world."},
+ "interval": 1000,
+ }
+ response = await self.client.post(
+ "/enqueue/sms/johdoe/",
+ content=json.dumps(request_params),
+ headers={"Content-Type": "application/json"},
+ )
+ self.assertEqual(response.status_code, 201)
+ data = response.json()
+ self.assertEqual(data["status"], "queued")
+
+ request_params = {
+ "job_id": "ef022088-d2b3-44ad-bf1d-a93d6d93b82c",
+ "payload": {"message": "Hello, world."},
+ "interval": 1000,
+ "requeue_limit": 10,
+ }
+ response = await self.client.post(
+ "/enqueue/sms/johdoe/",
+ content=json.dumps(request_params),
+ headers={"Content-Type": "application/json"},
+ )
+ self.assertEqual(response.status_code, 201)
+ data = response.json()
+ self.assertEqual(data["status"], "queued")
+
+ async def test_dequeue_fail(self):
+ response = await self.client.get("/dequeue/")
+ # your Starlette handler returns 400 or 404 – pick what your code actually does
+ self.assertEqual(response.status_code, 404)
+ self.assertEqual(response.json()["status"], "failure")
+
+ response = await self.client.get("/dequeue/sms/")
+ self.assertEqual(response.status_code, 404)
+ self.assertEqual(response.json()["status"], "failure")
+
+ async def test_dequeue(self):
+ # enqueue a job
+ request_params = {
+ "job_id": "ef022088-d2b3-44ad-bf0d-a93d6d93b82c",
+ "payload": {"message": "Hello, world."},
+ "interval": 1000,
+ }
+ await self.client.post(
+ "/enqueue/sms/johndoe/",
+ content=json.dumps(request_params),
+ headers={"Content-Type": "application/json"},
+ )
+
+ # dequeue a job
+ response = await self.client.get("/dequeue/sms/")
+ self.assertEqual(response.status_code, 200)
+ data = response.json()
+ self.assertEqual(data["status"], "success")
+ self.assertEqual(data["job_id"], "ef022088-d2b3-44ad-bf0d-a93d6d93b82c")
+ self.assertEqual(data["payload"], {"message": "Hello, world."})
+ self.assertEqual(data["queue_id"], "johndoe")
+ self.assertEqual(data["requeues_remaining"], -1) # from config
+
+ async def test_finish_fail(self):
+ response = await self.client.post(
+ "/finish/sms/johndoe/ef022088-d2b3-44ad-bf0d-a93d6d93b82c/"
+ )
+ self.assertEqual(response.status_code, 404)
+ self.assertEqual(response.json()["status"], "failure")
+
+ async def test_finish(self):
+ # enqueue a job
+ request_params = {
+ "job_id": "ef022088-d2b3-44ad-bf0d-a93d6d93b82c",
+ "payload": {"message": "Hello, world."},
+ "interval": 1000,
+ }
+ await self.client.post(
+ "/enqueue/sms/johndoe/",
+ content=json.dumps(request_params),
+ headers={"Content-Type": "application/json"},
+ )
+
+ # dequeue a job
+ await self.client.get("/dequeue/sms/")
+
+ # mark it as finished
+ response = await self.client.post(
+ "/finish/sms/johndoe/ef022088-d2b3-44ad-bf0d-a93d6d93b82c/"
+ )
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.json()["status"], "success")
+
+ async def test_interval(self):
+ # enqueue a job
+ request_params = {
+ "job_id": "ef022088-d2b3-44ad-bf0d-a93d6d93b82c",
+ "payload": {"message": "Hello, world."},
+ "interval": 1000,
+ }
+ await self.client.post(
+ "/enqueue/sms/johndoe/",
+ content=json.dumps(request_params),
+ headers={"Content-Type": "application/json"},
+ )
+
+ # change the interval
+ request_params = {"interval": 5000}
+ response = await self.client.post(
+ "/interval/sms/johndoe/",
+ content=json.dumps(request_params),
+ headers={"Content-Type": "application/json"},
+ )
+ self.assertEqual(response.json()["status"], "success")
+
+ async def test_interval_fail(self):
+ request_params = {"interval": 5000}
+ response = await self.client.post(
+ "/interval/sms/johndoe/",
+ content=json.dumps(request_params),
+ headers={"Content-Type": "application/json"},
+ )
+ self.assertEqual(response.json()["status"], "failure")
+
+ async def test_metrics(self):
+ response = await self.client.get("/metrics/")
+ data = response.json()
+ self.assertEqual(data["status"], "success")
+ self.assertIn("queue_types", data)
+ self.assertIn("enqueue_counts", data)
+ self.assertIn("dequeue_counts", data)
+
+ async def test_metrics_with_queue_type(self):
+ response = await self.client.get("/metrics/sms/")
+ data = response.json()
+ self.assertEqual(data["status"], "success")
+ self.assertIn("queue_ids", data)
+
+ async def test_metrics_with_queue_type_and_queue_id(self):
+ response = await self.client.get("/metrics/sms/johndoe/")
+ data = response.json()
+ self.assertEqual(data["status"], "success")
+ self.assertIn("queue_length", data)
+ self.assertIn("enqueue_counts", data)
+ self.assertIn("dequeue_counts", data)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/uv.lock b/uv.lock
new file mode 100644
index 0000000..2fd94cb
--- /dev/null
+++ b/uv.lock
@@ -0,0 +1,497 @@
+version = 1
+revision = 3
+requires-python = ">=3.12"
+
+[[package]]
+name = "anyio"
+version = "4.11.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "idna" },
+ { name = "sniffio" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094, upload-time = "2025-09-23T09:19:12.58Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" },
+]
+
+[[package]]
+name = "certifi"
+version = "2025.11.12"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" },
+]
+
+[[package]]
+name = "click"
+version = "8.3.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" },
+]
+
+[[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.12.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/89/26/4a96807b193b011588099c3b5c89fbb05294e5b90e71018e065465f34eb6/coverage-7.12.0.tar.gz", hash = "sha256:fc11e0a4e372cb5f282f16ef90d4a585034050ccda536451901abfb19a57f40c", size = 819341, upload-time = "2025-11-18T13:34:20.766Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/02/bf/638c0427c0f0d47638242e2438127f3c8ee3cfc06c7fdeb16778ed47f836/coverage-7.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:29644c928772c78512b48e14156b81255000dcfd4817574ff69def189bcb3647", size = 217704, upload-time = "2025-11-18T13:32:28.906Z" },
+ { url = "https://files.pythonhosted.org/packages/08/e1/706fae6692a66c2d6b871a608bbde0da6281903fa0e9f53a39ed441da36a/coverage-7.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8638cbb002eaa5d7c8d04da667813ce1067080b9a91099801a0053086e52b736", size = 218064, upload-time = "2025-11-18T13:32:30.161Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/8b/eb0231d0540f8af3ffda39720ff43cb91926489d01524e68f60e961366e4/coverage-7.12.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:083631eeff5eb9992c923e14b810a179798bb598e6a0dd60586819fc23be6e60", size = 249560, upload-time = "2025-11-18T13:32:31.835Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/a1/67fb52af642e974d159b5b379e4d4c59d0ebe1288677fbd04bbffe665a82/coverage-7.12.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:99d5415c73ca12d558e07776bd957c4222c687b9f1d26fa0e1b57e3598bdcde8", size = 252318, upload-time = "2025-11-18T13:32:33.178Z" },
+ { url = "https://files.pythonhosted.org/packages/41/e5/38228f31b2c7665ebf9bdfdddd7a184d56450755c7e43ac721c11a4b8dab/coverage-7.12.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e949ebf60c717c3df63adb4a1a366c096c8d7fd8472608cd09359e1bd48ef59f", size = 253403, upload-time = "2025-11-18T13:32:34.45Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/4b/df78e4c8188f9960684267c5a4897836f3f0f20a20c51606ee778a1d9749/coverage-7.12.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d907ddccbca819afa2cd014bc69983b146cca2735a0b1e6259b2a6c10be1e70", size = 249984, upload-time = "2025-11-18T13:32:35.747Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/51/bb163933d195a345c6f63eab9e55743413d064c291b6220df754075c2769/coverage-7.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b1518ecbad4e6173f4c6e6c4a46e49555ea5679bf3feda5edb1b935c7c44e8a0", size = 251339, upload-time = "2025-11-18T13:32:37.352Z" },
+ { url = "https://files.pythonhosted.org/packages/15/40/c9b29cdb8412c837cdcbc2cfa054547dd83affe6cbbd4ce4fdb92b6ba7d1/coverage-7.12.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:51777647a749abdf6f6fd8c7cffab12de68ab93aab15efc72fbbb83036c2a068", size = 249489, upload-time = "2025-11-18T13:32:39.212Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/da/b3131e20ba07a0de4437a50ef3b47840dfabf9293675b0cd5c2c7f66dd61/coverage-7.12.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:42435d46d6461a3b305cdfcad7cdd3248787771f53fe18305548cba474e6523b", size = 249070, upload-time = "2025-11-18T13:32:40.598Z" },
+ { url = "https://files.pythonhosted.org/packages/70/81/b653329b5f6302c08d683ceff6785bc60a34be9ae92a5c7b63ee7ee7acec/coverage-7.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5bcead88c8423e1855e64b8057d0544e33e4080b95b240c2a355334bb7ced937", size = 250929, upload-time = "2025-11-18T13:32:42.915Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/00/250ac3bca9f252a5fb1338b5ad01331ebb7b40223f72bef5b1b2cb03aa64/coverage-7.12.0-cp312-cp312-win32.whl", hash = "sha256:dcbb630ab034e86d2a0f79aefd2be07e583202f41e037602d438c80044957baa", size = 220241, upload-time = "2025-11-18T13:32:44.665Z" },
+ { url = "https://files.pythonhosted.org/packages/64/1c/77e79e76d37ce83302f6c21980b45e09f8aa4551965213a10e62d71ce0ab/coverage-7.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:2fd8354ed5d69775ac42986a691fbf68b4084278710cee9d7c3eaa0c28fa982a", size = 221051, upload-time = "2025-11-18T13:32:46.008Z" },
+ { url = "https://files.pythonhosted.org/packages/31/f5/641b8a25baae564f9e52cac0e2667b123de961985709a004e287ee7663cc/coverage-7.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:737c3814903be30695b2de20d22bcc5428fdae305c61ba44cdc8b3252984c49c", size = 219692, upload-time = "2025-11-18T13:32:47.372Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/14/771700b4048774e48d2c54ed0c674273702713c9ee7acdfede40c2666747/coverage-7.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:47324fffca8d8eae7e185b5bb20c14645f23350f870c1649003618ea91a78941", size = 217725, upload-time = "2025-11-18T13:32:49.22Z" },
+ { url = "https://files.pythonhosted.org/packages/17/a7/3aa4144d3bcb719bf67b22d2d51c2d577bf801498c13cb08f64173e80497/coverage-7.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ccf3b2ede91decd2fb53ec73c1f949c3e034129d1e0b07798ff1d02ea0c8fa4a", size = 218098, upload-time = "2025-11-18T13:32:50.78Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/9c/b846bbc774ff81091a12a10203e70562c91ae71badda00c5ae5b613527b1/coverage-7.12.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b365adc70a6936c6b0582dc38746b33b2454148c02349345412c6e743efb646d", size = 249093, upload-time = "2025-11-18T13:32:52.554Z" },
+ { url = "https://files.pythonhosted.org/packages/76/b6/67d7c0e1f400b32c883e9342de4a8c2ae7c1a0b57c5de87622b7262e2309/coverage-7.12.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bc13baf85cd8a4cfcf4a35c7bc9d795837ad809775f782f697bf630b7e200211", size = 251686, upload-time = "2025-11-18T13:32:54.862Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/75/b095bd4b39d49c3be4bffbb3135fea18a99a431c52dd7513637c0762fecb/coverage-7.12.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:099d11698385d572ceafb3288a5b80fe1fc58bf665b3f9d362389de488361d3d", size = 252930, upload-time = "2025-11-18T13:32:56.417Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/f3/466f63015c7c80550bead3093aacabf5380c1220a2a93c35d374cae8f762/coverage-7.12.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:473dc45d69694069adb7680c405fb1e81f60b2aff42c81e2f2c3feaf544d878c", size = 249296, upload-time = "2025-11-18T13:32:58.074Z" },
+ { url = "https://files.pythonhosted.org/packages/27/86/eba2209bf2b7e28c68698fc13437519a295b2d228ba9e0ec91673e09fa92/coverage-7.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:583f9adbefd278e9de33c33d6846aa8f5d164fa49b47144180a0e037f0688bb9", size = 251068, upload-time = "2025-11-18T13:32:59.646Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/55/ca8ae7dbba962a3351f18940b359b94c6bafdd7757945fdc79ec9e452dc7/coverage-7.12.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b2089cc445f2dc0af6f801f0d1355c025b76c24481935303cf1af28f636688f0", size = 249034, upload-time = "2025-11-18T13:33:01.481Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/d7/39136149325cad92d420b023b5fd900dabdd1c3a0d1d5f148ef4a8cedef5/coverage-7.12.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:950411f1eb5d579999c5f66c62a40961f126fc71e5e14419f004471957b51508", size = 248853, upload-time = "2025-11-18T13:33:02.935Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/b6/76e1add8b87ef60e00643b0b7f8f7bb73d4bf5249a3be19ebefc5793dd25/coverage-7.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b1aab7302a87bafebfe76b12af681b56ff446dc6f32ed178ff9c092ca776e6bc", size = 250619, upload-time = "2025-11-18T13:33:04.336Z" },
+ { url = "https://files.pythonhosted.org/packages/95/87/924c6dc64f9203f7a3c1832a6a0eee5a8335dbe5f1bdadcc278d6f1b4d74/coverage-7.12.0-cp313-cp313-win32.whl", hash = "sha256:d7e0d0303c13b54db495eb636bc2465b2fb8475d4c8bcec8fe4b5ca454dfbae8", size = 220261, upload-time = "2025-11-18T13:33:06.493Z" },
+ { url = "https://files.pythonhosted.org/packages/91/77/dd4aff9af16ff776bf355a24d87eeb48fc6acde54c907cc1ea89b14a8804/coverage-7.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:ce61969812d6a98a981d147d9ac583a36ac7db7766f2e64a9d4d059c2fe29d07", size = 221072, upload-time = "2025-11-18T13:33:07.926Z" },
+ { url = "https://files.pythonhosted.org/packages/70/49/5c9dc46205fef31b1b226a6e16513193715290584317fd4df91cdaf28b22/coverage-7.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:bcec6f47e4cb8a4c2dc91ce507f6eefc6a1b10f58df32cdc61dff65455031dfc", size = 219702, upload-time = "2025-11-18T13:33:09.631Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/62/f87922641c7198667994dd472a91e1d9b829c95d6c29529ceb52132436ad/coverage-7.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:459443346509476170d553035e4a3eed7b860f4fe5242f02de1010501956ce87", size = 218420, upload-time = "2025-11-18T13:33:11.153Z" },
+ { url = "https://files.pythonhosted.org/packages/85/dd/1cc13b2395ef15dbb27d7370a2509b4aee77890a464fb35d72d428f84871/coverage-7.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:04a79245ab2b7a61688958f7a855275997134bc84f4a03bc240cf64ff132abf6", size = 218773, upload-time = "2025-11-18T13:33:12.569Z" },
+ { url = "https://files.pythonhosted.org/packages/74/40/35773cc4bb1e9d4658d4fb669eb4195b3151bef3bbd6f866aba5cd5dac82/coverage-7.12.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:09a86acaaa8455f13d6a99221d9654df249b33937b4e212b4e5a822065f12aa7", size = 260078, upload-time = "2025-11-18T13:33:14.037Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/ee/231bb1a6ffc2905e396557585ebc6bdc559e7c66708376d245a1f1d330fc/coverage-7.12.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:907e0df1b71ba77463687a74149c6122c3f6aac56c2510a5d906b2f368208560", size = 262144, upload-time = "2025-11-18T13:33:15.601Z" },
+ { url = "https://files.pythonhosted.org/packages/28/be/32f4aa9f3bf0b56f3971001b56508352c7753915345d45fab4296a986f01/coverage-7.12.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b57e2d0ddd5f0582bae5437c04ee71c46cd908e7bc5d4d0391f9a41e812dd12", size = 264574, upload-time = "2025-11-18T13:33:17.354Z" },
+ { url = "https://files.pythonhosted.org/packages/68/7c/00489fcbc2245d13ab12189b977e0cf06ff3351cb98bc6beba8bd68c5902/coverage-7.12.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:58c1c6aa677f3a1411fe6fb28ec3a942e4f665df036a3608816e0847fad23296", size = 259298, upload-time = "2025-11-18T13:33:18.958Z" },
+ { url = "https://files.pythonhosted.org/packages/96/b4/f0760d65d56c3bea95b449e02570d4abd2549dc784bf39a2d4721a2d8ceb/coverage-7.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4c589361263ab2953e3c4cd2a94db94c4ad4a8e572776ecfbad2389c626e4507", size = 262150, upload-time = "2025-11-18T13:33:20.644Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/71/9a9314df00f9326d78c1e5a910f520d599205907432d90d1c1b7a97aa4b1/coverage-7.12.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:91b810a163ccad2e43b1faa11d70d3cf4b6f3d83f9fd5f2df82a32d47b648e0d", size = 259763, upload-time = "2025-11-18T13:33:22.189Z" },
+ { url = "https://files.pythonhosted.org/packages/10/34/01a0aceed13fbdf925876b9a15d50862eb8845454301fe3cdd1df08b2182/coverage-7.12.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:40c867af715f22592e0d0fb533a33a71ec9e0f73a6945f722a0c85c8c1cbe3a2", size = 258653, upload-time = "2025-11-18T13:33:24.239Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/04/81d8fd64928acf1574bbb0181f66901c6c1c6279c8ccf5f84259d2c68ae9/coverage-7.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:68b0d0a2d84f333de875666259dadf28cc67858bc8fd8b3f1eae84d3c2bec455", size = 260856, upload-time = "2025-11-18T13:33:26.365Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/76/fa2a37bfaeaf1f766a2d2360a25a5297d4fb567098112f6517475eee120b/coverage-7.12.0-cp313-cp313t-win32.whl", hash = "sha256:73f9e7fbd51a221818fd11b7090eaa835a353ddd59c236c57b2199486b116c6d", size = 220936, upload-time = "2025-11-18T13:33:28.165Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/52/60f64d932d555102611c366afb0eb434b34266b1d9266fc2fe18ab641c47/coverage-7.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:24cff9d1f5743f67db7ba46ff284018a6e9aeb649b67aa1e70c396aa1b7cb23c", size = 222001, upload-time = "2025-11-18T13:33:29.656Z" },
+ { url = "https://files.pythonhosted.org/packages/77/df/c303164154a5a3aea7472bf323b7c857fed93b26618ed9fc5c2955566bb0/coverage-7.12.0-cp313-cp313t-win_arm64.whl", hash = "sha256:c87395744f5c77c866d0f5a43d97cc39e17c7f1cb0115e54a2fe67ca75c5d14d", size = 220273, upload-time = "2025-11-18T13:33:31.415Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/2e/fc12db0883478d6e12bbd62d481210f0c8daf036102aa11434a0c5755825/coverage-7.12.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a1c59b7dc169809a88b21a936eccf71c3895a78f5592051b1af8f4d59c2b4f92", size = 217777, upload-time = "2025-11-18T13:33:32.86Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/c1/ce3e525d223350c6ec16b9be8a057623f54226ef7f4c2fee361ebb6a02b8/coverage-7.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8787b0f982e020adb732b9f051f3e49dd5054cebbc3f3432061278512a2b1360", size = 218100, upload-time = "2025-11-18T13:33:34.532Z" },
+ { url = "https://files.pythonhosted.org/packages/15/87/113757441504aee3808cb422990ed7c8bcc2d53a6779c66c5adef0942939/coverage-7.12.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5ea5a9f7dc8877455b13dd1effd3202e0bca72f6f3ab09f9036b1bcf728f69ac", size = 249151, upload-time = "2025-11-18T13:33:36.135Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/1d/9529d9bd44049b6b05bb319c03a3a7e4b0a8a802d28fa348ad407e10706d/coverage-7.12.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fdba9f15849534594f60b47c9a30bc70409b54947319a7c4fd0e8e3d8d2f355d", size = 251667, upload-time = "2025-11-18T13:33:37.996Z" },
+ { url = "https://files.pythonhosted.org/packages/11/bb/567e751c41e9c03dc29d3ce74b8c89a1e3396313e34f255a2a2e8b9ebb56/coverage-7.12.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a00594770eb715854fb1c57e0dea08cce6720cfbc531accdb9850d7c7770396c", size = 253003, upload-time = "2025-11-18T13:33:39.553Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/b3/c2cce2d8526a02fb9e9ca14a263ca6fc074449b33a6afa4892838c903528/coverage-7.12.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5560c7e0d82b42eb1951e4f68f071f8017c824ebfd5a6ebe42c60ac16c6c2434", size = 249185, upload-time = "2025-11-18T13:33:42.086Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/a7/967f93bb66e82c9113c66a8d0b65ecf72fc865adfba5a145f50c7af7e58d/coverage-7.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d6c2e26b481c9159c2773a37947a9718cfdc58893029cdfb177531793e375cfc", size = 251025, upload-time = "2025-11-18T13:33:43.634Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/b2/f2f6f56337bc1af465d5b2dc1ee7ee2141b8b9272f3bf6213fcbc309a836/coverage-7.12.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6e1a8c066dabcde56d5d9fed6a66bc19a2883a3fe051f0c397a41fc42aedd4cc", size = 248979, upload-time = "2025-11-18T13:33:46.04Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/7a/bf4209f45a4aec09d10a01a57313a46c0e0e8f4c55ff2965467d41a92036/coverage-7.12.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f7ba9da4726e446d8dd8aae5a6cd872511184a5d861de80a86ef970b5dacce3e", size = 248800, upload-time = "2025-11-18T13:33:47.546Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/b7/1e01b8696fb0521810f60c5bbebf699100d6754183e6cc0679bf2ed76531/coverage-7.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e0f483ab4f749039894abaf80c2f9e7ed77bbf3c737517fb88c8e8e305896a17", size = 250460, upload-time = "2025-11-18T13:33:49.537Z" },
+ { url = "https://files.pythonhosted.org/packages/71/ae/84324fb9cb46c024760e706353d9b771a81b398d117d8c1fe010391c186f/coverage-7.12.0-cp314-cp314-win32.whl", hash = "sha256:76336c19a9ef4a94b2f8dc79f8ac2da3f193f625bb5d6f51a328cd19bfc19933", size = 220533, upload-time = "2025-11-18T13:33:51.16Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/71/1033629deb8460a8f97f83e6ac4ca3b93952e2b6f826056684df8275e015/coverage-7.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:7c1059b600aec6ef090721f8f633f60ed70afaffe8ecab85b59df748f24b31fe", size = 221348, upload-time = "2025-11-18T13:33:52.776Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/5f/ac8107a902f623b0c251abdb749be282dc2ab61854a8a4fcf49e276fce2f/coverage-7.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:172cf3a34bfef42611963e2b661302a8931f44df31629e5b1050567d6b90287d", size = 219922, upload-time = "2025-11-18T13:33:54.316Z" },
+ { url = "https://files.pythonhosted.org/packages/79/6e/f27af2d4da367f16077d21ef6fe796c874408219fa6dd3f3efe7751bd910/coverage-7.12.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:aa7d48520a32cb21c7a9b31f81799e8eaec7239db36c3b670be0fa2403828d1d", size = 218511, upload-time = "2025-11-18T13:33:56.343Z" },
+ { url = "https://files.pythonhosted.org/packages/67/dd/65fd874aa460c30da78f9d259400d8e6a4ef457d61ab052fd248f0050558/coverage-7.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:90d58ac63bc85e0fb919f14d09d6caa63f35a5512a2205284b7816cafd21bb03", size = 218771, upload-time = "2025-11-18T13:33:57.966Z" },
+ { url = "https://files.pythonhosted.org/packages/55/e0/7c6b71d327d8068cb79c05f8f45bf1b6145f7a0de23bbebe63578fe5240a/coverage-7.12.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ca8ecfa283764fdda3eae1bdb6afe58bf78c2c3ec2b2edcb05a671f0bba7b3f9", size = 260151, upload-time = "2025-11-18T13:33:59.597Z" },
+ { url = "https://files.pythonhosted.org/packages/49/ce/4697457d58285b7200de6b46d606ea71066c6e674571a946a6ea908fb588/coverage-7.12.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:874fe69a0785d96bd066059cd4368022cebbec1a8958f224f0016979183916e6", size = 262257, upload-time = "2025-11-18T13:34:01.166Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/33/acbc6e447aee4ceba88c15528dbe04a35fb4d67b59d393d2e0d6f1e242c1/coverage-7.12.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5b3c889c0b8b283a24d721a9eabc8ccafcfc3aebf167e4cd0d0e23bf8ec4e339", size = 264671, upload-time = "2025-11-18T13:34:02.795Z" },
+ { url = "https://files.pythonhosted.org/packages/87/ec/e2822a795c1ed44d569980097be839c5e734d4c0c1119ef8e0a073496a30/coverage-7.12.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8bb5b894b3ec09dcd6d3743229dc7f2c42ef7787dc40596ae04c0edda487371e", size = 259231, upload-time = "2025-11-18T13:34:04.397Z" },
+ { url = "https://files.pythonhosted.org/packages/72/c5/a7ec5395bb4a49c9b7ad97e63f0c92f6bf4a9e006b1393555a02dae75f16/coverage-7.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:79a44421cd5fba96aa57b5e3b5a4d3274c449d4c622e8f76882d76635501fd13", size = 262137, upload-time = "2025-11-18T13:34:06.068Z" },
+ { url = "https://files.pythonhosted.org/packages/67/0c/02c08858b764129f4ecb8e316684272972e60777ae986f3865b10940bdd6/coverage-7.12.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:33baadc0efd5c7294f436a632566ccc1f72c867f82833eb59820ee37dc811c6f", size = 259745, upload-time = "2025-11-18T13:34:08.04Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/04/4fd32b7084505f3829a8fe45c1a74a7a728cb251aaadbe3bec04abcef06d/coverage-7.12.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c406a71f544800ef7e9e0000af706b88465f3573ae8b8de37e5f96c59f689ad1", size = 258570, upload-time = "2025-11-18T13:34:09.676Z" },
+ { url = "https://files.pythonhosted.org/packages/48/35/2365e37c90df4f5342c4fa202223744119fe31264ee2924f09f074ea9b6d/coverage-7.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e71bba6a40883b00c6d571599b4627f50c360b3d0d02bfc658168936be74027b", size = 260899, upload-time = "2025-11-18T13:34:11.259Z" },
+ { url = "https://files.pythonhosted.org/packages/05/56/26ab0464ca733fa325e8e71455c58c1c374ce30f7c04cebb88eabb037b18/coverage-7.12.0-cp314-cp314t-win32.whl", hash = "sha256:9157a5e233c40ce6613dead4c131a006adfda70e557b6856b97aceed01b0e27a", size = 221313, upload-time = "2025-11-18T13:34:12.863Z" },
+ { url = "https://files.pythonhosted.org/packages/da/1c/017a3e1113ed34d998b27d2c6dba08a9e7cb97d362f0ec988fcd873dcf81/coverage-7.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e84da3a0fd233aeec797b981c51af1cabac74f9bd67be42458365b30d11b5291", size = 222423, upload-time = "2025-11-18T13:34:15.14Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/36/bcc504fdd5169301b52568802bb1b9cdde2e27a01d39fbb3b4b508ab7c2c/coverage-7.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:01d24af36fedda51c2b1aca56e4330a3710f83b02a5ff3743a6b015ffa7c9384", size = 220459, upload-time = "2025-11-18T13:34:17.222Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/a3/43b749004e3c09452e39bb56347a008f0a0668aad37324a99b5c8ca91d9e/coverage-7.12.0-py3-none-any.whl", hash = "sha256:159d50c0b12e060b15ed3d39f87ed43d4f7f7ad40b8a534f4dd331adbb51104a", size = 209503, upload-time = "2025-11-18T13:34:18.892Z" },
+]
+
+[[package]]
+name = "flowdacity-queue"
+version = "0.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "msgpack" },
+ { name = "redis", extra = ["hiredis"] },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/18/5a/6e780fb4e062b8d40f86a2020b1200ed787f57b25c3e77c6a03134e764bc/flowdacity_queue-0.1.0.tar.gz", hash = "sha256:7818775ff3b31aa835e5fa5faeddea922457eb0be7eeb9544d5b17f30e6d79b0", size = 11077, upload-time = "2025-11-26T21:56:13.475Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/bb/bb/6bbbdfb18b2d62541c8b4a93f95df204154c68b22b3d09118b482d062503/flowdacity_queue-0.1.0-py3-none-any.whl", hash = "sha256:44c3c9b25133db578034937999b088dc93766081a5ee976377c5027e04c1dee0", size = 14955, upload-time = "2025-11-26T21:56:12.273Z" },
+]
+
+[[package]]
+name = "flowdacity-queue-server"
+version = "0.1.0"
+source = { virtual = "." }
+dependencies = [
+ { name = "flowdacity-queue" },
+ { name = "httpx" },
+ { name = "msgpack" },
+ { name = "redis", extra = ["hiredis"] },
+ { name = "starlette" },
+ { name = "ujson" },
+ { name = "uvicorn" },
+]
+
+[package.dev-dependencies]
+dev = [
+ { name = "pytest" },
+ { name = "pytest-cov" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "flowdacity-queue", specifier = ">=0.1.0" },
+ { name = "httpx", specifier = ">=0.28.1" },
+ { name = "msgpack", specifier = ">=1.1.2" },
+ { name = "redis", extras = ["hiredis"], specifier = ">=7.1.0" },
+ { name = "starlette", specifier = ">=0.50.0" },
+ { name = "ujson", specifier = ">=5.11.0" },
+ { name = "uvicorn", specifier = ">=0.38.0" },
+]
+
+[package.metadata.requires-dev]
+dev = [
+ { name = "pytest", specifier = ">=9.0.1" },
+ { name = "pytest-cov", specifier = ">=7.0.0" },
+]
+
+[[package]]
+name = "h11"
+version = "0.16.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
+]
+
+[[package]]
+name = "hiredis"
+version = "3.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/65/82/d2817ce0653628e0a0cb128533f6af0dd6318a49f3f3a6a7bd1f2f2154af/hiredis-3.3.0.tar.gz", hash = "sha256:105596aad9249634361815c574351f1bd50455dc23b537c2940066c4a9dea685", size = 89048, upload-time = "2025-10-14T16:33:34.263Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/48/1c/ed28ae5d704f5c7e85b946fa327f30d269e6272c847fef7e91ba5fc86193/hiredis-3.3.0-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:5b8e1d6a2277ec5b82af5dce11534d3ed5dffeb131fd9b210bc1940643b39b5f", size = 82026, upload-time = "2025-10-14T16:32:12.004Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/9b/79f30c5c40e248291023b7412bfdef4ad9a8a92d9e9285d65d600817dac7/hiredis-3.3.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:c4981de4d335f996822419e8a8b3b87367fcef67dc5fb74d3bff4df9f6f17783", size = 46217, upload-time = "2025-10-14T16:32:13.133Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/c3/02b9ed430ad9087aadd8afcdf616717452d16271b701fa47edfe257b681e/hiredis-3.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1706480a683e328ae9ba5d704629dee2298e75016aa0207e7067b9c40cecc271", size = 41858, upload-time = "2025-10-14T16:32:13.98Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/98/b2a42878b82130a535c7aa20bc937ba2d07d72e9af3ad1ad93e837c419b5/hiredis-3.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a95cef9989736ac313639f8f545b76b60b797e44e65834aabbb54e4fad8d6c8", size = 170195, upload-time = "2025-10-14T16:32:14.728Z" },
+ { url = "https://files.pythonhosted.org/packages/66/1d/9dcde7a75115d3601b016113d9b90300726fa8e48aacdd11bf01a453c145/hiredis-3.3.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca2802934557ccc28a954414c245ba7ad904718e9712cb67c05152cf6b9dd0a3", size = 181808, upload-time = "2025-10-14T16:32:15.622Z" },
+ { url = "https://files.pythonhosted.org/packages/56/a1/60f6bda9b20b4e73c85f7f5f046bc2c154a5194fc94eb6861e1fd97ced52/hiredis-3.3.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fe730716775f61e76d75810a38ee4c349d3af3896450f1525f5a4034cf8f2ed7", size = 180578, upload-time = "2025-10-14T16:32:16.514Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/01/859d21de65085f323a701824e23ea3330a0ac05f8e184544d7aa5c26128d/hiredis-3.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:749faa69b1ce1f741f5eaf743435ac261a9262e2d2d66089192477e7708a9abc", size = 172508, upload-time = "2025-10-14T16:32:17.411Z" },
+ { url = "https://files.pythonhosted.org/packages/99/a8/28fd526e554c80853d0fbf57ef2a3235f00e4ed34ce0e622e05d27d0f788/hiredis-3.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:95c9427f2ac3f1dd016a3da4e1161fa9d82f221346c8f3fdd6f3f77d4e28946c", size = 166341, upload-time = "2025-10-14T16:32:18.561Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/91/ded746b7d2914f557fbbf77be55e90d21f34ba758ae10db6591927c642c8/hiredis-3.3.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c863ee44fe7bff25e41f3a5105c936a63938b76299b802d758f40994ab340071", size = 176765, upload-time = "2025-10-14T16:32:19.491Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/4c/04aa46ff386532cb5f08ee495c2bf07303e93c0acf2fa13850e031347372/hiredis-3.3.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2213c7eb8ad5267434891f3241c7776e3bafd92b5933fc57d53d4456247dc542", size = 170312, upload-time = "2025-10-14T16:32:20.404Z" },
+ { url = "https://files.pythonhosted.org/packages/90/6e/67f9d481c63f542a9cf4c9f0ea4e5717db0312fb6f37fb1f78f3a66de93c/hiredis-3.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a172bae3e2837d74530cd60b06b141005075db1b814d966755977c69bd882ce8", size = 167965, upload-time = "2025-10-14T16:32:21.259Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/df/dde65144d59c3c0d85e43255798f1fa0c48d413e668cfd92b3d9f87924ef/hiredis-3.3.0-cp312-cp312-win32.whl", hash = "sha256:cb91363b9fd6d41c80df9795e12fffbaf5c399819e6ae8120f414dedce6de068", size = 20533, upload-time = "2025-10-14T16:32:22.192Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/a9/55a4ac9c16fdf32e92e9e22c49f61affe5135e177ca19b014484e28950f7/hiredis-3.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:04ec150e95eea3de9ff8bac754978aa17b8bf30a86d4ab2689862020945396b0", size = 22379, upload-time = "2025-10-14T16:32:22.916Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/39/2b789ebadd1548ccb04a2c18fbc123746ad1a7e248b7f3f3cac618ca10a6/hiredis-3.3.0-cp313-cp313-macosx_10_15_universal2.whl", hash = "sha256:b7048b4ec0d5dddc8ddd03da603de0c4b43ef2540bf6e4c54f47d23e3480a4fa", size = 82035, upload-time = "2025-10-14T16:32:23.715Z" },
+ { url = "https://files.pythonhosted.org/packages/85/74/4066d9c1093be744158ede277f2a0a4e4cd0fefeaa525c79e2876e9e5c72/hiredis-3.3.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:e5f86ce5a779319c15567b79e0be806e8e92c18bb2ea9153e136312fafa4b7d6", size = 46219, upload-time = "2025-10-14T16:32:24.554Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/3f/f9e0f6d632f399d95b3635703e1558ffaa2de3aea4cfcbc2d7832606ba43/hiredis-3.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fbdb97a942e66016fff034df48a7a184e2b7dc69f14c4acd20772e156f20d04b", size = 41860, upload-time = "2025-10-14T16:32:25.356Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/c5/b7dde5ec390dabd1cabe7b364a509c66d4e26de783b0b64cf1618f7149fc/hiredis-3.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0fb4bea72fe45ff13e93ddd1352b43ff0749f9866263b5cca759a4c960c776f", size = 170094, upload-time = "2025-10-14T16:32:26.148Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/d6/7f05c08ee74d41613be466935688068e07f7b6c55266784b5ace7b35b766/hiredis-3.3.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:85b9baf98050e8f43c2826ab46aaf775090d608217baf7af7882596aef74e7f9", size = 181746, upload-time = "2025-10-14T16:32:27.844Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/d2/aaf9f8edab06fbf5b766e0cae3996324297c0516a91eb2ca3bd1959a0308/hiredis-3.3.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:69079fb0f0ebb61ba63340b9c4bce9388ad016092ca157e5772eb2818209d930", size = 180465, upload-time = "2025-10-14T16:32:29.185Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/1e/93ded8b9b484519b211fc71746a231af98c98928e3ebebb9086ed20bb1ad/hiredis-3.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c17f77b79031ea4b0967d30255d2ae6e7df0603ee2426ad3274067f406938236", size = 172419, upload-time = "2025-10-14T16:32:30.059Z" },
+ { url = "https://files.pythonhosted.org/packages/68/13/02880458e02bbfcedcaabb8f7510f9dda1c89d7c1921b1bb28c22bb38cbf/hiredis-3.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:45d14f745fc177bc05fc24bdf20e2b515e9a068d3d4cce90a0fb78d04c9c9d9a", size = 166400, upload-time = "2025-10-14T16:32:31.173Z" },
+ { url = "https://files.pythonhosted.org/packages/11/60/896e03267670570f19f61dc65a2137fcb2b06e83ab0911d58eeec9f3cb88/hiredis-3.3.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:ba063fdf1eff6377a0c409609cbe890389aefddfec109c2d20fcc19cfdafe9da", size = 176845, upload-time = "2025-10-14T16:32:32.12Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/90/a1d4bd0cdcf251fda72ac0bd932f547b48ad3420f89bb2ef91bf6a494534/hiredis-3.3.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:1799cc66353ad066bfdd410135c951959da9f16bcb757c845aab2f21fc4ef099", size = 170365, upload-time = "2025-10-14T16:32:33.035Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/9a/7c98f7bb76bdb4a6a6003cf8209721f083e65d2eed2b514f4a5514bda665/hiredis-3.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2cbf71a121996ffac82436b6153290815b746afb010cac19b3290a1644381b07", size = 168022, upload-time = "2025-10-14T16:32:34.81Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/ca/672ee658ffe9525558615d955b554ecd36aa185acd4431ccc9701c655c9b/hiredis-3.3.0-cp313-cp313-win32.whl", hash = "sha256:a7cbbc6026bf03659f0b25e94bbf6e64f6c8c22f7b4bc52fe569d041de274194", size = 20533, upload-time = "2025-10-14T16:32:35.7Z" },
+ { url = "https://files.pythonhosted.org/packages/20/93/511fd94f6a7b6d72a4cf9c2b159bf3d780585a9a1dca52715dd463825299/hiredis-3.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:a8def89dd19d4e2e4482b7412d453dec4a5898954d9a210d7d05f60576cedef6", size = 22387, upload-time = "2025-10-14T16:32:36.441Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/b3/b948ee76a6b2bc7e45249861646f91f29704f743b52565cf64cee9c4658b/hiredis-3.3.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c135bda87211f7af9e2fd4e046ab433c576cd17b69e639a0f5bb2eed5e0e71a9", size = 82105, upload-time = "2025-10-14T16:32:37.204Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/9b/4210f4ebfb3ab4ada964b8de08190f54cbac147198fb463cd3c111cc13e0/hiredis-3.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2f855c678230aed6fc29b962ce1cc67e5858a785ef3a3fd6b15dece0487a2e60", size = 46237, upload-time = "2025-10-14T16:32:38.07Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/7a/e38bfd7d04c05036b4ccc6f42b86b1032185cf6ae426e112a97551fece14/hiredis-3.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4059c78a930cbb33c391452ccce75b137d6f89e2eebf6273d75dafc5c2143c03", size = 41894, upload-time = "2025-10-14T16:32:38.929Z" },
+ { url = "https://files.pythonhosted.org/packages/28/d3/eae43d9609c5d9a6effef0586ee47e13a0d84b44264b688d97a75cd17ee5/hiredis-3.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:334a3f1d14c253bb092e187736c3384203bd486b244e726319bbb3f7dffa4a20", size = 170486, upload-time = "2025-10-14T16:32:40.147Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/fd/34d664554880b27741ab2916d66207357563b1639e2648685f4c84cfb755/hiredis-3.3.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd137b147235447b3d067ec952c5b9b95ca54b71837e1b38dbb2ec03b89f24fc", size = 182031, upload-time = "2025-10-14T16:32:41.06Z" },
+ { url = "https://files.pythonhosted.org/packages/08/a3/0c69fdde3f4155b9f7acc64ccffde46f312781469260061b3bbaa487fd34/hiredis-3.3.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8f88f4f2aceb73329ece86a1cb0794fdbc8e6d614cb5ca2d1023c9b7eb432db8", size = 180542, upload-time = "2025-10-14T16:32:42.993Z" },
+ { url = "https://files.pythonhosted.org/packages/68/7a/ad5da4d7bc241e57c5b0c4fe95aa75d1f2116e6e6c51577394d773216e01/hiredis-3.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:550f4d1538822fc75ebf8cf63adc396b23d4958bdbbad424521f2c0e3dfcb169", size = 172353, upload-time = "2025-10-14T16:32:43.965Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/dc/c46eace64eb047a5b31acd5e4b0dc6d2f0390a4a3f6d507442d9efa570ad/hiredis-3.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:54b14211fbd5930fc696f6fcd1f1f364c660970d61af065a80e48a1fa5464dd6", size = 166435, upload-time = "2025-10-14T16:32:44.97Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/ac/ad13a714e27883a2e4113c980c94caf46b801b810de5622c40f8d3e8335f/hiredis-3.3.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9e96f63dbc489fc86f69951e9f83dadb9582271f64f6822c47dcffa6fac7e4a", size = 177218, upload-time = "2025-10-14T16:32:45.936Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/38/268fabd85b225271fe1ba82cb4a484fcc1bf922493ff2c74b400f1a6f339/hiredis-3.3.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:106e99885d46684d62ab3ec1d6b01573cc0e0083ac295b11aaa56870b536c7ec", size = 170477, upload-time = "2025-10-14T16:32:46.898Z" },
+ { url = "https://files.pythonhosted.org/packages/20/6b/02bb8af810ea04247334ab7148acff7a61c08a8832830c6703f464be83a9/hiredis-3.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:087e2ef3206361281b1a658b5b4263572b6ba99465253e827796964208680459", size = 167915, upload-time = "2025-10-14T16:32:47.847Z" },
+ { url = "https://files.pythonhosted.org/packages/83/94/901fa817e667b2e69957626395e6dee416e31609dca738f28e6b545ca6c2/hiredis-3.3.0-cp314-cp314-win32.whl", hash = "sha256:80638ebeab1cefda9420e9fedc7920e1ec7b4f0513a6b23d58c9d13c882f8065", size = 21165, upload-time = "2025-10-14T16:32:50.753Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/7e/4881b9c1d0b4cdaba11bd10e600e97863f977ea9d67c5988f7ec8cd363e5/hiredis-3.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:a68aaf9ba024f4e28cf23df9196ff4e897bd7085872f3a30644dca07fa787816", size = 22996, upload-time = "2025-10-14T16:32:51.543Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/b6/d7e6c17da032665a954a89c1e6ee3bd12cb51cd78c37527842b03519981d/hiredis-3.3.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:f7f80442a32ce51ee5d89aeb5a84ee56189a0e0e875f1a57bbf8d462555ae48f", size = 83034, upload-time = "2025-10-14T16:32:52.395Z" },
+ { url = "https://files.pythonhosted.org/packages/27/6c/6751b698060cdd1b2d8427702cff367c9ed7a1705bcf3792eb5b896f149b/hiredis-3.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a1a67530da714954ed50579f4fe1ab0ddbac9c43643b1721c2cb226a50dde263", size = 46701, upload-time = "2025-10-14T16:32:53.572Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/8e/20a5cf2c83c7a7e08c76b9abab113f99f71cd57468a9c7909737ce6e9bf8/hiredis-3.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:616868352e47ab355559adca30f4f3859f9db895b4e7bc71e2323409a2add751", size = 42381, upload-time = "2025-10-14T16:32:54.762Z" },
+ { url = "https://files.pythonhosted.org/packages/be/0a/547c29c06e8c9c337d0df3eec39da0cf1aad701daf8a9658dd37f25aca66/hiredis-3.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e799b79f3150083e9702fc37e6243c0bd47a443d6eae3f3077b0b3f510d6a145", size = 180313, upload-time = "2025-10-14T16:32:55.644Z" },
+ { url = "https://files.pythonhosted.org/packages/89/8a/488de5469e3d0921a1c425045bf00e983d48b2111a90e47cf5769eaa536c/hiredis-3.3.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9ef1dfb0d2c92c3701655e2927e6bbe10c499aba632c7ea57b6392516df3864b", size = 190488, upload-time = "2025-10-14T16:32:56.649Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/59/8493edc3eb9ae0dbea2b2230c2041a52bc03e390b02ffa3ac0bca2af9aea/hiredis-3.3.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c290da6bc2a57e854c7da9956cd65013483ede935677e84560da3b848f253596", size = 189210, upload-time = "2025-10-14T16:32:57.759Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/de/8c9a653922057b32fb1e2546ecd43ef44c9aa1a7cf460c87cae507eb2bc7/hiredis-3.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd8c438d9e1728f0085bf9b3c9484d19ec31f41002311464e75b69550c32ffa8", size = 180972, upload-time = "2025-10-14T16:32:58.737Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/a3/51e6e6afaef2990986d685ca6e254ffbd191f1635a59b2d06c9e5d10c8a2/hiredis-3.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1bbc6b8a88bbe331e3ebf6685452cebca6dfe6d38a6d4efc5651d7e363ba28bd", size = 175315, upload-time = "2025-10-14T16:32:59.774Z" },
+ { url = "https://files.pythonhosted.org/packages/96/54/e436312feb97601f70f8b39263b8da5ac4a5d18305ebdfb08ad7621f6119/hiredis-3.3.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:55d8c18fe9a05496c5c04e6eccc695169d89bf358dff964bcad95696958ec05f", size = 185653, upload-time = "2025-10-14T16:33:00.749Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/a3/88e66030d066337c6c0f883a912c6d4b2d6d7173490fbbc113a6cbe414ff/hiredis-3.3.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:4ddc79afa76b805d364e202a754666cb3c4d9c85153cbfed522871ff55827838", size = 179032, upload-time = "2025-10-14T16:33:01.711Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/1f/fb7375467e9adaa371cd617c2984fefe44bdce73add4c70b8dd8cab1b33a/hiredis-3.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8e8a4b8540581dcd1b2b25827a54cfd538e0afeaa1a0e3ca87ad7126965981cc", size = 176127, upload-time = "2025-10-14T16:33:02.793Z" },
+ { url = "https://files.pythonhosted.org/packages/66/14/0dc2b99209c400f3b8f24067273e9c3cb383d894e155830879108fb19e98/hiredis-3.3.0-cp314-cp314t-win32.whl", hash = "sha256:298593bb08487753b3afe6dc38bac2532e9bac8dcee8d992ef9977d539cc6776", size = 22024, upload-time = "2025-10-14T16:33:03.812Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/2f/8a0befeed8bbe142d5a6cf3b51e8cbe019c32a64a596b0ebcbc007a8f8f1/hiredis-3.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b442b6ab038a6f3b5109874d2514c4edf389d8d8b553f10f12654548808683bc", size = 23808, upload-time = "2025-10-14T16:33:04.965Z" },
+]
+
+[[package]]
+name = "httpcore"
+version = "1.0.9"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "h11" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
+]
+
+[[package]]
+name = "httpx"
+version = "0.28.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "certifi" },
+ { name = "httpcore" },
+ { name = "idna" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
+]
+
+[[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 = "msgpack"
+version = "1.1.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/4d/f2/bfb55a6236ed8725a96b0aa3acbd0ec17588e6a2c3b62a93eb513ed8783f/msgpack-1.1.2.tar.gz", hash = "sha256:3b60763c1373dd60f398488069bcdc703cd08a711477b5d480eecc9f9626f47e", size = 173581, upload-time = "2025-10-08T09:15:56.596Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ad/bd/8b0d01c756203fbab65d265859749860682ccd2a59594609aeec3a144efa/msgpack-1.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:70a0dff9d1f8da25179ffcf880e10cf1aad55fdb63cd59c9a49a1b82290062aa", size = 81939, upload-time = "2025-10-08T09:15:01.472Z" },
+ { url = "https://files.pythonhosted.org/packages/34/68/ba4f155f793a74c1483d4bdef136e1023f7bcba557f0db4ef3db3c665cf1/msgpack-1.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:446abdd8b94b55c800ac34b102dffd2f6aa0ce643c55dfc017ad89347db3dbdb", size = 85064, upload-time = "2025-10-08T09:15:03.764Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/60/a064b0345fc36c4c3d2c743c82d9100c40388d77f0b48b2f04d6041dbec1/msgpack-1.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c63eea553c69ab05b6747901b97d620bb2a690633c77f23feb0c6a947a8a7b8f", size = 417131, upload-time = "2025-10-08T09:15:05.136Z" },
+ { url = "https://files.pythonhosted.org/packages/65/92/a5100f7185a800a5d29f8d14041f61475b9de465ffcc0f3b9fba606e4505/msgpack-1.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:372839311ccf6bdaf39b00b61288e0557916c3729529b301c52c2d88842add42", size = 427556, upload-time = "2025-10-08T09:15:06.837Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/87/ffe21d1bf7d9991354ad93949286f643b2bb6ddbeab66373922b44c3b8cc/msgpack-1.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2929af52106ca73fcb28576218476ffbb531a036c2adbcf54a3664de124303e9", size = 404920, upload-time = "2025-10-08T09:15:08.179Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/41/8543ed2b8604f7c0d89ce066f42007faac1eaa7d79a81555f206a5cdb889/msgpack-1.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be52a8fc79e45b0364210eef5234a7cf8d330836d0a64dfbb878efa903d84620", size = 415013, upload-time = "2025-10-08T09:15:09.83Z" },
+ { url = "https://files.pythonhosted.org/packages/41/0d/2ddfaa8b7e1cee6c490d46cb0a39742b19e2481600a7a0e96537e9c22f43/msgpack-1.1.2-cp312-cp312-win32.whl", hash = "sha256:1fff3d825d7859ac888b0fbda39a42d59193543920eda9d9bea44d958a878029", size = 65096, upload-time = "2025-10-08T09:15:11.11Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/ec/d431eb7941fb55a31dd6ca3404d41fbb52d99172df2e7707754488390910/msgpack-1.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1de460f0403172cff81169a30b9a92b260cb809c4cb7e2fc79ae8d0510c78b6b", size = 72708, upload-time = "2025-10-08T09:15:12.554Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/31/5b1a1f70eb0e87d1678e9624908f86317787b536060641d6798e3cf70ace/msgpack-1.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:be5980f3ee0e6bd44f3a9e9dea01054f175b50c3e6cdb692bc9424c0bbb8bf69", size = 64119, upload-time = "2025-10-08T09:15:13.589Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/31/b46518ecc604d7edf3a4f94cb3bf021fc62aa301f0cb849936968164ef23/msgpack-1.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4efd7b5979ccb539c221a4c4e16aac1a533efc97f3b759bb5a5ac9f6d10383bf", size = 81212, upload-time = "2025-10-08T09:15:14.552Z" },
+ { url = "https://files.pythonhosted.org/packages/92/dc/c385f38f2c2433333345a82926c6bfa5ecfff3ef787201614317b58dd8be/msgpack-1.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42eefe2c3e2af97ed470eec850facbe1b5ad1d6eacdbadc42ec98e7dcf68b4b7", size = 84315, upload-time = "2025-10-08T09:15:15.543Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/68/93180dce57f684a61a88a45ed13047558ded2be46f03acb8dec6d7c513af/msgpack-1.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fdf7d83102bf09e7ce3357de96c59b627395352a4024f6e2458501f158bf999", size = 412721, upload-time = "2025-10-08T09:15:16.567Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/ba/459f18c16f2b3fc1a1ca871f72f07d70c07bf768ad0a507a698b8052ac58/msgpack-1.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fac4be746328f90caa3cd4bc67e6fe36ca2bf61d5c6eb6d895b6527e3f05071e", size = 424657, upload-time = "2025-10-08T09:15:17.825Z" },
+ { url = "https://files.pythonhosted.org/packages/38/f8/4398c46863b093252fe67368b44edc6c13b17f4e6b0e4929dbf0bdb13f23/msgpack-1.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fffee09044073e69f2bad787071aeec727183e7580443dfeb8556cbf1978d162", size = 402668, upload-time = "2025-10-08T09:15:19.003Z" },
+ { url = "https://files.pythonhosted.org/packages/28/ce/698c1eff75626e4124b4d78e21cca0b4cc90043afb80a507626ea354ab52/msgpack-1.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5928604de9b032bc17f5099496417f113c45bc6bc21b5c6920caf34b3c428794", size = 419040, upload-time = "2025-10-08T09:15:20.183Z" },
+ { url = "https://files.pythonhosted.org/packages/67/32/f3cd1667028424fa7001d82e10ee35386eea1408b93d399b09fb0aa7875f/msgpack-1.1.2-cp313-cp313-win32.whl", hash = "sha256:a7787d353595c7c7e145e2331abf8b7ff1e6673a6b974ded96e6d4ec09f00c8c", size = 65037, upload-time = "2025-10-08T09:15:21.416Z" },
+ { url = "https://files.pythonhosted.org/packages/74/07/1ed8277f8653c40ebc65985180b007879f6a836c525b3885dcc6448ae6cb/msgpack-1.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:a465f0dceb8e13a487e54c07d04ae3ba131c7c5b95e2612596eafde1dccf64a9", size = 72631, upload-time = "2025-10-08T09:15:22.431Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/db/0314e4e2db56ebcf450f277904ffd84a7988b9e5da8d0d61ab2d057df2b6/msgpack-1.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:e69b39f8c0aa5ec24b57737ebee40be647035158f14ed4b40e6f150077e21a84", size = 64118, upload-time = "2025-10-08T09:15:23.402Z" },
+ { url = "https://files.pythonhosted.org/packages/22/71/201105712d0a2ff07b7873ed3c220292fb2ea5120603c00c4b634bcdafb3/msgpack-1.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e23ce8d5f7aa6ea6d2a2b326b4ba46c985dbb204523759984430db7114f8aa00", size = 81127, upload-time = "2025-10-08T09:15:24.408Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/9f/38ff9e57a2eade7bf9dfee5eae17f39fc0e998658050279cbb14d97d36d9/msgpack-1.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c15b7d74c939ebe620dd8e559384be806204d73b4f9356320632d783d1f7939", size = 84981, upload-time = "2025-10-08T09:15:25.812Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/a9/3536e385167b88c2cc8f4424c49e28d49a6fc35206d4a8060f136e71f94c/msgpack-1.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99e2cb7b9031568a2a5c73aa077180f93dd2e95b4f8d3b8e14a73ae94a9e667e", size = 411885, upload-time = "2025-10-08T09:15:27.22Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/40/dc34d1a8d5f1e51fc64640b62b191684da52ca469da9cd74e84936ffa4a6/msgpack-1.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:180759d89a057eab503cf62eeec0aa61c4ea1200dee709f3a8e9397dbb3b6931", size = 419658, upload-time = "2025-10-08T09:15:28.4Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/ef/2b92e286366500a09a67e03496ee8b8ba00562797a52f3c117aa2b29514b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:04fb995247a6e83830b62f0b07bf36540c213f6eac8e851166d8d86d83cbd014", size = 403290, upload-time = "2025-10-08T09:15:29.764Z" },
+ { url = "https://files.pythonhosted.org/packages/78/90/e0ea7990abea5764e4655b8177aa7c63cdfa89945b6e7641055800f6c16b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8e22ab046fa7ede9e36eeb4cfad44d46450f37bb05d5ec482b02868f451c95e2", size = 415234, upload-time = "2025-10-08T09:15:31.022Z" },
+ { url = "https://files.pythonhosted.org/packages/72/4e/9390aed5db983a2310818cd7d3ec0aecad45e1f7007e0cda79c79507bb0d/msgpack-1.1.2-cp314-cp314-win32.whl", hash = "sha256:80a0ff7d4abf5fecb995fcf235d4064b9a9a8a40a3ab80999e6ac1e30b702717", size = 66391, upload-time = "2025-10-08T09:15:32.265Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/f1/abd09c2ae91228c5f3998dbd7f41353def9eac64253de3c8105efa2082f7/msgpack-1.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:9ade919fac6a3e7260b7f64cea89df6bec59104987cbea34d34a2fa15d74310b", size = 73787, upload-time = "2025-10-08T09:15:33.219Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/b0/9d9f667ab48b16ad4115c1935d94023b82b3198064cb84a123e97f7466c1/msgpack-1.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:59415c6076b1e30e563eb732e23b994a61c159cec44deaf584e5cc1dd662f2af", size = 66453, upload-time = "2025-10-08T09:15:34.225Z" },
+ { url = "https://files.pythonhosted.org/packages/16/67/93f80545eb1792b61a217fa7f06d5e5cb9e0055bed867f43e2b8e012e137/msgpack-1.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:897c478140877e5307760b0ea66e0932738879e7aa68144d9b78ea4c8302a84a", size = 85264, upload-time = "2025-10-08T09:15:35.61Z" },
+ { url = "https://files.pythonhosted.org/packages/87/1c/33c8a24959cf193966ef11a6f6a2995a65eb066bd681fd085afd519a57ce/msgpack-1.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a668204fa43e6d02f89dbe79a30b0d67238d9ec4c5bd8a940fc3a004a47b721b", size = 89076, upload-time = "2025-10-08T09:15:36.619Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/6b/62e85ff7193663fbea5c0254ef32f0c77134b4059f8da89b958beb7696f3/msgpack-1.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5559d03930d3aa0f3aacb4c42c776af1a2ace2611871c84a75afe436695e6245", size = 435242, upload-time = "2025-10-08T09:15:37.647Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/47/5c74ecb4cc277cf09f64e913947871682ffa82b3b93c8dad68083112f412/msgpack-1.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70c5a7a9fea7f036b716191c29047374c10721c389c21e9ffafad04df8c52c90", size = 432509, upload-time = "2025-10-08T09:15:38.794Z" },
+ { url = "https://files.pythonhosted.org/packages/24/a4/e98ccdb56dc4e98c929a3f150de1799831c0a800583cde9fa022fa90602d/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f2cb069d8b981abc72b41aea1c580ce92d57c673ec61af4c500153a626cb9e20", size = 415957, upload-time = "2025-10-08T09:15:40.238Z" },
+ { url = "https://files.pythonhosted.org/packages/da/28/6951f7fb67bc0a4e184a6b38ab71a92d9ba58080b27a77d3e2fb0be5998f/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d62ce1f483f355f61adb5433ebfd8868c5f078d1a52d042b0a998682b4fa8c27", size = 422910, upload-time = "2025-10-08T09:15:41.505Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/03/42106dcded51f0a0b5284d3ce30a671e7bd3f7318d122b2ead66ad289fed/msgpack-1.1.2-cp314-cp314t-win32.whl", hash = "sha256:1d1418482b1ee984625d88aa9585db570180c286d942da463533b238b98b812b", size = 75197, upload-time = "2025-10-08T09:15:42.954Z" },
+ { url = "https://files.pythonhosted.org/packages/15/86/d0071e94987f8db59d4eeb386ddc64d0bb9b10820a8d82bcd3e53eeb2da6/msgpack-1.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:5a46bf7e831d09470ad92dff02b8b1ac92175ca36b087f904a0519857c6be3ff", size = 85772, upload-time = "2025-10-08T09:15:43.954Z" },
+ { url = "https://files.pythonhosted.org/packages/81/f2/08ace4142eb281c12701fc3b93a10795e4d4dc7f753911d836675050f886/msgpack-1.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d99ef64f349d5ec3293688e91486c5fdb925ed03807f64d98d205d2713c60b46", size = 70868, upload-time = "2025-10-08T09:15:44.959Z" },
+]
+
+[[package]]
+name = "packaging"
+version = "25.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
+]
+
+[[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 = "pygments"
+version = "2.19.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
+]
+
+[[package]]
+name = "pytest"
+version = "9.0.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+ { name = "iniconfig" },
+ { name = "packaging" },
+ { name = "pluggy" },
+ { name = "pygments" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/07/56/f013048ac4bc4c1d9be45afd4ab209ea62822fb1598f40687e6bf45dcea4/pytest-9.0.1.tar.gz", hash = "sha256:3e9c069ea73583e255c3b21cf46b8d3c56f6e3a1a8f6da94ccb0fcf57b9d73c8", size = 1564125, upload-time = "2025-11-12T13:05:09.333Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0b/8b/6300fb80f858cda1c51ffa17075df5d846757081d11ab4aa35cef9e6258b/pytest-9.0.1-py3-none-any.whl", hash = "sha256:67be0030d194df2dfa7b556f2e56fb3c3315bd5c8822c6951162b92b32ce7dad", size = 373668, upload-time = "2025-11-12T13:05:07.379Z" },
+]
+
+[[package]]
+name = "pytest-cov"
+version = "7.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "coverage" },
+ { name = "pluggy" },
+ { name = "pytest" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" },
+]
+
+[[package]]
+name = "redis"
+version = "7.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/43/c8/983d5c6579a411d8a99bc5823cc5712768859b5ce2c8afe1a65b37832c81/redis-7.1.0.tar.gz", hash = "sha256:b1cc3cfa5a2cb9c2ab3ba700864fb0ad75617b41f01352ce5779dabf6d5f9c3c", size = 4796669, upload-time = "2025-11-19T15:54:39.961Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/89/f0/8956f8a86b20d7bb9d6ac0187cf4cd54d8065bc9a1a09eb8011d4d326596/redis-7.1.0-py3-none-any.whl", hash = "sha256:23c52b208f92b56103e17c5d06bdc1a6c2c0b3106583985a76a18f83b265de2b", size = 354159, upload-time = "2025-11-19T15:54:38.064Z" },
+]
+
+[package.optional-dependencies]
+hiredis = [
+ { name = "hiredis" },
+]
+
+[[package]]
+name = "sniffio"
+version = "1.3.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
+]
+
+[[package]]
+name = "starlette"
+version = "0.50.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ba/b8/73a0e6a6e079a9d9cfa64113d771e421640b6f679a52eeb9b32f72d871a1/starlette-0.50.0.tar.gz", hash = "sha256:a2a17b22203254bcbc2e1f926d2d55f3f9497f769416b3190768befe598fa3ca", size = 2646985, upload-time = "2025-11-01T15:25:27.516Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033, upload-time = "2025-11-01T15:25:25.461Z" },
+]
+
+[[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 = "ujson"
+version = "5.11.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/43/d9/3f17e3c5773fb4941c68d9a37a47b1a79c9649d6c56aefbed87cc409d18a/ujson-5.11.0.tar.gz", hash = "sha256:e204ae6f909f099ba6b6b942131cee359ddda2b6e4ea39c12eb8b991fe2010e0", size = 7156583, upload-time = "2025-08-20T11:57:02.452Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b9/ef/a9cb1fce38f699123ff012161599fb9f2ff3f8d482b4b18c43a2dc35073f/ujson-5.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7895f0d2d53bd6aea11743bd56e3cb82d729980636cd0ed9b89418bf66591702", size = 55434, upload-time = "2025-08-20T11:55:34.987Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/05/dba51a00eb30bd947791b173766cbed3492269c150a7771d2750000c965f/ujson-5.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12b5e7e22a1fe01058000d1b317d3b65cc3daf61bd2ea7a2b76721fe160fa74d", size = 53190, upload-time = "2025-08-20T11:55:36.384Z" },
+ { url = "https://files.pythonhosted.org/packages/03/3c/fd11a224f73fbffa299fb9644e425f38b38b30231f7923a088dd513aabb4/ujson-5.11.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0180a480a7d099082501cad1fe85252e4d4bf926b40960fb3d9e87a3a6fbbc80", size = 57600, upload-time = "2025-08-20T11:55:37.692Z" },
+ { url = "https://files.pythonhosted.org/packages/55/b9/405103cae24899df688a3431c776e00528bd4799e7d68820e7ebcf824f92/ujson-5.11.0-cp312-cp312-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:fa79fdb47701942c2132a9dd2297a1a85941d966d8c87bfd9e29b0cf423f26cc", size = 59791, upload-time = "2025-08-20T11:55:38.877Z" },
+ { url = "https://files.pythonhosted.org/packages/17/7b/2dcbc2bbfdbf68f2368fb21ab0f6735e872290bb604c75f6e06b81edcb3f/ujson-5.11.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8254e858437c00f17cb72e7a644fc42dad0ebb21ea981b71df6e84b1072aaa7c", size = 57356, upload-time = "2025-08-20T11:55:40.036Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/71/fea2ca18986a366c750767b694430d5ded6b20b6985fddca72f74af38a4c/ujson-5.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1aa8a2ab482f09f6c10fba37112af5f957689a79ea598399c85009f2f29898b5", size = 1036313, upload-time = "2025-08-20T11:55:41.408Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/bb/d4220bd7532eac6288d8115db51710fa2d7d271250797b0bfba9f1e755af/ujson-5.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a638425d3c6eed0318df663df44480f4a40dc87cc7c6da44d221418312f6413b", size = 1195782, upload-time = "2025-08-20T11:55:43.357Z" },
+ { url = "https://files.pythonhosted.org/packages/80/47/226e540aa38878ce1194454385701d82df538ccb5ff8db2cf1641dde849a/ujson-5.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7e3cff632c1d78023b15f7e3a81c3745cd3f94c044d1e8fa8efbd6b161997bbc", size = 1088817, upload-time = "2025-08-20T11:55:45.262Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/81/546042f0b23c9040d61d46ea5ca76f0cc5e0d399180ddfb2ae976ebff5b5/ujson-5.11.0-cp312-cp312-win32.whl", hash = "sha256:be6b0eaf92cae8cdee4d4c9e074bde43ef1c590ed5ba037ea26c9632fb479c88", size = 39757, upload-time = "2025-08-20T11:55:46.522Z" },
+ { url = "https://files.pythonhosted.org/packages/44/1b/27c05dc8c9728f44875d74b5bfa948ce91f6c33349232619279f35c6e817/ujson-5.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:b7b136cc6abc7619124fd897ef75f8e63105298b5ca9bdf43ebd0e1fa0ee105f", size = 43859, upload-time = "2025-08-20T11:55:47.987Z" },
+ { url = "https://files.pythonhosted.org/packages/22/2d/37b6557c97c3409c202c838aa9c960ca3896843b4295c4b7bb2bbd260664/ujson-5.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:6cd2df62f24c506a0ba322d5e4fe4466d47a9467b57e881ee15a31f7ecf68ff6", size = 38361, upload-time = "2025-08-20T11:55:49.122Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/ec/2de9dd371d52c377abc05d2b725645326c4562fc87296a8907c7bcdf2db7/ujson-5.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:109f59885041b14ee9569bf0bb3f98579c3fa0652317b355669939e5fc5ede53", size = 55435, upload-time = "2025-08-20T11:55:50.243Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/a4/f611f816eac3a581d8a4372f6967c3ed41eddbae4008d1d77f223f1a4e0a/ujson-5.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a31c6b8004438e8c20fc55ac1c0e07dad42941db24176fe9acf2815971f8e752", size = 53193, upload-time = "2025-08-20T11:55:51.373Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/c5/c161940967184de96f5cbbbcce45b562a4bf851d60f4c677704b1770136d/ujson-5.11.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78c684fb21255b9b90320ba7e199780f653e03f6c2528663768965f4126a5b50", size = 57603, upload-time = "2025-08-20T11:55:52.583Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/d6/c7b2444238f5b2e2d0e3dab300b9ddc3606e4b1f0e4bed5a48157cebc792/ujson-5.11.0-cp313-cp313-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:4c9f5d6a27d035dd90a146f7761c2272cf7103de5127c9ab9c4cd39ea61e878a", size = 59794, upload-time = "2025-08-20T11:55:53.69Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/a3/292551f936d3d02d9af148f53e1bc04306b00a7cf1fcbb86fa0d1c887242/ujson-5.11.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:837da4d27fed5fdc1b630bd18f519744b23a0b5ada1bbde1a36ba463f2900c03", size = 57363, upload-time = "2025-08-20T11:55:54.843Z" },
+ { url = "https://files.pythonhosted.org/packages/90/a6/82cfa70448831b1a9e73f882225980b5c689bf539ec6400b31656a60ea46/ujson-5.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:787aff4a84da301b7f3bac09bc696e2e5670df829c6f8ecf39916b4e7e24e701", size = 1036311, upload-time = "2025-08-20T11:55:56.197Z" },
+ { url = "https://files.pythonhosted.org/packages/84/5c/96e2266be50f21e9b27acaee8ca8f23ea0b85cb998c33d4f53147687839b/ujson-5.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6dd703c3e86dc6f7044c5ac0b3ae079ed96bf297974598116aa5fb7f655c3a60", size = 1195783, upload-time = "2025-08-20T11:55:58.081Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/20/78abe3d808cf3bb3e76f71fca46cd208317bf461c905d79f0d26b9df20f1/ujson-5.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3772e4fe6b0c1e025ba3c50841a0ca4786825a4894c8411bf8d3afe3a8061328", size = 1088822, upload-time = "2025-08-20T11:55:59.469Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/50/8856e24bec5e2fc7f775d867aeb7a3f137359356200ac44658f1f2c834b2/ujson-5.11.0-cp313-cp313-win32.whl", hash = "sha256:8fa2af7c1459204b7a42e98263b069bd535ea0cd978b4d6982f35af5a04a4241", size = 39753, upload-time = "2025-08-20T11:56:01.345Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/d8/1baee0f4179a4d0f5ce086832147b6cc9b7731c24ca08e14a3fdb8d39c32/ujson-5.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:34032aeca4510a7c7102bd5933f59a37f63891f30a0706fb46487ab6f0edf8f0", size = 43866, upload-time = "2025-08-20T11:56:02.552Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/8c/6d85ef5be82c6d66adced3ec5ef23353ed710a11f70b0b6a836878396334/ujson-5.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:ce076f2df2e1aa62b685086fbad67f2b1d3048369664b4cdccc50707325401f9", size = 38363, upload-time = "2025-08-20T11:56:03.688Z" },
+ { url = "https://files.pythonhosted.org/packages/28/08/4518146f4984d112764b1dfa6fb7bad691c44a401adadaa5e23ccd930053/ujson-5.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:65724738c73645db88f70ba1f2e6fb678f913281804d5da2fd02c8c5839af302", size = 55462, upload-time = "2025-08-20T11:56:04.873Z" },
+ { url = "https://files.pythonhosted.org/packages/29/37/2107b9a62168867a692654d8766b81bd2fd1e1ba13e2ec90555861e02b0c/ujson-5.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29113c003ca33ab71b1b480bde952fbab2a0b6b03a4ee4c3d71687cdcbd1a29d", size = 53246, upload-time = "2025-08-20T11:56:06.054Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/f8/25583c70f83788edbe3ca62ce6c1b79eff465d78dec5eb2b2b56b3e98b33/ujson-5.11.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c44c703842024d796b4c78542a6fcd5c3cb948b9fc2a73ee65b9c86a22ee3638", size = 57631, upload-time = "2025-08-20T11:56:07.374Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/ca/19b3a632933a09d696f10dc1b0dfa1d692e65ad507d12340116ce4f67967/ujson-5.11.0-cp314-cp314-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:e750c436fb90edf85585f5c62a35b35082502383840962c6983403d1bd96a02c", size = 59877, upload-time = "2025-08-20T11:56:08.534Z" },
+ { url = "https://files.pythonhosted.org/packages/55/7a/4572af5324ad4b2bfdd2321e898a527050290147b4ea337a79a0e4e87ec7/ujson-5.11.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f278b31a7c52eb0947b2db55a5133fbc46b6f0ef49972cd1a80843b72e135aba", size = 57363, upload-time = "2025-08-20T11:56:09.758Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/71/a2b8c19cf4e1efe53cf439cdf7198ac60ae15471d2f1040b490c1f0f831f/ujson-5.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ab2cb8351d976e788669c8281465d44d4e94413718af497b4e7342d7b2f78018", size = 1036394, upload-time = "2025-08-20T11:56:11.168Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/3e/7b98668cba3bb3735929c31b999b374ebc02c19dfa98dfebaeeb5c8597ca/ujson-5.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:090b4d11b380ae25453100b722d0609d5051ffe98f80ec52853ccf8249dfd840", size = 1195837, upload-time = "2025-08-20T11:56:12.6Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/ea/8870f208c20b43571a5c409ebb2fe9b9dba5f494e9e60f9314ac01ea8f78/ujson-5.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:80017e870d882d5517d28995b62e4e518a894f932f1e242cbc802a2fd64d365c", size = 1088837, upload-time = "2025-08-20T11:56:14.15Z" },
+ { url = "https://files.pythonhosted.org/packages/63/b6/c0e6607e37fa47929920a685a968c6b990a802dec65e9c5181e97845985d/ujson-5.11.0-cp314-cp314-win32.whl", hash = "sha256:1d663b96eb34c93392e9caae19c099ec4133ba21654b081956613327f0e973ac", size = 41022, upload-time = "2025-08-20T11:56:15.509Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/56/f4fe86b4c9000affd63e9219e59b222dc48b01c534533093e798bf617a7e/ujson-5.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:849e65b696f0d242833f1df4182096cedc50d414215d1371fca85c541fbff629", size = 45111, upload-time = "2025-08-20T11:56:16.597Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/f3/669437f0280308db4783b12a6d88c00730b394327d8334cc7a32ef218e64/ujson-5.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:e73df8648c9470af2b6a6bf5250d4744ad2cf3d774dcf8c6e31f018bdd04d764", size = 39682, upload-time = "2025-08-20T11:56:17.763Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/cd/e9809b064a89fe5c4184649adeb13c1b98652db3f8518980b04227358574/ujson-5.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:de6e88f62796372fba1de973c11138f197d3e0e1d80bcb2b8aae1e826096d433", size = 55759, upload-time = "2025-08-20T11:56:18.882Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/be/ae26a6321179ebbb3a2e2685b9007c71bcda41ad7a77bbbe164005e956fc/ujson-5.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:49e56ef8066f11b80d620985ae36869a3ff7e4b74c3b6129182ec5d1df0255f3", size = 53634, upload-time = "2025-08-20T11:56:20.012Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/e9/fb4a220ee6939db099f4cfeeae796ecb91e7584ad4d445d4ca7f994a9135/ujson-5.11.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1a325fd2c3a056cf6c8e023f74a0c478dd282a93141356ae7f16d5309f5ff823", size = 58547, upload-time = "2025-08-20T11:56:21.175Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/f8/fc4b952b8f5fea09ea3397a0bd0ad019e474b204cabcb947cead5d4d1ffc/ujson-5.11.0-cp314-cp314t-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:a0af6574fc1d9d53f4ff371f58c96673e6d988ed2b5bf666a6143c782fa007e9", size = 60489, upload-time = "2025-08-20T11:56:22.342Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/e5/af5491dfda4f8b77e24cf3da68ee0d1552f99a13e5c622f4cef1380925c3/ujson-5.11.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10f29e71ecf4ecd93a6610bd8efa8e7b6467454a363c3d6416db65de883eb076", size = 58035, upload-time = "2025-08-20T11:56:23.92Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/09/0945349dd41f25cc8c38d78ace49f14c5052c5bbb7257d2f466fa7bdb533/ujson-5.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1a0a9b76a89827a592656fe12e000cf4f12da9692f51a841a4a07aa4c7ecc41c", size = 1037212, upload-time = "2025-08-20T11:56:25.274Z" },
+ { url = "https://files.pythonhosted.org/packages/49/44/8e04496acb3d5a1cbee3a54828d9652f67a37523efa3d3b18a347339680a/ujson-5.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b16930f6a0753cdc7d637b33b4e8f10d5e351e1fb83872ba6375f1e87be39746", size = 1196500, upload-time = "2025-08-20T11:56:27.517Z" },
+ { url = "https://files.pythonhosted.org/packages/64/ae/4bc825860d679a0f208a19af2f39206dfd804ace2403330fdc3170334a2f/ujson-5.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:04c41afc195fd477a59db3a84d5b83a871bd648ef371cf8c6f43072d89144eef", size = 1089487, upload-time = "2025-08-20T11:56:29.07Z" },
+ { url = "https://files.pythonhosted.org/packages/30/ed/5a057199fb0a5deabe0957073a1c1c1c02a3e99476cd03daee98ea21fa57/ujson-5.11.0-cp314-cp314t-win32.whl", hash = "sha256:aa6d7a5e09217ff93234e050e3e380da62b084e26b9f2e277d2606406a2fc2e5", size = 41859, upload-time = "2025-08-20T11:56:30.495Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/03/b19c6176bdf1dc13ed84b886e99677a52764861b6cc023d5e7b6ebda249d/ujson-5.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:48055e1061c1bb1f79e75b4ac39e821f3f35a9b82de17fce92c3140149009bec", size = 46183, upload-time = "2025-08-20T11:56:31.574Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/ca/a0413a3874b2dc1708b8796ca895bf363292f9c70b2e8ca482b7dbc0259d/ujson-5.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:1194b943e951092db611011cb8dbdb6cf94a3b816ed07906e14d3bc6ce0e90ab", size = 40264, upload-time = "2025-08-20T11:56:32.773Z" },
+]
+
+[[package]]
+name = "uvicorn"
+version = "0.38.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+ { name = "h11" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/cb/ce/f06b84e2697fef4688ca63bdb2fdf113ca0a3be33f94488f2cadb690b0cf/uvicorn-0.38.0.tar.gz", hash = "sha256:fd97093bdd120a2609fc0d3afe931d4d4ad688b6e75f0f929fde1bc36fe0e91d", size = 80605, upload-time = "2025-10-18T13:46:44.63Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ee/d9/d88e73ca598f4f6ff671fb5fde8a32925c2e08a637303a1d12883c7305fa/uvicorn-0.38.0-py3-none-any.whl", hash = "sha256:48c0afd214ceb59340075b4a052ea1ee91c16fbc2a9b1469cca0e54566977b02", size = 68109, upload-time = "2025-10-18T13:46:42.958Z" },
+]
diff --git a/wsgi.py b/wsgi.py
deleted file mode 100644
index aa713af..0000000
--- a/wsgi.py
+++ /dev/null
@@ -1,17 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2014 Plivo Team. See LICENSE.txt for details.
-# A WSGI application driver to deploy Sharq Server using other
-# application servers like uWSGI, etc.
-
-import os
-from sharq_server import setup_server
-
-# read path from variable / default to current working directory
-sharq_config_path = os.environ.get('SHARQ_CONFIG', './sharq.conf')
-sharq_config_path = os.path.abspath(sharq_config_path)
-server = setup_server(sharq_config_path)
-app = server.app
-
-
-if __name__ == '__main__':
- app.run()