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 %} -Fork me on GitHub +Fork me on GitHub {% 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()