From 9b7fd953d3dd92d54e5d4906802fb15306939fd4 Mon Sep 17 00:00:00 2001 From: "Ochui, Princewill Patrick" Date: Wed, 26 Nov 2025 15:06:19 +0100 Subject: [PATCH 01/10] Remove deprecated configuration files and scripts --- .pre-commit-config.yaml | 11 ------- .secrets.baseline | 40 ----------------------- CHANGELOG | 11 ------- Jenkinsfile | 9 ------ Makefile | 24 -------------- catalog-mms-mpssharq.yaml | 18 ----------- catalog-mms-npssharq.yaml | 18 ----------- catalog-npssharq-clustered.yaml | 18 ----------- catalog-rcs-mpssharq.yaml | 18 ----------- catalog-rcs-npssharq.yaml | 18 ----------- catalog-sharq-clustered.yaml | 18 ----------- catalog-washarq-clustered.yaml | 18 ----------- ci/Dockerfile | 27 ---------------- ci/config.yml | 8 ----- ci/entrypoint.sh | 34 -------------------- src/config/nginx-sharq.conf | 47 --------------------------- src/config/nginx.conf | 53 ------------------------------- src/config/sharq-server-basicauth | 3 -- src/config/sharq.conf.ctmpl | 24 -------------- src/config/sharq.ini.ctmpl | 39 ----------------------- src/config/supervisord.conf | 18 ----------- 21 files changed, 474 deletions(-) delete mode 100644 .pre-commit-config.yaml delete mode 100644 .secrets.baseline delete mode 100644 CHANGELOG delete mode 100644 Jenkinsfile delete mode 100644 Makefile delete mode 100644 catalog-mms-mpssharq.yaml delete mode 100644 catalog-mms-npssharq.yaml delete mode 100644 catalog-npssharq-clustered.yaml delete mode 100644 catalog-rcs-mpssharq.yaml delete mode 100644 catalog-rcs-npssharq.yaml delete mode 100644 catalog-sharq-clustered.yaml delete mode 100644 catalog-washarq-clustered.yaml delete mode 100644 ci/Dockerfile delete mode 100644 ci/config.yml delete mode 100644 ci/entrypoint.sh delete mode 100644 src/config/nginx-sharq.conf delete mode 100644 src/config/nginx.conf delete mode 100644 src/config/sharq-server-basicauth delete mode 100644 src/config/sharq.conf.ctmpl delete mode 100644 src/config/sharq.ini.ctmpl delete mode 100644 src/config/supervisord.conf 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/.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/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/Makefile b/Makefile deleted file mode 100644 index eb26cbc..0000000 --- a/Makefile +++ /dev/null @@ -1,24 +0,0 @@ -.PHONY: clean build install uninstall test run - -all: clean - -clean: - find . -name \*.pyc -delete - find . -name \*.pyo -delete - find . -name \*~ -delete - rm -rf build dist SharQServer.egg-info - -build: - python setup.py sdist - -install: - pip install dist/SharQServer-*.tar.gz - -uninstall: - yes | pip uninstall sharqserver - -test: - python -m tests - -run: - sharq-server --config sharq.conf 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/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.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 From a2b09fd49f4779cedef9a1ee1551daf4a0bc3298 Mon Sep 17 00:00:00 2001 From: "Ochui, Princewill Patrick" Date: Wed, 26 Nov 2025 23:16:00 +0100 Subject: [PATCH 02/10] Update config file reference in package manifest Replaces the previously included configuration file with an updated name, ensuring the correct config is distributed with the package. --- MANIFEST.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 3bc8d0fec529e5daaa6a73d12153f9f1e0dbfee8 Mon Sep 17 00:00:00 2001 From: "Ochui, Princewill Patrick" Date: Wed, 26 Nov 2025 23:16:54 +0100 Subject: [PATCH 03/10] Migrates SharQ Server to async FQ Server with Starlette Replaces the legacy Flask/Gevent-based SharQ Server with an async implementation using Starlette and the Flowdacity Queue (FQ) core. Updates all references, configs, docs, and tests to reflect the new naming and async API. Modernizes project setup with pyproject.toml and removes obsolete files for Flask, Gunicorn, and uWSGI. Prepares the codebase for improved performance, extensibility, and Python 3.12 compatibility. --- .gitignore | 2 +- .python-version | 1 + README.md | 14 +- asgi.py | 14 ++ sharq.conf => default.conf | 15 +- docs/Makefile | 8 +- docs/_templates/layout.html | 2 +- docs/conf.py | 14 +- docs/configuration.rst | 14 +- docs/contributing.rst | 8 +- docs/faqs.rst | 6 +- docs/gettingstarted.rst | 8 +- docs/index.rst | 4 +- docs/installation.rst | 8 +- docs/internals.rst | 2 +- fq_server/__init__.py | 3 + fq_server/server.py | 386 ++++++++++++++++++++++++++++++++++++ pyproject.toml | 14 ++ requirements.txt | 13 -- runner.py | 97 --------- setup.py | 39 ---- sharq_server/__init__.py | 3 - sharq_server/server.py | 300 ---------------------------- src/config/sharq.conf | 2 +- tests.py | 8 +- uv.lock | 306 ++++++++++++++++++++++++++++ wsgi.py | 17 -- 27 files changed, 782 insertions(+), 526 deletions(-) create mode 100644 .python-version create mode 100644 asgi.py rename sharq.conf => default.conf (59%) create mode 100644 fq_server/__init__.py create mode 100644 fq_server/server.py create mode 100644 pyproject.toml delete mode 100644 requirements.txt delete mode 100644 runner.py delete mode 100644 setup.py delete mode 100644 sharq_server/__init__.py delete mode 100644 sharq_server/server.py create mode 100644 uv.lock delete mode 100644 wsgi.py 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/.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/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/sharq.conf b/default.conf similarity index 59% rename from sharq.conf rename to default.conf index 9588f84..4f484ef 100644 --- a/sharq.conf +++ b/default.conf @@ -1,17 +1,18 @@ -[sharq] -job_expire_interval : 1000 ; in milliseconds -job_requeue_interval : 1000 ; in milliseconds -default_job_requeue_limit : 0 ; value of -1 retries infinitely +[fq] +job_expire_interval : 1000 +job_requeue_interval : 1000 +default_job_requeue_limit : 0 +enable_requeue_script : true -[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/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..48c9163 --- /dev/null +++ b/fq_server/__init__.py @@ -0,0 +1,3 @@ +from .server import FQServer, setup_server + +__version__ = '0.1.0' \ No newline at end of file diff --git a/fq_server/server.py b/fq_server/server.py new file mode 100644 index 0000000..22d5a06 --- /dev/null +++ b/fq_server/server.py @@ -0,0 +1,386 @@ +# -*- 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 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) + self.queue._initialize() + + # Starlette app with routes and startup hook + self.app = Starlette( + routes=self._build_routes(), + on_startup=[self._on_startup], + ) + + # ------------------------------------------------------------------ + # 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) + + async def _on_startup(self): + """Configure background tasks on app startup.""" + # mimic original behavior: use requeue_with_lock loop + asyncio.create_task(self.requeue_with_lock()) + + # ------------------------------------------------------------------ + # 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..1c5c111 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,14 @@ +[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", + "msgpack>=1.1.2", + "redis[hiredis]>=7.1.0", + "starlette>=0.50.0", + "ujson>=5.11.0", + "uvicorn>=0.38.0", +] 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_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/sharq.conf b/src/config/sharq.conf index 22a0ed3..39968cc 100644 --- a/src/config/sharq.conf +++ b/src/config/sharq.conf @@ -1,4 +1,4 @@ -[sharq] +[fq] job_expire_interval = 1000 job_requeue_interval = 1000 default_job_requeue_limit = -1 diff --git a/tests.py b/tests.py index d7af7db..09f539e 100644 --- a/tests.py +++ b/tests.py @@ -2,14 +2,14 @@ # Copyright (c) 2014 Plivo Team. See LICENSE.txt for details. import unittest import ujson as json -from sharq_server import setup_server +from fq_server import setup_server -class SharQServerTestCase(unittest.TestCase): +class FQServerTestCase(unittest.TestCase): def setUp(self): # get test client & redis connection - server = setup_server('./sharq.conf') + server = setup_server('./fq.conf') self.app = server.app.test_client() self.r = server.sq._r @@ -20,7 +20,7 @@ def test_root(self): response = self.app.get('/') self.assertEqual(response.status_code, 200) self.assertEqual( - json.loads(response.data), {'message': 'Hello, SharQ!'}) + json.loads(response.data), {'message': 'Hello, FQ!'}) def test_enqueue(self): request_params = { diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..f92fda9 --- /dev/null +++ b/uv.lock @@ -0,0 +1,306 @@ +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 = "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 = "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 = "msgpack" }, + { name = "redis", extra = ["hiredis"] }, + { name = "starlette" }, + { name = "ujson" }, + { name = "uvicorn" }, +] + +[package.metadata] +requires-dist = [ + { name = "flowdacity-queue", specifier = ">=0.1.0" }, + { 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]] +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 = "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 = "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 = "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() From 5eb273b04e20c52baf76f3386866934f37739489 Mon Sep 17 00:00:00 2001 From: "Ochui, Princewill Patrick" Date: Wed, 26 Nov 2025 23:35:19 +0100 Subject: [PATCH 04/10] Defers queue initialization to async startup Moves queue initialization to the async startup hook, ensuring any asynchronous setup completes before background tasks begin. Updates configuration defaults for clarity and disables Redis clustering by default. --- default.conf | 9 +++++---- fq_server/server.py | 3 ++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/default.conf b/default.conf index 4f484ef..df0c915 100644 --- a/default.conf +++ b/default.conf @@ -7,16 +7,17 @@ enable_requeue_script : true [fq-server] host : 127.0.0.1 port : 8080 -workers : 1 ; optional -accesslog : /tmp/fq.log ; optional +workers : 1 +accesslog : /tmp/fq.log [redis] db : 0 key_prefix : fq_server -conn_type : tcp_sock ; or unix_sock +conn_type : tcp_sock ;; unix connection settings unix_socket_path : /tmp/redis.sock ;; tcp connection settings port : 6379 host : 127.0.0.1 -clustered : true +password : +clustered : false diff --git a/fq_server/server.py b/fq_server/server.py index 22d5a06..29e52d3 100644 --- a/fq_server/server.py +++ b/fq_server/server.py @@ -27,7 +27,6 @@ def __init__(self, config_path: str): self.config.read(config_path) # pass the config file to configure the FQ core. self.queue = FQ(config_path) - self.queue._initialize() # Starlette app with routes and startup hook self.app = Starlette( @@ -77,6 +76,8 @@ async def requeue_with_lock(self): async def _on_startup(self): """Configure background tasks on app startup.""" + + await self.queue._initialize() # mimic original behavior: use requeue_with_lock loop asyncio.create_task(self.requeue_with_lock()) From cf19d8031909c9d3723fbe62f4cedbc3105315ba Mon Sep 17 00:00:00 2001 From: "Ochui, Princewill Patrick" Date: Thu, 27 Nov 2025 11:34:06 +0100 Subject: [PATCH 05/10] Migrates tests to async, adds Makefile and dev tooling Modernizes the test suite to use async test clients and HTTPX for improved compatibility with async frameworks. Introduces a Makefile with common dev commands and adds explicit dev dependency groups for testing tools. Enhances local and CI development workflow by improving test isolation, coverage options, and environment reproducibility. --- Makefile | 47 +++++++++++ pyproject.toml | 7 ++ tests.py | 173 --------------------------------------- tests/test_routes.py | 190 ++++++++++++++++++++++++++++++++++++++++++ uv.lock | 191 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 435 insertions(+), 173 deletions(-) create mode 100644 Makefile delete mode 100644 tests.py create mode 100644 tests/test_routes.py diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..6f28f2a --- /dev/null +++ b/Makefile @@ -0,0 +1,47 @@ +.PHONY: all clean build install uninstall test publish redis redis-down + +# Default target +all: clean build + +# Remove Python + build artifacts +clean: + 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 -m build + +# Install locally built package +install: + pip install --force-reinstall dist/*.whl + +# Uninstall FQ completely +uninstall: + pip uninstall -y flowdacity-queue + +# Run tests — prefers pytest, falls back to python modules +test: + @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 + +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/pyproject.toml b/pyproject.toml index 1c5c111..fd3bfc2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,9 +6,16 @@ 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", +] diff --git a/tests.py b/tests.py deleted file mode 100644 index 09f539e..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 fq_server import setup_server - - -class FQServerTestCase(unittest.TestCase): - - def setUp(self): - # get test client & redis connection - server = setup_server('./fq.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, FQ!'}) - - 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 index f92fda9..2fd94cb 100644 --- a/uv.lock +++ b/uv.lock @@ -16,6 +16,15 @@ 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" @@ -37,6 +46,80 @@ 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" @@ -56,6 +139,7 @@ version = "0.1.0" source = { virtual = "." } dependencies = [ { name = "flowdacity-queue" }, + { name = "httpx" }, { name = "msgpack" }, { name = "redis", extra = ["hiredis"] }, { name = "starlette" }, @@ -63,9 +147,16 @@ dependencies = [ { 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" }, @@ -73,6 +164,12 @@ requires-dist = [ { 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" @@ -142,6 +239,34 @@ wheels = [ { 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" @@ -151,6 +276,15 @@ 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" @@ -195,6 +329,63 @@ wheels = [ { 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" From 8d79a6842955fbafd7ccc52479444788f3cd077a Mon Sep 17 00:00:00 2001 From: "Ochui, Princewill Patrick" Date: Thu, 27 Nov 2025 11:44:05 +0100 Subject: [PATCH 06/10] Refactor FQServer to use async lifespan handler for startup and shutdown --- fq_server/server.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/fq_server/server.py b/fq_server/server.py index 29e52d3..1d43b04 100644 --- a/fq_server/server.py +++ b/fq_server/server.py @@ -7,6 +7,7 @@ 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 @@ -31,7 +32,7 @@ def __init__(self, config_path: str): # Starlette app with routes and startup hook self.app = Starlette( routes=self._build_routes(), - on_startup=[self._on_startup], + lifespan=self._lifespan, ) # ------------------------------------------------------------------ @@ -74,12 +75,24 @@ async def requeue_with_lock(self): finally: await asyncio.sleep(job_requeue_interval / 1000.0) - async def _on_startup(self): - """Configure background tasks on app startup.""" - + # ------------------------------------------------------------------ + # Lifespan handler + # ------------------------------------------------------------------ + @asynccontextmanager + async def _lifespan(self, app: Starlette): + # --- startup --- await self.queue._initialize() # mimic original behavior: use requeue_with_lock loop - asyncio.create_task(self.requeue_with_lock()) + 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 From bf1ee942b416e3ec37668edac53128be22aeb4c0 Mon Sep 17 00:00:00 2001 From: "Ochui, Princewill Patrick" Date: Thu, 27 Nov 2025 22:29:12 +0100 Subject: [PATCH 07/10] Updates config defaults and removes unused config file Sets the default job requeue limit to -1 to indicate no limit, makes available classes explicit in the package, and deletes an obsolete configuration file to reduce confusion and maintenance overhead. --- default.conf | 2 +- fq_server/__init__.py | 3 ++- src/config/sharq.conf | 14 -------------- 3 files changed, 3 insertions(+), 16 deletions(-) delete mode 100644 src/config/sharq.conf diff --git a/default.conf b/default.conf index df0c915..9911a70 100644 --- a/default.conf +++ b/default.conf @@ -1,7 +1,7 @@ [fq] job_expire_interval : 1000 job_requeue_interval : 1000 -default_job_requeue_limit : 0 +default_job_requeue_limit : -1 enable_requeue_script : true [fq-server] diff --git a/fq_server/__init__.py b/fq_server/__init__.py index 48c9163..1cf1a62 100644 --- a/fq_server/__init__.py +++ b/fq_server/__init__.py @@ -1,3 +1,4 @@ from .server import FQServer, setup_server -__version__ = '0.1.0' \ No newline at end of file +__version__ = '0.1.0' +__all__ = ['FQServer', 'setup_server'] \ No newline at end of file diff --git a/src/config/sharq.conf b/src/config/sharq.conf deleted file mode 100644 index 39968cc..0000000 --- a/src/config/sharq.conf +++ /dev/null @@ -1,14 +0,0 @@ -[fq] -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 From c34f2a22ee03d7125d1983a61fc0a72807930e1d Mon Sep 17 00:00:00 2001 From: "Ochui, Princewill Patrick" Date: Thu, 27 Nov 2025 22:53:03 +0100 Subject: [PATCH 08/10] Add CI workflow for tests and code coverage reporting Introduces an automated workflow to run tests with coverage on pushes and pull requests. Configures service dependencies and coverage upload to Codecov. Improves project packaging and test configuration for consistency and maintainability. --- .github/workflows/test.yml | 51 ++++++++++++++++++++++++++++++++++++++ pyproject.toml | 19 ++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..34d33a8 --- /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: + - 6380: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/pyproject.toml b/pyproject.toml index fd3bfc2..fed8980 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,3 +19,22 @@ 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", +] From 022e3011585452c6a3afb28402a8738fe85de143 Mon Sep 17 00:00:00 2001 From: "Ochui, Princewill Patrick" Date: Thu, 27 Nov 2025 23:25:13 +0100 Subject: [PATCH 09/10] Adds Docker support with Compose and config files Enables containerized development and deployment using Docker and docker-compose, including a base Python image, app and Redis services, and unified configuration for consistent local and production setups. Facilitates easier setup, orchestration, and environment management. --- Dockerfile | 28 ++++++++++++++++++++++++++++ docker-compose.yml | 29 +++++++++++++++++++++++++++++ docker.conf | 23 +++++++++++++++++++++++ 3 files changed, 80 insertions(+) create mode 100644 Dockerfile create mode 100644 docker-compose.yml create mode 100644 docker.conf 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/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 From aa1dd66eda77eb443595cd250b507aec0749df30 Mon Sep 17 00:00:00 2001 From: "Ochui, Princewill Patrick" Date: Thu, 27 Nov 2025 23:28:36 +0100 Subject: [PATCH 10/10] Fix Redis port mapping in CI workflow --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 34d33a8..eb92025 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,7 +18,7 @@ jobs: redis: image: redis:7-alpine ports: - - 6380:6379 + - 6379:6379 options: >- --health-cmd "redis-cli ping" --health-interval 5s